2021-05-04 20:53:03 +02:00
import RNS
2026-05-17 02:02:27 +02:00
import collections
2021-09-05 20:38:10 +02:00
import os
2026-03-28 18:19:11 -04:00
import shutil
2021-05-04 20:53:03 +02:00
import time
import nomadnet
2021-05-13 16:39:31 +02:00
import LXMF
2021-05-04 20:53:03 +02:00
2021-05-12 14:02:44 +02:00
import urwid
2026-03-28 18:19:11 -04:00
from datetime import datetime , timedelta
2021-05-13 16:39:31 +02:00
from nomadnet . Directory import DirectoryEntry
2026-03-28 18:32:18 -04:00
from nomadnet . Conversation import ConversationMessage
2026-03-28 18:19:11 -04:00
2026-05-06 18:44:17 +02:00
from nomadnet . util import strip_modifiers
from nomadnet . util import sanitize_name
2026-03-28 18:19:11 -04:00
def relative_time ( timestamp ) :
now = time . time ( )
delta = now - timestamp
if delta < 0 :
return " just now "
elif delta < 60 :
return " just now "
elif delta < 3600 :
m = int ( delta / 60 )
return str ( m ) + " m ago "
elif delta < 86400 :
h = int ( delta / 3600 )
return str ( h ) + " h ago "
elif delta < 172800 :
return " yesterday "
elif delta < 604800 :
d = int ( delta / 86400 )
return str ( d ) + " d ago "
elif delta < 2592000 :
w = int ( delta / 604800 )
return str ( w ) + " w ago "
else :
return datetime . fromtimestamp ( timestamp ) . strftime ( " % Y- % m- %d " )
def _format_size ( size ) :
if size < 1024 :
return str ( size ) + " B "
elif size < 1048576 :
return str ( round ( size / 1024 , 1 ) ) + " KB "
else :
return str ( round ( size / 1048576 , 1 ) ) + " MB "
2021-05-14 17:36:35 +02:00
from nomadnet . vendor . additional_urwid_widgets import IndicativeListBox
2021-05-12 14:02:44 +02:00
class ConversationListDisplayShortcuts ( ) :
def __init__ ( self , app ) :
self . app = app
2026-03-28 18:19:11 -04:00
self . widget = urwid . AttrMap ( urwid . Text ( " [C-e] Peer Info [C-x] Delete [C-r] Sync [C-n] New [C-u] Ingest URI [C-o] Sort [C-g] Fullscreen " ) , " shortcutbar " )
2021-05-12 14:02:44 +02:00
class ConversationDisplayShortcuts ( ) :
2021-05-04 11:08:16 +02:00
def __init__ ( self , app ) :
self . app = app
2026-03-28 18:19:11 -04:00
self . widget = urwid . AttrMap ( urwid . Text ( " [C-d] Send [C-p] Paper Msg [C-t] Title [C-a] Attach [C-s] Save [C-k] Clear [C-w] Close [C-u] Purge [C-x] Clear History [C-o] Sort " ) , " shortcutbar " )
2021-05-13 16:39:31 +02:00
class ConversationsArea ( urwid . LineBox ) :
def keypress ( self , size , key ) :
2021-05-14 14:45:20 +02:00
if key == " ctrl e " :
self . delegate . edit_selected_in_directory ( )
2021-05-13 16:39:31 +02:00
elif key == " ctrl x " :
self . delegate . delete_selected_conversation ( )
elif key == " ctrl n " :
self . delegate . new_conversation ( )
2022-11-19 20:04:01 +01:00
elif key == " ctrl u " :
2022-11-22 19:55:17 +01:00
self . delegate . ingest_lxm_uri ( )
2021-10-07 18:14:06 +02:00
elif key == " ctrl r " :
self . delegate . sync_conversations ( )
2022-07-04 17:34:12 +02:00
elif key == " ctrl g " :
self . delegate . toggle_fullscreen ( )
2026-03-28 18:19:11 -04:00
elif key == " ctrl o " :
self . delegate . toggle_list_sort ( )
2021-05-14 17:36:35 +02:00
elif key == " tab " :
2024-01-18 11:58:22 +01:00
self . delegate . app . ui . main_display . frame . focus_position = " header "
2021-05-19 09:26:12 +02:00
elif key == " up " and ( self . delegate . ilb . first_item_is_selected ( ) or self . delegate . ilb . body_is_empty ( ) ) :
2024-01-18 11:58:22 +01:00
self . delegate . app . ui . main_display . frame . focus_position = " header "
2021-05-13 16:39:31 +02:00
else :
return super ( ConversationsArea , self ) . keypress ( size , key )
2021-05-04 11:08:16 +02:00
2021-05-14 22:12:42 +02:00
class DialogLineBox ( urwid . LineBox ) :
def keypress ( self , size , key ) :
if key == " esc " :
2026-03-28 18:19:11 -04:00
if hasattr ( self . delegate , " update_conversation_list " ) :
self . delegate . update_conversation_list ( )
elif hasattr ( self . delegate , " dialog_active " ) :
self . delegate . dialog_active = False
self . delegate . conversation_changed ( None )
2021-05-14 22:12:42 +02:00
else :
return super ( DialogLineBox , self ) . keypress ( size , key )
2021-05-04 11:08:16 +02:00
class ConversationsDisplay ( ) :
2021-05-04 20:53:03 +02:00
list_width = 0.33
2022-07-04 17:07:54 +02:00
given_list_width = 52
2021-05-04 20:53:03 +02:00
cached_conversation_widgets = { }
2026-03-28 18:19:11 -04:00
SORT_RECENT = 0
SORT_NAME = 1
2021-05-04 11:08:16 +02:00
def __init__ ( self , app ) :
self . app = app
2021-05-13 16:39:31 +02:00
self . dialog_open = False
2021-10-07 18:14:06 +02:00
self . sync_dialog = None
2021-05-15 21:08:30 +02:00
self . currently_displayed_conversation = None
2026-03-28 18:19:11 -04:00
self . list_sort_mode = ConversationsDisplay . SORT_RECENT
2021-05-04 15:10:21 +02:00
2021-05-12 14:02:44 +02:00
def disp_list_shortcuts ( sender , arg1 , arg2 ) :
self . shortcuts_display = self . list_shortcuts
self . app . ui . main_display . update_active_shortcuts ( )
2021-05-13 16:39:31 +02:00
self . update_listbox ( )
2021-05-04 15:10:21 +02:00
2021-05-15 21:08:30 +02:00
self . columns_widget = urwid . Columns (
[
2024-01-18 11:58:22 +01:00
# (urwid.WEIGHT, ConversationsDisplay.list_width, self.listbox),
# (urwid.WEIGHT, 1-ConversationsDisplay.list_width, self.make_conversation_widget(None))
2022-07-04 17:07:54 +02:00
( ConversationsDisplay . given_list_width , self . listbox ) ,
2024-01-18 11:58:22 +01:00
( urwid . WEIGHT , 1 , self . make_conversation_widget ( None ) )
2021-05-15 21:08:30 +02:00
] ,
dividechars = 0 , focus_column = 0 , box_columns = [ 0 ]
)
2021-05-04 11:08:16 +02:00
2021-05-12 14:02:44 +02:00
self . list_shortcuts = ConversationListDisplayShortcuts ( self . app )
self . editor_shortcuts = ConversationDisplayShortcuts ( self . app )
self . shortcuts_display = self . list_shortcuts
2021-05-13 16:39:31 +02:00
self . widget = self . columns_widget
2026-05-17 02:02:27 +02:00
self . _pending_actions = collections . deque ( )
self . _wake_fd = None
try :
self . _wake_fd = self . app . ui . loop . watch_pipe ( self . _process_pending )
except Exception :
pass
nomadnet . Conversation . created_callback = lambda : self . _wake ( self . update_conversation_list )
def _process_pending ( self , data ) :
while True :
try :
action = self . _pending_actions . popleft ( )
except IndexError :
break
try :
action ( )
except Exception as e :
RNS . log ( " Conversations UI action failed: " + str ( e ) , RNS . LOG_ERROR )
return True
def _wake ( self , action ) :
self . _pending_actions . append ( action )
if self . _wake_fd is not None :
try :
os . write ( self . _wake_fd , b " . " )
return
except Exception :
pass
try :
self . app . ui . loop . set_alarm_in ( 0.0 , lambda l , d : self . _process_pending ( None ) )
except Exception :
pass
2021-05-13 16:39:31 +02:00
def focus_change_event ( self ) :
if not self . dialog_open :
self . update_conversation_list ( )
2026-03-28 18:19:11 -04:00
def toggle_list_sort ( self ) :
if self . list_sort_mode == ConversationsDisplay . SORT_RECENT :
self . list_sort_mode = ConversationsDisplay . SORT_NAME
else :
self . list_sort_mode = ConversationsDisplay . SORT_RECENT
self . update_conversation_list ( )
2021-05-13 16:39:31 +02:00
def update_listbox ( self ) :
2026-03-28 18:19:11 -04:00
conversations = self . app . conversations ( )
if self . list_sort_mode == ConversationsDisplay . SORT_NAME :
conversations . sort ( key = lambda e : ( e [ 3 ] . lower ( ) , e [ 0 ] ) )
2021-05-13 16:39:31 +02:00
conversation_list_widgets = [ ]
2026-03-28 18:19:11 -04:00
for conversation in conversations :
2021-05-13 16:39:31 +02:00
conversation_list_widgets . append ( self . conversation_list_widget ( conversation ) )
self . list_widgets = conversation_list_widgets
self . ilb = IndicativeListBox (
self . list_widgets ,
on_selection_change = self . conversation_list_selection ,
initialization_is_selection_change = False ,
highlight_offFocus = " list_off_focus "
)
2024-01-18 11:58:22 +01:00
self . listbox = ConversationsArea ( urwid . Filler ( self . ilb , height = urwid . RELATIVE_100 ) , title = " Conversations " )
2021-05-13 16:39:31 +02:00
self . listbox . delegate = self
def delete_selected_conversation ( self ) :
self . dialog_open = True
2023-02-09 13:45:37 +02:00
item = self . ilb . get_selected_item ( )
if item == None :
return
source_hash = item . source_hash
2021-05-13 16:39:31 +02:00
def dismiss_dialog ( sender ) :
self . update_conversation_list ( )
self . dialog_open = False
def confirmed ( sender ) :
self . dialog_open = False
self . delete_conversation ( source_hash )
nomadnet . Conversation . delete_conversation ( source_hash , self . app )
self . update_conversation_list ( )
2021-05-14 22:12:42 +02:00
dialog = DialogLineBox (
2021-05-13 16:39:31 +02:00
urwid . Pile ( [
2024-01-18 11:58:22 +01:00
urwid . Text (
" Delete conversation with \n " + self . app . directory . simplest_display_str ( bytes . fromhex ( source_hash ) ) + " \n " ,
align = urwid . CENTER ,
) ,
urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Yes " , on_press = confirmed ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " No " , on_press = dismiss_dialog ) ) ,
] )
2021-05-13 16:39:31 +02:00
] ) , title = " ? "
)
2021-05-14 22:12:42 +02:00
dialog . delegate = self
2021-05-13 16:39:31 +02:00
bottom = self . listbox
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay (
dialog ,
bottom ,
align = urwid . CENTER ,
width = urwid . RELATIVE_100 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
2021-05-13 16:39:31 +02:00
2024-01-18 11:58:22 +01:00
# options = self.columns_widget.options(urwid.WEIGHT, ConversationsDisplay.list_width)
options = self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width )
2021-05-13 16:39:31 +02:00
self . columns_widget . contents [ 0 ] = ( overlay , options )
2021-05-14 14:45:20 +02:00
def edit_selected_in_directory ( self ) :
2021-07-02 13:35:10 +02:00
g = self . app . ui . glyphs
2021-05-13 16:39:31 +02:00
self . dialog_open = True
2023-02-09 13:45:37 +02:00
item = self . ilb . get_selected_item ( )
if item == None :
return
source_hash_text = item . source_hash
2021-05-13 16:39:31 +02:00
display_name = self . ilb . get_selected_item ( ) . display_name
2021-05-14 22:12:42 +02:00
if display_name == None :
display_name = " "
2021-05-13 16:39:31 +02:00
2021-05-15 21:08:30 +02:00
e_id = urwid . Edit ( caption = " Addr : " , edit_text = source_hash_text )
t_id = urwid . Text ( " Addr : " + source_hash_text )
2021-05-13 16:39:31 +02:00
e_name = urwid . Edit ( caption = " Name : " , edit_text = display_name )
2021-05-14 14:45:20 +02:00
selected_id_widget = t_id
2021-10-03 18:44:00 +02:00
untrusted_selected = False
unknown_selected = True
trusted_selected = False
direct_selected = True
propagated_selected = False
2021-05-14 14:45:20 +02:00
try :
if self . app . directory . find ( bytes . fromhex ( source_hash_text ) ) :
trust_level = self . app . directory . trust_level ( bytes . fromhex ( source_hash_text ) )
if trust_level == DirectoryEntry . UNTRUSTED :
untrusted_selected = True
unknown_selected = False
trusted_selected = False
elif trust_level == DirectoryEntry . UNKNOWN :
untrusted_selected = False
unknown_selected = True
trusted_selected = False
elif trust_level == DirectoryEntry . TRUSTED :
untrusted_selected = False
unknown_selected = False
trusted_selected = True
2021-10-03 18:44:00 +02:00
if self . app . directory . preferred_delivery ( bytes . fromhex ( source_hash_text ) ) == DirectoryEntry . PROPAGATED :
direct_selected = False
propagated_selected = True
2021-05-14 14:45:20 +02:00
except Exception as e :
2021-05-14 17:36:35 +02:00
pass
2021-05-14 14:45:20 +02:00
2021-05-13 16:39:31 +02:00
trust_button_group = [ ]
2021-05-14 14:45:20 +02:00
r_untrusted = urwid . RadioButton ( trust_button_group , " Untrusted " , state = untrusted_selected )
r_unknown = urwid . RadioButton ( trust_button_group , " Unknown " , state = unknown_selected )
r_trusted = urwid . RadioButton ( trust_button_group , " Trusted " , state = trusted_selected )
2021-05-13 16:39:31 +02:00
2021-10-03 18:44:00 +02:00
method_button_group = [ ]
r_direct = urwid . RadioButton ( method_button_group , " Deliver directly " , state = direct_selected )
r_propagated = urwid . RadioButton ( method_button_group , " Use propagation nodes " , state = propagated_selected )
2021-05-13 16:39:31 +02:00
def dismiss_dialog ( sender ) :
self . update_conversation_list ( )
self . dialog_open = False
def confirmed ( sender ) :
try :
display_name = e_name . get_edit_text ( )
source_hash = bytes . fromhex ( e_id . get_edit_text ( ) )
trust_level = DirectoryEntry . UNTRUSTED
if r_unknown . state == True :
trust_level = DirectoryEntry . UNKNOWN
elif r_trusted . state == True :
trust_level = DirectoryEntry . TRUSTED
2021-10-03 18:44:00 +02:00
delivery = DirectoryEntry . DIRECT
if r_propagated . state == True :
delivery = DirectoryEntry . PROPAGATED
entry = DirectoryEntry ( source_hash , display_name , trust_level , preferred_delivery = delivery )
2021-05-13 16:39:31 +02:00
self . app . directory . remember ( entry )
self . update_conversation_list ( )
self . dialog_open = False
2021-05-15 21:08:30 +02:00
self . app . ui . main_display . sub_displays . network_display . directory_change_callback ( )
2021-05-13 16:39:31 +02:00
except Exception as e :
RNS . log ( " Could not save directory entry. The contained exception was: " + str ( e ) , RNS . LOG_VERBOSE )
if not dialog_pile . error_display :
dialog_pile . error_display = True
2024-01-18 11:58:22 +01:00
options = dialog_pile . options ( height_type = urwid . PACK )
2021-05-13 16:39:31 +02:00
dialog_pile . contents . append ( ( urwid . Text ( " " ) , options ) )
2024-01-18 11:58:22 +01:00
dialog_pile . contents . append ( (
urwid . Text ( ( " error_text " , " Could not save entry. Check your input. " ) , align = urwid . CENTER ) ,
options , )
)
2021-05-13 16:39:31 +02:00
2021-05-14 22:12:42 +02:00
source_is_known = self . app . directory . is_known ( bytes . fromhex ( source_hash_text ) )
if source_is_known :
2021-07-02 13:35:10 +02:00
known_section = urwid . Divider ( g [ " divider1 " ] )
2021-05-14 22:12:42 +02:00
else :
def query_action ( sender , user_data ) :
2021-05-15 21:08:30 +02:00
self . close_conversation_by_hash ( user_data )
2021-05-14 22:12:42 +02:00
nomadnet . Conversation . query_for_peer ( user_data )
2024-01-18 11:58:22 +01:00
options = dialog_pile . options ( height_type = urwid . PACK )
2021-05-14 22:12:42 +02:00
dialog_pile . contents = [
( urwid . Text ( " Query sent " ) , options ) ,
( urwid . Button ( " OK " , on_press = dismiss_dialog ) , options )
]
query_button = urwid . Button ( " Query network for keys " , on_press = query_action , user_data = source_hash_text )
2024-01-18 11:58:22 +01:00
known_section = urwid . Pile ( [
urwid . Divider ( g [ " divider1 " ] ) ,
urwid . Text ( g [ " info " ] + " \n " , align = urwid . CENTER ) ,
urwid . Text (
" The identity of this peer is not known, and you cannot currently send messages to it. "
" You can query the network to obtain the identity. \n " ,
align = urwid . CENTER ,
) ,
query_button ,
urwid . Divider ( g [ " divider1 " ] ) ,
] )
2021-05-14 22:12:42 +02:00
2021-05-13 16:39:31 +02:00
dialog_pile = urwid . Pile ( [
2021-05-14 14:45:20 +02:00
selected_id_widget ,
2021-05-13 16:39:31 +02:00
e_name ,
2021-07-02 13:35:10 +02:00
urwid . Divider ( g [ " divider1 " ] ) ,
2021-05-13 16:39:31 +02:00
r_untrusted ,
r_unknown ,
r_trusted ,
2021-10-03 18:44:00 +02:00
urwid . Divider ( g [ " divider1 " ] ) ,
r_direct ,
r_propagated ,
2021-05-14 22:12:42 +02:00
known_section ,
2024-01-18 11:58:22 +01:00
urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Save " , on_press = confirmed ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " Back " , on_press = dismiss_dialog ) ) ,
] )
2021-05-13 16:39:31 +02:00
] )
dialog_pile . error_display = False
2021-05-15 21:08:30 +02:00
dialog = DialogLineBox ( dialog_pile , title = " Peer Info " )
2021-05-14 22:12:42 +02:00
dialog . delegate = self
2021-05-13 16:39:31 +02:00
bottom = self . listbox
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay (
dialog ,
bottom ,
align = urwid . CENTER ,
width = urwid . RELATIVE_100 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
2021-05-13 16:39:31 +02:00
2024-01-18 11:58:22 +01:00
# options = self.columns_widget.options(urwid.WEIGHT, ConversationsDisplay.list_width)
options = self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width )
2021-05-13 16:39:31 +02:00
self . columns_widget . contents [ 0 ] = ( overlay , options )
def new_conversation ( self ) :
2021-05-14 14:45:20 +02:00
self . dialog_open = True
source_hash = " "
display_name = " "
2021-05-15 21:08:30 +02:00
e_id = urwid . Edit ( caption = " Addr : " , edit_text = source_hash )
2021-05-14 14:45:20 +02:00
e_name = urwid . Edit ( caption = " Name : " , edit_text = display_name )
trust_button_group = [ ]
r_untrusted = urwid . RadioButton ( trust_button_group , " Untrusted " )
r_unknown = urwid . RadioButton ( trust_button_group , " Unknown " , state = True )
r_trusted = urwid . RadioButton ( trust_button_group , " Trusted " )
def dismiss_dialog ( sender ) :
self . update_conversation_list ( )
self . dialog_open = False
def confirmed ( sender ) :
try :
existing_conversations = nomadnet . Conversation . conversation_list ( self . app )
display_name = e_name . get_edit_text ( )
2025-12-12 19:21:03 -05:00
source_hash_text = e_id . get_edit_text ( ) . strip ( )
2021-05-14 14:45:20 +02:00
source_hash = bytes . fromhex ( source_hash_text )
trust_level = DirectoryEntry . UNTRUSTED
if r_unknown . state == True :
trust_level = DirectoryEntry . UNKNOWN
elif r_trusted . state == True :
trust_level = DirectoryEntry . TRUSTED
if not source_hash in [ c [ 0 ] for c in existing_conversations ] :
entry = DirectoryEntry ( source_hash , display_name , trust_level )
self . app . directory . remember ( entry )
new_conversation = nomadnet . Conversation ( source_hash_text , nomadnet . NomadNetworkApp . get_shared_instance ( ) , initiator = True )
2022-09-13 21:47:41 +02:00
2021-05-14 14:45:20 +02:00
self . update_conversation_list ( )
self . display_conversation ( source_hash_text )
self . dialog_open = False
except Exception as e :
RNS . log ( " Could not start conversation. The contained exception was: " + str ( e ) , RNS . LOG_VERBOSE )
if not dialog_pile . error_display :
dialog_pile . error_display = True
2024-01-18 11:58:22 +01:00
options = dialog_pile . options ( height_type = urwid . PACK )
2021-05-14 14:45:20 +02:00
dialog_pile . contents . append ( ( urwid . Text ( " " ) , options ) )
2024-01-18 11:58:22 +01:00
dialog_pile . contents . append ( (
urwid . Text (
( " error_text " , " Could not start conversation. Check your input. " ) ,
align = urwid . CENTER ,
) ,
options ,
) )
2021-05-14 14:45:20 +02:00
dialog_pile = urwid . Pile ( [
e_id ,
e_name ,
urwid . Text ( " " ) ,
r_untrusted ,
r_unknown ,
r_trusted ,
urwid . Text ( " " ) ,
2024-01-18 11:58:22 +01:00
urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Create " , on_press = confirmed ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " Back " , on_press = dismiss_dialog ) ) ,
] )
2021-05-14 14:45:20 +02:00
] )
dialog_pile . error_display = False
2021-05-14 22:12:42 +02:00
dialog = DialogLineBox ( dialog_pile , title = " New Conversation " )
dialog . delegate = self
2021-05-14 14:45:20 +02:00
bottom = self . listbox
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay (
dialog ,
bottom ,
align = urwid . CENTER ,
width = urwid . RELATIVE_100 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
2021-05-14 14:45:20 +02:00
2024-01-18 11:58:22 +01:00
# options = self.columns_widget.options(urwid.WEIGHT, ConversationsDisplay.list_width)
options = self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width )
2021-05-14 14:45:20 +02:00
self . columns_widget . contents [ 0 ] = ( overlay , options )
2021-05-13 16:39:31 +02:00
2022-11-22 19:55:17 +01:00
def ingest_lxm_uri ( self ) :
2022-11-19 20:04:01 +01:00
self . dialog_open = True
2022-11-22 19:55:17 +01:00
lxm_uri = " "
e_uri = urwid . Edit ( caption = " URI : " , edit_text = lxm_uri )
2022-11-19 20:04:01 +01:00
def dismiss_dialog ( sender ) :
self . update_conversation_list ( )
self . dialog_open = False
def confirmed ( sender ) :
try :
local_delivery_signal = " local_delivery_occurred "
duplicate_signal = " duplicate_lxm "
2025-12-12 19:21:03 -05:00
lxm_uri = e_uri . get_edit_text ( ) . strip ( )
2022-11-19 20:04:01 +01:00
2022-11-22 19:55:17 +01:00
ingest_result = self . app . message_router . ingest_lxm_uri (
lxm_uri ,
2022-11-19 20:04:01 +01:00
signal_local_delivery = local_delivery_signal ,
signal_duplicate = duplicate_signal
)
if ingest_result == False :
2022-11-22 19:55:17 +01:00
raise ValueError ( " The URI contained no decodable messages " )
2022-11-19 20:04:01 +01:00
elif ingest_result == local_delivery_signal :
rdialog_pile = urwid . Pile ( [
urwid . Text ( " Message was decoded, decrypted successfully, and added to your conversation list. " ) ,
urwid . Text ( " " ) ,
2024-01-18 11:58:22 +01:00
urwid . Columns ( [
( urwid . WEIGHT , 0.6 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.4 , urwid . Button ( " OK " , on_press = dismiss_dialog ) ) ,
] )
2022-11-19 20:04:01 +01:00
] )
rdialog_pile . error_display = False
2022-11-22 19:55:17 +01:00
rdialog = DialogLineBox ( rdialog_pile , title = " Ingest message URI " )
2022-11-19 20:04:01 +01:00
rdialog . delegate = self
bottom = self . listbox
2024-01-18 11:58:22 +01:00
roverlay = urwid . Overlay (
rdialog ,
bottom ,
align = urwid . CENTER ,
width = urwid . RELATIVE_100 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
options = self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width )
2022-11-19 20:04:01 +01:00
self . columns_widget . contents [ 0 ] = ( roverlay , options )
elif ingest_result == duplicate_signal :
rdialog_pile = urwid . Pile ( [
urwid . Text ( " The decoded message has already been processed by the LXMF Router, and will not be ingested again. " ) ,
urwid . Text ( " " ) ,
2024-01-18 11:58:22 +01:00
urwid . Columns ( [
( urwid . WEIGHT , 0.6 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.4 , urwid . Button ( " OK " , on_press = dismiss_dialog ) ) ,
] )
2022-11-19 20:04:01 +01:00
] )
rdialog_pile . error_display = False
2022-11-22 19:55:17 +01:00
rdialog = DialogLineBox ( rdialog_pile , title = " Ingest message URI " )
2022-11-19 20:04:01 +01:00
rdialog . delegate = self
bottom = self . listbox
2024-01-18 11:58:22 +01:00
roverlay = urwid . Overlay (
rdialog ,
bottom ,
align = urwid . CENTER ,
width = urwid . RELATIVE_100 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
options = self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width )
2022-11-19 20:04:01 +01:00
self . columns_widget . contents [ 0 ] = ( roverlay , options )
else :
if self . app . enable_node :
propagation_text = " The decoded message was not addressed to this LXMF address, but has been added to the propagation node queues, and will be distributed on the propagation network. "
else :
propagation_text = " The decoded message was not addressed to this LXMF address, and has been discarded. "
rdialog_pile = urwid . Pile ( [
urwid . Text ( propagation_text ) ,
urwid . Text ( " " ) ,
2024-01-18 11:58:22 +01:00
urwid . Columns ( [
( urwid . WEIGHT , 0.6 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.4 , urwid . Button ( " OK " , on_press = dismiss_dialog ) ) ,
] )
2022-11-19 20:04:01 +01:00
] )
rdialog_pile . error_display = False
2022-11-22 19:55:17 +01:00
rdialog = DialogLineBox ( rdialog_pile , title = " Ingest message URI " )
2022-11-19 20:04:01 +01:00
rdialog . delegate = self
bottom = self . listbox
2024-01-18 11:58:22 +01:00
roverlay = urwid . Overlay (
rdialog ,
bottom ,
align = urwid . CENTER ,
width = urwid . RELATIVE_100 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
options = self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width )
2022-11-19 20:04:01 +01:00
self . columns_widget . contents [ 0 ] = ( roverlay , options )
except Exception as e :
2022-11-22 19:55:17 +01:00
RNS . log ( " Could not ingest LXM URI. The contained exception was: " + str ( e ) , RNS . LOG_VERBOSE )
2022-11-19 20:04:01 +01:00
if not dialog_pile . error_display :
dialog_pile . error_display = True
2024-01-18 11:58:22 +01:00
options = dialog_pile . options ( height_type = urwid . PACK )
2022-11-19 20:04:01 +01:00
dialog_pile . contents . append ( ( urwid . Text ( " " ) , options ) )
2024-01-18 11:58:22 +01:00
dialog_pile . contents . append ( ( urwid . Text ( ( " error_text " , " Could ingest LXM from URI data. Check your input. " ) , align = urwid . CENTER ) , options ) )
2022-11-19 20:04:01 +01:00
dialog_pile = urwid . Pile ( [
2022-11-22 19:55:17 +01:00
e_uri ,
2022-11-19 20:04:01 +01:00
urwid . Text ( " " ) ,
2024-01-18 11:58:22 +01:00
urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Ingest " , on_press = confirmed ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " Back " , on_press = dismiss_dialog ) ) ,
] )
2022-11-19 20:04:01 +01:00
] )
dialog_pile . error_display = False
2022-11-22 19:55:17 +01:00
dialog = DialogLineBox ( dialog_pile , title = " Ingest message URI " )
2022-11-19 20:04:01 +01:00
dialog . delegate = self
bottom = self . listbox
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay (
dialog ,
bottom ,
align = urwid . CENTER ,
width = urwid . RELATIVE_100 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
2022-11-19 20:04:01 +01:00
2024-01-18 11:58:22 +01:00
options = self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width )
2022-11-19 20:04:01 +01:00
self . columns_widget . contents [ 0 ] = ( overlay , options )
2021-05-13 16:39:31 +02:00
def delete_conversation ( self , source_hash ) :
if source_hash in ConversationsDisplay . cached_conversation_widgets :
conversation = ConversationsDisplay . cached_conversation_widgets [ source_hash ]
self . close_conversation ( conversation )
2022-07-04 17:34:12 +02:00
def toggle_fullscreen ( self ) :
if ConversationsDisplay . given_list_width != 0 :
self . saved_list_width = ConversationsDisplay . given_list_width
ConversationsDisplay . given_list_width = 0
else :
ConversationsDisplay . given_list_width = self . saved_list_width
self . update_conversation_list ( )
2021-10-07 18:14:06 +02:00
def sync_conversations ( self ) :
2021-10-08 11:58:24 +02:00
g = self . app . ui . glyphs
2021-10-07 18:14:06 +02:00
self . dialog_open = True
def dismiss_dialog ( sender ) :
self . dialog_open = False
self . sync_dialog = None
self . update_conversation_list ( )
2022-06-17 13:43:38 +02:00
if self . app . message_router . propagation_transfer_state > = LXMF . LXMRouter . PR_COMPLETE :
2021-10-07 18:14:06 +02:00
self . app . cancel_lxmf_sync ( )
2021-10-08 11:58:24 +02:00
max_messages_group = [ ]
r_mall = urwid . RadioButton ( max_messages_group , " Download all " , state = True )
r_mlim = urwid . RadioButton ( max_messages_group , " Limit to " , state = False )
ie_lim = urwid . IntEdit ( " " , 5 )
2024-01-18 11:58:22 +01:00
rbs = urwid . GridFlow ( [ r_mlim , ie_lim ] , 12 , 1 , 0 , align = urwid . LEFT )
2021-10-07 18:14:06 +02:00
def sync_now ( sender ) :
2021-10-08 11:58:24 +02:00
limit = None
if r_mlim . get_state ( ) :
limit = ie_lim . value ( )
self . app . request_lxmf_sync ( limit )
2021-10-07 18:14:06 +02:00
self . update_sync_dialog ( )
def cancel_sync ( sender ) :
self . app . cancel_lxmf_sync ( )
2021-10-12 21:12:32 +02:00
self . update_sync_dialog ( )
2021-10-07 18:14:06 +02:00
cancel_button = urwid . Button ( " Close " , on_press = dismiss_dialog )
sync_progress = SyncProgressBar ( " progress_empty " , " progress_full " , current = self . app . get_sync_progress ( ) , done = 1.0 , satt = None )
real_sync_button = urwid . Button ( " Sync Now " , on_press = sync_now )
hidden_sync_button = urwid . Button ( " Cancel Sync " , on_press = cancel_sync )
2022-06-17 13:43:38 +02:00
if self . app . get_sync_status ( ) == " Idle " or self . app . message_router . propagation_transfer_state > = LXMF . LXMRouter . PR_COMPLETE :
2021-10-07 18:14:06 +02:00
sync_button = real_sync_button
else :
sync_button = hidden_sync_button
2024-01-18 11:58:22 +01:00
button_columns = urwid . Columns ( [
( urwid . WEIGHT , 0.45 , sync_button ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , cancel_button ) ,
] )
2021-10-07 18:14:06 +02:00
real_sync_button . bc = button_columns
2021-10-08 13:34:04 +02:00
pn_ident = None
2021-10-08 09:12:20 +02:00
if self . app . get_default_propagation_node ( ) != None :
2021-10-08 11:58:24 +02:00
pn_hash = self . app . get_default_propagation_node ( )
pn_ident = RNS . Identity . recall ( pn_hash )
2021-10-08 13:34:04 +02:00
if pn_ident == None :
RNS . log ( " Propagation node identity is unknown, requesting from network... " , RNS . LOG_DEBUG )
RNS . Transport . request_path ( pn_hash )
if pn_ident != None :
2021-10-08 11:58:24 +02:00
node_hash = RNS . Destination . hash_from_name_and_identity ( " nomadnetwork.node " , pn_ident )
pn_entry = self . app . directory . find ( node_hash )
2022-10-25 12:52:02 +02:00
pn_display_str = " "
if pn_entry != None :
pn_display_str + = " " + str ( pn_entry . display_name )
else :
pn_display_str + = " " + RNS . prettyhexrep ( pn_hash )
2021-10-08 11:58:24 +02:00
2021-10-08 09:12:20 +02:00
dialog = DialogLineBox (
urwid . Pile ( [
2024-01-18 11:58:22 +01:00
urwid . Text ( " " + g [ " node " ] + pn_display_str , align = urwid . CENTER ) ,
2021-10-08 11:58:24 +02:00
urwid . Divider ( g [ " divider1 " ] ) ,
2021-10-08 09:12:20 +02:00
sync_progress ,
2021-10-08 11:58:24 +02:00
urwid . Divider ( g [ " divider1 " ] ) ,
r_mall ,
rbs ,
2021-10-08 09:12:20 +02:00
urwid . Text ( " " ) ,
button_columns
] ) , title = " Message Sync "
)
else :
2024-01-18 11:58:22 +01:00
button_columns = urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , cancel_button ) ,
] )
2021-10-08 09:12:20 +02:00
dialog = DialogLineBox (
urwid . Pile ( [
urwid . Text ( " " ) ,
2024-01-18 11:58:22 +01:00
urwid . Text ( " No trusted nodes found, cannot sync! \n " , align = urwid . CENTER ) ,
urwid . Text (
" To synchronise messages from the network, "
" one or more nodes must be marked as trusted in the Known Nodes list, "
" or a node must manually be selected as the default propagation node. "
" Nomad Network will then automatically sync from the nearest trusted node, "
" or the manually selected one. " ,
align = urwid . LEFT ,
) ,
2021-10-08 09:12:20 +02:00
urwid . Text ( " " ) ,
button_columns
] ) , title = " Message Sync "
)
2021-10-07 18:14:06 +02:00
dialog . delegate = self
dialog . sync_progress = sync_progress
dialog . cancel_button = cancel_button
dialog . real_sync_button = real_sync_button
dialog . hidden_sync_button = hidden_sync_button
dialog . bc = button_columns
self . sync_dialog = dialog
bottom = self . listbox
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay (
dialog ,
bottom ,
align = urwid . CENTER ,
width = urwid . RELATIVE_100 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
2021-10-07 18:14:06 +02:00
2024-01-18 11:58:22 +01:00
# options = self.columns_widget.options(urwid.WEIGHT, ConversationsDisplay.list_width)
options = self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width )
2021-10-07 18:14:06 +02:00
self . columns_widget . contents [ 0 ] = ( overlay , options )
def update_sync_dialog ( self , loop = None , sender = None ) :
if self . dialog_open and self . sync_dialog != None :
self . sync_dialog . sync_progress . set_completion ( self . app . get_sync_progress ( ) )
2022-06-17 13:43:38 +02:00
if self . app . get_sync_status ( ) == " Idle " or self . app . message_router . propagation_transfer_state > = LXMF . LXMRouter . PR_COMPLETE :
2024-01-18 11:58:22 +01:00
self . sync_dialog . bc . contents [ 0 ] = ( self . sync_dialog . real_sync_button , self . sync_dialog . bc . options ( urwid . WEIGHT , 0.45 ) )
2021-10-07 18:14:06 +02:00
else :
2024-01-18 11:58:22 +01:00
self . sync_dialog . bc . contents [ 0 ] = ( self . sync_dialog . hidden_sync_button , self . sync_dialog . bc . options ( urwid . WEIGHT , 0.45 ) )
2021-10-07 18:14:06 +02:00
self . app . ui . loop . set_alarm_in ( 0.2 , self . update_sync_dialog )
2021-05-13 16:39:31 +02:00
def conversation_list_selection ( self , arg1 , arg2 ) :
pass
def update_conversation_list ( self ) :
2026-03-28 18:19:11 -04:00
selected_hash = None
selected_item = self . ilb . get_selected_item ( )
if selected_item is not None :
if hasattr ( selected_item , " source_hash " ) :
selected_hash = selected_item . source_hash
2021-05-13 16:39:31 +02:00
self . update_listbox ( )
2024-01-18 11:58:22 +01:00
options = self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width )
2021-10-07 18:14:06 +02:00
if not ( self . dialog_open and self . sync_dialog != None ) :
self . columns_widget . contents [ 0 ] = ( self . listbox , options )
else :
bottom = self . listbox
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay (
self . sync_dialog ,
bottom ,
align = urwid . CENTER ,
width = urwid . RELATIVE_100 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
2021-10-07 18:14:06 +02:00
self . columns_widget . contents [ 0 ] = ( overlay , options )
2026-03-28 18:19:11 -04:00
if selected_hash is not None :
for idx , widget in enumerate ( self . list_widgets ) :
if widget . source_hash == selected_hash :
self . ilb . select_item ( idx )
break
2021-05-13 16:39:31 +02:00
nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . loop . draw_screen ( )
2021-09-23 17:20:13 +02:00
if self . app . ui . main_display . sub_displays . active_display == self . app . ui . main_display . sub_displays . conversations_display :
if self . currently_displayed_conversation != None :
if self . app . conversation_is_unread ( self . currently_displayed_conversation ) :
self . app . mark_conversation_read ( self . currently_displayed_conversation )
try :
if os . path . isfile ( self . app . conversationpath + " / " + self . currently_displayed_conversation + " /unread " ) :
os . unlink ( self . app . conversationpath + " / " + self . currently_displayed_conversation + " /unread " )
except Exception as e :
raise e
2021-09-05 20:38:10 +02:00
2021-05-13 16:39:31 +02:00
2021-05-04 11:08:16 +02:00
2021-05-04 20:53:03 +02:00
def display_conversation ( self , sender = None , source_hash = None ) :
2021-09-05 20:38:10 +02:00
if self . currently_displayed_conversation != None :
if self . app . conversation_is_unread ( self . currently_displayed_conversation ) :
self . app . mark_conversation_read ( self . currently_displayed_conversation )
2021-05-13 16:39:31 +02:00
self . currently_displayed_conversation = source_hash
2024-01-18 11:58:22 +01:00
# options = self.widget.options(urwid.WEIGHT, 1-ConversationsDisplay.list_width)
options = self . widget . options ( urwid . WEIGHT , 1 )
2021-05-04 20:53:03 +02:00
self . widget . contents [ 1 ] = ( self . make_conversation_widget ( source_hash ) , options )
2021-06-30 14:49:26 +02:00
if source_hash == None :
2024-01-18 11:58:22 +01:00
self . widget . focus_position = 0
2021-06-30 14:49:26 +02:00
else :
2021-09-05 20:38:10 +02:00
if self . app . conversation_is_unread ( source_hash ) :
self . app . mark_conversation_read ( source_hash )
self . update_conversation_list ( )
2024-01-18 11:58:22 +01:00
self . widget . focus_position = 1
2021-09-05 20:38:10 +02:00
conversation_position = None
index = 0
for widget in self . list_widgets :
if widget . source_hash == source_hash :
conversation_position = index
index + = 1
if conversation_position != None :
self . ilb . select_item ( conversation_position )
2021-05-04 20:53:03 +02:00
def make_conversation_widget ( self , source_hash ) :
2021-05-13 16:39:31 +02:00
if source_hash in ConversationsDisplay . cached_conversation_widgets :
conversation_widget = ConversationsDisplay . cached_conversation_widgets [ source_hash ]
if source_hash != None :
conversation_widget . update_message_widgets ( replace = True )
2021-05-14 22:12:42 +02:00
conversation_widget . check_editor_allowed ( )
2021-05-13 16:39:31 +02:00
return conversation_widget
else :
widget = ConversationWidget ( source_hash )
widget . delegate = self
ConversationsDisplay . cached_conversation_widgets [ source_hash ] = widget
2021-05-14 22:12:42 +02:00
widget . check_editor_allowed ( )
2021-05-13 16:39:31 +02:00
return widget
2021-05-15 21:08:30 +02:00
def close_conversation_by_hash ( self , conversation_hash ) :
if conversation_hash in ConversationsDisplay . cached_conversation_widgets :
ConversationsDisplay . cached_conversation_widgets . pop ( conversation_hash )
if self . currently_displayed_conversation == conversation_hash :
self . display_conversation ( sender = None , source_hash = None )
2021-05-13 16:39:31 +02:00
def close_conversation ( self , conversation ) :
2021-05-15 21:08:30 +02:00
if conversation . source_hash in ConversationsDisplay . cached_conversation_widgets :
ConversationsDisplay . cached_conversation_widgets . pop ( conversation . source_hash )
if self . currently_displayed_conversation == conversation . source_hash :
self . display_conversation ( sender = None , source_hash = None )
2021-05-13 16:39:31 +02:00
def conversation_list_widget ( self , conversation ) :
2026-03-28 18:19:11 -04:00
trust_level = conversation [ 2 ]
display_name = conversation [ 1 ]
source_hash = conversation [ 0 ]
unread = conversation [ 4 ]
last_activity = conversation [ 5 ]
2021-05-13 16:39:31 +02:00
2021-07-02 13:35:10 +02:00
g = self . app . ui . glyphs
2021-05-13 16:39:31 +02:00
if trust_level == DirectoryEntry . UNTRUSTED :
2021-07-02 13:35:10 +02:00
symbol = g [ " cross " ]
2021-05-14 14:45:20 +02:00
style = " list_untrusted "
focus_style = " list_focus_untrusted "
2021-05-13 16:39:31 +02:00
elif trust_level == DirectoryEntry . UNKNOWN :
2021-05-14 14:45:20 +02:00
symbol = " ? "
style = " list_unknown "
focus_style = " list_focus "
2021-05-13 16:39:31 +02:00
elif trust_level == DirectoryEntry . TRUSTED :
2021-07-02 13:35:10 +02:00
symbol = g [ " check " ]
2021-05-14 14:45:20 +02:00
style = " list_trusted "
focus_style = " list_focus_trusted "
2021-05-13 16:39:31 +02:00
elif trust_level == DirectoryEntry . WARNING :
2021-07-02 13:35:10 +02:00
symbol = g [ " warning " ]
2021-05-14 14:45:20 +02:00
style = " list_warning "
focus_style = " list_focus "
2021-05-13 16:39:31 +02:00
else :
2021-07-02 13:35:10 +02:00
symbol = g [ " warning " ]
2021-05-14 14:45:20 +02:00
style = " list_untrusted "
focus_style = " list_focus_untrusted "
2021-05-13 16:39:31 +02:00
display_text = symbol
2021-09-05 20:38:10 +02:00
2021-05-14 22:12:42 +02:00
if display_name != None and display_name != " " :
2021-05-13 16:39:31 +02:00
display_text + = " " + display_name
if trust_level != DirectoryEntry . TRUSTED :
display_text + = " < " + source_hash + " > "
2021-09-25 16:57:29 +02:00
if trust_level != DirectoryEntry . UNTRUSTED :
2021-09-05 20:38:10 +02:00
if unread :
if source_hash != self . currently_displayed_conversation :
2026-03-28 18:19:11 -04:00
if unread > 1 :
display_text + = " " + g [ " unread " ] + " ( " + str ( unread ) + " ) "
else :
display_text + = " " + g [ " unread " ]
2021-09-05 20:38:10 +02:00
2026-03-28 18:19:11 -04:00
if last_activity > 0 :
display_text + = " \n " + relative_time ( last_activity )
2021-05-13 16:39:31 +02:00
widget = ListEntry ( display_text )
urwid . connect_signal ( widget , " click " , self . display_conversation , conversation [ 0 ] )
2021-05-14 14:45:20 +02:00
display_widget = urwid . AttrMap ( widget , style , focus_style )
2021-05-13 16:39:31 +02:00
display_widget . source_hash = source_hash
display_widget . display_name = display_name
return display_widget
def shortcuts ( self ) :
focus_path = self . widget . get_focus_path ( )
if focus_path [ 0 ] == 0 :
return self . list_shortcuts
elif focus_path [ 0 ] == 1 :
return self . editor_shortcuts
else :
return self . list_shortcuts
class ListEntry ( urwid . Text ) :
_selectable = True
signals = [ " click " ]
def keypress ( self , size , key ) :
"""
Send ' click ' signal on ' activate ' command .
"""
if self . _command_map [ key ] != urwid . ACTIVATE :
return key
self . _emit ( ' click ' )
def mouse_event ( self , size , event , button , x , y , focus ) :
"""
Send ' click ' signal on button 1 press .
"""
if button != 1 or not urwid . util . is_mouse_press ( event ) :
return False
self . _emit ( ' click ' )
return True
2021-05-04 20:53:03 +02:00
2021-05-13 16:39:31 +02:00
class MessageEdit ( urwid . Edit ) :
def keypress ( self , size , key ) :
if key == " ctrl d " :
self . delegate . send_message ( )
2022-11-19 20:04:01 +01:00
elif key == " ctrl p " :
self . delegate . paper_message ( )
2026-03-28 18:19:11 -04:00
elif key == " ctrl a " :
self . delegate . attach_file ( )
elif key == " ctrl s " :
self . delegate . save_focused_attachments ( )
2021-05-13 16:39:31 +02:00
elif key == " ctrl k " :
self . delegate . clear_editor ( )
2021-05-14 17:36:35 +02:00
elif key == " up " :
y = self . get_cursor_coords ( size ) [ 1 ]
if y == 0 :
if self . delegate . full_editor_active and self . name == " title_editor " :
2024-01-18 11:58:22 +01:00
self . delegate . frame . focus_position = " body "
2021-05-14 17:36:35 +02:00
elif not self . delegate . full_editor_active and self . name == " content_editor " :
2024-01-18 11:58:22 +01:00
self . delegate . frame . focus_position = " body "
2021-05-14 17:36:35 +02:00
else :
return super ( MessageEdit , self ) . keypress ( size , key )
else :
return super ( MessageEdit , self ) . keypress ( size , key )
2021-05-13 16:39:31 +02:00
else :
return super ( MessageEdit , self ) . keypress ( size , key )
2021-05-14 17:36:35 +02:00
class ConversationFrame ( urwid . Frame ) :
def keypress ( self , size , key ) :
2024-01-18 11:58:22 +01:00
if self . focus_position == " body " :
2026-03-28 18:19:11 -04:00
if getattr ( self . delegate , " dialog_active " , False ) or getattr ( self . delegate , " dialog_open " , False ) :
return super ( ConversationFrame , self ) . keypress ( size , key )
elif key == " up " and self . delegate . messagelist . top_is_visible :
2024-01-18 11:58:22 +01:00
nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . main_display . frame . focus_position = " header "
2021-05-14 17:36:35 +02:00
elif key == " down " and self . delegate . messagelist . bottom_is_visible :
2024-01-18 11:58:22 +01:00
self . focus_position = " footer "
2021-05-14 17:36:35 +02:00
else :
return super ( ConversationFrame , self ) . keypress ( size , key )
elif key == " ctrl k " :
self . delegate . clear_editor ( )
else :
return super ( ConversationFrame , self ) . keypress ( size , key )
2021-05-13 16:39:31 +02:00
class ConversationWidget ( urwid . WidgetWrap ) :
def __init__ ( self , source_hash ) :
2021-07-02 13:35:10 +02:00
self . app = nomadnet . NomadNetworkApp . get_shared_instance ( )
g = self . app . ui . glyphs
2021-05-04 20:53:03 +02:00
if source_hash == None :
2021-05-14 22:12:42 +02:00
self . frame = None
2021-05-15 21:08:30 +02:00
display_widget = urwid . LineBox ( urwid . Filler ( urwid . Text ( " \n No conversation selected " ) , " top " ) )
2024-01-18 10:22:38 +01:00
super ( ) . __init__ ( display_widget )
2021-05-04 20:53:03 +02:00
else :
if source_hash in ConversationsDisplay . cached_conversation_widgets :
return ConversationsDisplay . cached_conversation_widgets [ source_hash ]
else :
2021-05-13 16:39:31 +02:00
self . source_hash = source_hash
self . conversation = nomadnet . Conversation ( source_hash , nomadnet . NomadNetworkApp . get_shared_instance ( ) )
self . message_widgets = [ ]
2021-10-08 14:46:20 +02:00
self . sort_by_timestamp = False
2021-05-13 16:39:31 +02:00
self . updating_message_widgets = False
2026-03-28 18:19:11 -04:00
self . pending_attachments = [ ]
self . dialog_active = False
2021-05-04 20:53:03 +02:00
2021-05-13 16:39:31 +02:00
self . update_message_widgets ( )
2021-05-04 20:53:03 +02:00
2026-05-17 02:02:27 +02:00
self . conversation . register_changed_callback ( self . _on_conversation_changed_from_callback )
2021-05-04 20:53:03 +02:00
2021-05-14 15:13:17 +02:00
#title_editor = MessageEdit(caption="\u270E", edit_text="", multiline=False)
title_editor = MessageEdit ( caption = " " , edit_text = " " , multiline = False )
title_editor . delegate = self
2021-05-14 17:36:35 +02:00
title_editor . name = " title_editor "
2021-05-14 15:13:17 +02:00
#msg_editor = MessageEdit(caption="\u270E", edit_text="", multiline=True)
msg_editor = MessageEdit ( caption = " " , edit_text = " " , multiline = True )
2021-05-13 16:39:31 +02:00
msg_editor . delegate = self
2021-05-14 17:36:35 +02:00
msg_editor . name = " content_editor "
2021-05-12 14:02:44 +02:00
2026-03-28 18:19:11 -04:00
self . peer_info_widget = urwid . AttrMap ( urwid . Text ( " " ) , " msg_header_sent " )
self . _update_peer_info ( )
header_widgets = [ self . peer_info_widget ]
2021-05-13 16:39:31 +02:00
if self . conversation . trust_level == DirectoryEntry . UNTRUSTED :
2026-03-28 18:19:11 -04:00
header_widgets . append ( urwid . AttrMap (
2024-01-18 11:58:22 +01:00
urwid . Padding (
urwid . Text ( g [ " warning " ] + " Warning: Conversation with untrusted peer " + g [ " warning " ] , align = urwid . CENTER ) ) ,
" msg_warning_untrusted " ,
2026-03-28 18:19:11 -04:00
) )
header = urwid . Pile ( header_widgets )
2021-05-13 16:39:31 +02:00
2021-05-14 15:13:17 +02:00
self . minimal_editor = urwid . AttrMap ( msg_editor , " msg_editor " )
2021-05-14 17:36:35 +02:00
self . minimal_editor . name = " minimal_editor "
2021-05-14 15:13:17 +02:00
title_columns = urwid . Columns ( [
( 8 , urwid . Text ( " Title " ) ) ,
urwid . AttrMap ( title_editor , " msg_editor " ) ,
] )
content_columns = urwid . Columns ( [
( 8 , urwid . Text ( " Content " ) ) ,
urwid . AttrMap ( msg_editor , " msg_editor " )
] )
self . full_editor = urwid . Pile ( [
title_columns ,
content_columns
] )
2021-05-14 17:36:35 +02:00
self . full_editor . name = " full_editor "
2021-05-14 15:13:17 +02:00
self . content_editor = msg_editor
self . title_editor = title_editor
self . full_editor_active = False
2021-05-04 20:53:03 +02:00
2021-05-14 17:36:35 +02:00
self . frame = ConversationFrame (
2021-05-13 16:39:31 +02:00
self . messagelist ,
header = header ,
2021-05-15 21:08:30 +02:00
footer = self . minimal_editor ,
focus_part = " footer "
2021-05-13 16:39:31 +02:00
)
2021-05-14 17:36:35 +02:00
self . frame . delegate = self
2021-05-12 14:02:44 +02:00
2021-05-13 16:39:31 +02:00
self . display_widget = urwid . LineBox (
self . frame
)
2021-05-12 14:02:44 +02:00
2024-01-18 10:22:38 +01:00
super ( ) . __init__ ( self . display_widget )
2021-05-13 16:39:31 +02:00
2026-03-28 18:19:11 -04:00
def _update_peer_info ( self ) :
2026-05-06 18:44:17 +02:00
def san ( name ) :
if self . app . config [ " textui " ] [ " sanitize_names " ] : return sanitize_name ( name )
else : return strip_modifiers ( name )
2026-03-28 18:19:11 -04:00
g = self . app . ui . glyphs
source_hash_bytes = bytes . fromhex ( self . source_hash )
display_name = self . app . directory . display_name ( source_hash_bytes )
app_data = None
if display_name is None or self . app . message_router . get_outbound_stamp_cost ( source_hash_bytes ) is None :
app_data = RNS . Identity . recall_app_data ( source_hash_bytes )
if display_name is None :
if app_data :
2026-05-06 18:44:17 +02:00
display_name = san ( LXMF . display_name_from_app_data ( app_data ) )
2026-03-28 18:19:11 -04:00
if display_name is None :
display_name = RNS . prettyhexrep ( source_hash_bytes )
stamp_cost = self . app . message_router . get_outbound_stamp_cost ( source_hash_bytes )
if stamp_cost is None and app_data :
stamp_cost = LXMF . stamp_cost_from_app_data ( app_data )
hops = RNS . Transport . hops_to ( source_hash_bytes )
if hops > = RNS . Transport . PATHFINDER_M :
hops_str = " unknown "
else :
hops_str = str ( hops ) + " hop " + ( " s " if hops != 1 else " " )
right_parts = [ ]
if stamp_cost is not None :
right_parts . append ( " Stamp: " + str ( stamp_cost ) )
right_parts . append ( g [ " speed " ] + hops_str )
left = " " + display_name
right = " " . join ( right_parts ) + " "
self . peer_info_widget . original_widget . set_text ( left + " | " + right )
2021-09-11 11:33:20 +02:00
def clear_history_dialog ( self ) :
def dismiss_dialog ( sender ) :
self . dialog_open = False
self . conversation_changed ( None )
def confirmed ( sender ) :
self . dialog_open = False
self . conversation . clear_history ( )
self . conversation_changed ( None )
dialog = DialogLineBox (
urwid . Pile ( [
2024-01-18 11:58:22 +01:00
urwid . Text ( " Clear conversation history \n " , align = urwid . CENTER ) ,
urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Yes " , on_press = confirmed ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " No " , on_press = dismiss_dialog ) ) ,
] )
2021-09-11 11:33:20 +02:00
] ) , title = " ? "
)
dialog . delegate = self
bottom = self . messagelist
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay (
dialog ,
bottom ,
align = urwid . CENTER ,
width = 34 ,
valign = urwid . MIDDLE ,
height = urwid . PACK ,
left = 2 ,
right = 2 ,
)
2021-09-11 11:33:20 +02:00
self . frame . contents [ " body " ] = ( overlay , self . frame . options ( ) )
2024-01-18 11:58:22 +01:00
self . frame . focus_position = " body "
2021-09-11 11:33:20 +02:00
2026-03-28 18:19:11 -04:00
def _build_footer ( self ) :
g = self . app . ui . glyphs
if self . full_editor_active :
editor = self . full_editor
else :
editor = self . minimal_editor
if self . pending_attachments :
attachment_texts = [ ]
for path in self . pending_attachments :
attachment_texts . append ( os . path . basename ( path ) )
indicator = urwid . AttrMap (
urwid . Text ( g [ " file " ] + " " + str ( len ( self . pending_attachments ) ) + " file(s): " + " , " . join ( attachment_texts ) ) ,
" msg_header_sent " ,
)
return urwid . Pile ( [ indicator , editor ] )
else :
return editor
2021-05-14 15:13:17 +02:00
def toggle_editor ( self ) :
if self . full_editor_active :
self . full_editor_active = False
else :
self . full_editor_active = True
2026-03-28 18:19:11 -04:00
self . frame . contents [ " footer " ] = ( self . _build_footer ( ) , None )
2021-05-14 15:13:17 +02:00
2021-05-14 22:12:42 +02:00
def check_editor_allowed ( self ) :
2021-07-02 13:35:10 +02:00
g = self . app . ui . glyphs
2021-05-14 22:12:42 +02:00
if self . frame :
allowed = nomadnet . NomadNetworkApp . get_shared_instance ( ) . directory . is_known ( bytes . fromhex ( self . source_hash ) )
if allowed :
2026-03-28 18:19:11 -04:00
self . frame . contents [ " footer " ] = ( self . _build_footer ( ) , None )
2021-05-14 22:12:42 +02:00
else :
2024-01-18 11:58:22 +01:00
warning = urwid . AttrMap (
urwid . Padding ( urwid . Text (
2024-07-03 11:47:55 -05:00
" \n " + g [ " info " ] + " \n \n You cannot currently message this peer, since its identity keys are not known. "
2024-01-18 11:58:22 +01:00
" The keys have been requested from the network and should arrive shortly, if available. "
" Close this conversation and reopen it to try again. \n \n "
" To query the network manually, select this conversation in the conversation list, "
" press Ctrl-E, and use the query button. \n " ,
align = urwid . CENTER ,
) ) ,
" msg_header_caution " ,
)
2021-05-14 22:12:42 +02:00
self . frame . contents [ " footer " ] = ( warning , None )
2021-05-14 17:36:35 +02:00
def toggle_focus_area ( self ) :
name = " "
try :
name = self . frame . get_focus_widgets ( ) [ 0 ] . name
except Exception as e :
pass
if name == " messagelist " :
2024-01-18 11:58:22 +01:00
self . frame . focus_position = " footer "
2021-05-14 17:36:35 +02:00
elif name == " minimal_editor " or name == " full_editor " :
2024-01-18 11:58:22 +01:00
self . frame . focus_position = " body "
2021-05-14 17:36:35 +02:00
2021-05-14 14:45:20 +02:00
def keypress ( self , size , key ) :
2021-05-14 17:36:35 +02:00
if key == " tab " :
self . toggle_focus_area ( )
elif key == " ctrl w " :
2021-05-14 14:45:20 +02:00
self . close ( )
2022-11-19 20:04:01 +01:00
elif key == " ctrl u " :
2021-05-14 14:45:20 +02:00
self . conversation . purge_failed ( )
self . conversation_changed ( None )
2021-05-14 15:13:17 +02:00
elif key == " ctrl t " :
self . toggle_editor ( )
2021-09-11 11:33:20 +02:00
elif key == " ctrl x " :
self . clear_history_dialog ( )
2022-07-04 17:34:12 +02:00
elif key == " ctrl g " :
nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . main_display . sub_displays . conversations_display . toggle_fullscreen ( )
2021-10-08 14:46:20 +02:00
elif key == " ctrl o " :
self . sort_by_timestamp ^ = True
self . conversation_changed ( None )
2026-03-28 18:19:11 -04:00
elif key == " ctrl a " :
self . attach_file ( )
elif key == " ctrl s " :
self . save_focused_attachments ( )
2021-05-14 14:45:20 +02:00
else :
return super ( ConversationWidget , self ) . keypress ( size , key )
2026-05-17 02:02:27 +02:00
def _on_conversation_changed_from_callback ( self , conversation ) :
delegate = getattr ( self , " delegate " , None )
if delegate is not None and hasattr ( delegate , " _wake " ) :
delegate . _wake ( lambda : self . conversation_changed ( conversation ) )
else :
self . conversation_changed ( conversation )
2021-05-13 16:39:31 +02:00
def conversation_changed ( self , conversation ) :
2026-03-28 18:19:11 -04:00
if hasattr ( self , " peer_info_widget " ) :
self . _update_peer_info ( )
2021-05-13 16:39:31 +02:00
self . update_message_widgets ( replace = True )
def update_message_widgets ( self , replace = False ) :
while self . updating_message_widgets :
time . sleep ( 0.5 )
self . updating_message_widgets = True
self . message_widgets = [ ]
added_hashes = [ ]
2026-03-28 18:32:18 -04:00
needs_index = [ ]
2021-05-13 16:39:31 +02:00
for message in self . conversation . messages :
message_hash = message . get_hash ( )
if not message_hash in added_hashes :
added_hashes . append ( message_hash )
2026-03-28 18:32:18 -04:00
was_loaded = message . loaded
2021-05-13 16:39:31 +02:00
message_widget = LXMessageWidget ( message )
self . message_widgets . append ( message_widget )
2026-03-28 18:32:18 -04:00
if not was_loaded and message . loaded :
needs_index . append ( message )
2026-03-28 18:19:11 -04:00
message . unload ( )
2026-03-28 18:32:18 -04:00
if needs_index :
try :
ConversationMessage . write_index (
self . conversation . messages_path , needs_index )
except Exception :
pass
2021-10-08 14:46:20 +02:00
if self . sort_by_timestamp :
self . message_widgets . sort ( key = lambda m : m . timestamp , reverse = False )
else :
self . message_widgets . sort ( key = lambda m : m . sort_timestamp , reverse = False )
2021-05-04 20:53:03 +02:00
2021-05-13 16:39:31 +02:00
from nomadnet . vendor . additional_urwid_widgets import IndicativeListBox
self . messagelist = IndicativeListBox ( self . message_widgets , position = len ( self . message_widgets ) - 1 )
2021-05-14 17:36:35 +02:00
self . messagelist . name = " messagelist "
2021-05-13 16:39:31 +02:00
if replace :
self . frame . contents [ " body " ] = ( self . messagelist , None )
nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . loop . draw_screen ( )
2021-05-04 20:53:03 +02:00
2021-05-13 16:39:31 +02:00
self . updating_message_widgets = False
2021-05-04 20:53:03 +02:00
2021-05-13 16:39:31 +02:00
def clear_editor ( self ) :
2021-05-14 15:13:17 +02:00
self . content_editor . set_edit_text ( " " )
self . title_editor . set_edit_text ( " " )
2026-03-28 18:19:11 -04:00
self . pending_attachments = [ ]
self . frame . contents [ " footer " ] = ( self . _build_footer ( ) , None )
def _collect_attachment_refs ( self ) :
g = self . app . ui . glyphs
refs = [ ]
sorted_messages = sorted ( self . conversation . messages , key = lambda m : m . sort_timestamp , reverse = True )
for conv_message in sorted_messages :
if not conv_message . has_attachments ( ) :
continue
cached_names = conv_message . _cached_attachment_names or [ ]
att_file_idx = 0
for atype , aname , * arest in cached_names :
asize = arest [ 0 ] if arest else 0
glyph = g [ " file " ] if atype == " file " else g [ atype ]
label = glyph + " " + aname
if asize > 0 :
label + = " ( " + _format_size ( asize ) + " ) "
if atype == " file " :
refs . append ( ( label , aname , conv_message , " file " , att_file_idx ) )
att_file_idx + = 1
else :
refs . append ( ( label , aname , conv_message , atype , 0 ) )
return refs
def save_focused_attachments ( self ) :
g = self . app . ui . glyphs
self . dialog_active = True
try :
attachment_items = self . _collect_attachment_refs ( )
except Exception as e :
RNS . log ( " Error collecting attachments: " + str ( e ) , RNS . LOG_ERROR )
attachment_items = [ ]
save_dir = self . app . attachment_save_path if self . app . attachment_save_path else self . app . downloads_path
def dismiss_dialog ( sender ) :
self . dialog_active = False
self . conversation_changed ( None )
if not attachment_items :
dialog = DialogLineBox (
urwid . Pile ( [
urwid . Text ( " No attachments in this conversation. \n " ) ,
urwid . Columns ( [
( urwid . WEIGHT , 0.6 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.4 , urwid . Button ( " OK " , on_press = dismiss_dialog ) ) ,
] )
] ) , title = " Attachments "
)
dialog . delegate = self
bottom = self . messagelist
overlay = urwid . Overlay ( dialog , bottom , align = urwid . CENTER , width = 45 , valign = urwid . MIDDLE , height = urwid . PACK , left = 2 , right = 2 )
self . frame . contents [ " body " ] = ( overlay , self . frame . options ( ) )
self . frame . focus_position = " body "
return
checkboxes = [ ]
for label , filename , conv_msg , field_type , field_index in attachment_items :
cb = urwid . CheckBox ( label , state = False )
cb . _attachment_filename = filename
cb . _conv_message = conv_msg
cb . _field_type = field_type
cb . _field_index = field_index
checkboxes . append ( cb )
status_text = urwid . Text ( " " )
def do_save ( sender ) :
saved = [ ]
errors = [ ]
for cb in checkboxes :
if cb . get_state ( ) :
try :
src_path = cb . _conv_message . get_attachment_file_path ( cb . _field_type , cb . _field_index )
if src_path and os . path . isfile ( src_path ) :
path = _copy_attachment_to_dest ( cb . _attachment_filename , src_path )
saved . append ( path )
except Exception as e :
errors . append ( str ( e ) )
if saved :
lines = [ g [ " check " ] + " Copied " + str ( len ( saved ) ) + " file(s) to " + save_dir + " : " ]
for p in saved :
lines . append ( " " + os . path . basename ( p ) )
if errors :
lines . append ( g [ " cross " ] + " " + str ( len ( errors ) ) + " failed " )
status_text . set_text ( " \n " . join ( lines ) )
elif errors :
status_text . set_text ( g [ " cross " ] + " Failed: " + errors [ 0 ] )
else :
status_text . set_text ( " No files selected " )
dialog_widgets = list ( checkboxes )
dialog_widgets . append ( urwid . Divider ( g [ " divider1 " ] ) )
dialog_widgets . append ( urwid . Text ( " Copy to: " + save_dir ) )
dialog_widgets . append ( status_text )
dialog_widgets . append ( urwid . Text ( " " ) )
dialog_widgets . append ( urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Copy to Downloads " , on_press = do_save ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " Close " , on_press = dismiss_dialog ) ) ,
] ) )
dialog = DialogLineBox ( urwid . ListBox ( urwid . SimpleFocusListWalker ( dialog_widgets ) ) , title = " Attachments " )
dialog . delegate = self
bottom = self . messagelist
overlay = urwid . Overlay ( dialog , bottom , align = urwid . CENTER , width = ( " relative " , 80 ) , valign = urwid . MIDDLE , height = ( " relative " , 80 ) , left = 2 , right = 2 )
self . frame . contents [ " body " ] = ( overlay , self . frame . options ( ) )
self . frame . focus_position = " body "
2021-05-13 16:39:31 +02:00
def send_message ( self ) :
2021-05-14 15:13:17 +02:00
content = self . content_editor . get_edit_text ( )
title = self . title_editor . get_edit_text ( )
2021-05-13 16:39:31 +02:00
if not content == " " :
2026-03-28 18:19:11 -04:00
fields = None
if self . pending_attachments :
file_attachments = [ ]
for file_path in self . pending_attachments :
try :
with open ( file_path , " rb " ) as af :
file_data = af . read ( )
file_name = os . path . basename ( file_path )
file_attachments . append ( [ file_name , file_data ] )
except Exception as e :
RNS . log ( " Error reading attachment " + str ( file_path ) + " : " + str ( e ) , RNS . LOG_ERROR )
if file_attachments :
fields = { LXMF . FIELD_FILE_ATTACHMENTS : file_attachments }
if self . conversation . send ( content , title , fields = fields ) :
2021-05-14 22:12:42 +02:00
self . clear_editor ( )
2026-03-28 18:19:11 -04:00
def attach_file ( self ) :
self . dialog_active = True
browser = FileBrowserDialog ( self )
bottom = self . messagelist
overlay = urwid . Overlay ( browser , bottom , align = urwid . CENTER , width = ( " relative " , 90 ) , valign = urwid . MIDDLE , height = ( " relative " , 80 ) , left = 2 , right = 2 )
self . frame . contents [ " body " ] = ( overlay , self . frame . options ( ) )
self . frame . focus_position = " body "
def file_browser_closed ( self ) :
self . dialog_active = False
self . frame . contents [ " footer " ] = ( self . _build_footer ( ) , None )
self . conversation_changed ( None )
2021-05-13 16:39:31 +02:00
2023-10-15 20:35:21 +02:00
def paper_message_saved ( self , path ) :
g = self . app . ui . glyphs
def dismiss_dialog ( sender ) :
self . dialog_open = False
self . conversation_changed ( None )
dialog = DialogLineBox (
urwid . Pile ( [
2024-01-18 11:58:22 +01:00
urwid . Text ( " The paper message was saved to: \n \n " + str ( path ) + " \n " , align = urwid . CENTER ) ,
urwid . Columns ( [
( urwid . WEIGHT , 0.6 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.4 , urwid . Button ( " OK " , on_press = dismiss_dialog ) ) ,
] )
2023-10-16 00:16:04 +02:00
] ) , title = g [ " papermsg " ] . replace ( " " , " " )
2023-10-15 20:35:21 +02:00
)
dialog . delegate = self
bottom = self . messagelist
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay ( dialog , bottom , align = urwid . CENTER , width = 60 , valign = urwid . MIDDLE , height = urwid . PACK , left = 2 , right = 2 )
2023-10-15 20:35:21 +02:00
self . frame . contents [ " body " ] = ( overlay , self . frame . options ( ) )
2024-01-18 11:58:22 +01:00
self . frame . focus_position = " body "
2023-10-15 20:35:21 +02:00
def print_paper_message_qr ( self ) :
2022-11-19 20:04:01 +01:00
content = self . content_editor . get_edit_text ( )
title = self . title_editor . get_edit_text ( )
if not content == " " :
if self . conversation . paper_output ( content , title ) :
self . clear_editor ( )
else :
self . paper_message_failed ( )
2023-10-15 20:35:21 +02:00
def save_paper_message_qr ( self ) :
content = self . content_editor . get_edit_text ( )
title = self . title_editor . get_edit_text ( )
if not content == " " :
output_result = self . conversation . paper_output ( content , title , mode = " save_qr " )
if output_result != False :
self . clear_editor ( )
self . paper_message_saved ( output_result )
else :
self . paper_message_failed ( )
def save_paper_message_uri ( self ) :
content = self . content_editor . get_edit_text ( )
title = self . title_editor . get_edit_text ( )
if not content == " " :
output_result = self . conversation . paper_output ( content , title , mode = " save_uri " )
if output_result != False :
self . clear_editor ( )
self . paper_message_saved ( output_result )
else :
self . paper_message_failed ( )
def paper_message ( self ) :
def dismiss_dialog ( sender ) :
self . dialog_open = False
self . conversation_changed ( None )
def print_qr ( sender ) :
dismiss_dialog ( self )
self . print_paper_message_qr ( )
def save_qr ( sender ) :
dismiss_dialog ( self )
self . save_paper_message_qr ( )
def save_uri ( sender ) :
dismiss_dialog ( self )
self . save_paper_message_uri ( )
dialog = DialogLineBox (
urwid . Pile ( [
2024-01-18 11:58:22 +01:00
urwid . Text (
" Select the desired paper message output method. \n Saved files will be written to: \n \n " + str ( self . app . downloads_path ) + " \n " ,
align = urwid . CENTER ,
) ,
2023-10-15 20:35:21 +02:00
urwid . Columns ( [
2024-01-18 11:58:22 +01:00
( urwid . WEIGHT , 0.5 , urwid . Button ( " Print QR " , on_press = print_qr ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.5 , urwid . Button ( " Save QR " , on_press = save_qr ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.5 , urwid . Button ( " Save URI " , on_press = save_uri ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.5 , urwid . Button ( " Cancel " , on_press = dismiss_dialog ) )
2023-10-15 20:35:21 +02:00
] )
] ) , title = " Create Paper Message "
)
dialog . delegate = self
bottom = self . messagelist
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay ( dialog , bottom , align = urwid . CENTER , width = 60 , valign = urwid . MIDDLE , height = urwid . PACK , left = 2 , right = 2 )
2023-10-15 20:35:21 +02:00
self . frame . contents [ " body " ] = ( overlay , self . frame . options ( ) )
2024-01-18 11:58:22 +01:00
self . frame . focus_position = " body "
2023-10-15 20:35:21 +02:00
2022-11-19 20:04:01 +01:00
def paper_message_failed ( self ) :
def dismiss_dialog ( sender ) :
self . dialog_open = False
self . conversation_changed ( None )
dialog = DialogLineBox (
urwid . Pile ( [
2024-01-18 11:58:22 +01:00
urwid . Text (
" Could not output paper message, \n check your settings. See the log \n file for any error messages. \n " ,
align = urwid . CENTER ,
) ,
urwid . Columns ( [
( urwid . WEIGHT , 0.6 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.4 , urwid . Button ( " OK " , on_press = dismiss_dialog ) ) ,
] )
2022-11-19 20:04:01 +01:00
] ) , title = " ! "
)
dialog . delegate = self
bottom = self . messagelist
2024-01-18 11:58:22 +01:00
overlay = urwid . Overlay ( dialog , bottom , align = urwid . CENTER , width = 34 , valign = urwid . MIDDLE , height = urwid . PACK , left = 2 , right = 2 )
2022-11-19 20:04:01 +01:00
self . frame . contents [ " body " ] = ( overlay , self . frame . options ( ) )
2024-01-18 11:58:22 +01:00
self . frame . focus_position = " body "
2022-11-19 20:04:01 +01:00
2021-05-13 16:39:31 +02:00
def close ( self ) :
self . delegate . close_conversation ( self )
class LXMessageWidget ( urwid . WidgetWrap ) :
def __init__ ( self , message ) :
app = nomadnet . NomadNetworkApp . get_shared_instance ( )
2021-07-02 13:35:10 +02:00
g = app . ui . glyphs
2021-05-13 16:39:31 +02:00
self . timestamp = message . get_timestamp ( )
2021-09-23 17:45:01 +02:00
self . sort_timestamp = message . sort_timestamp
2026-03-28 18:19:11 -04:00
self . transfer_done = False
self . _live_lxm = None
msg_hash = message . get_hash ( )
msg_state = message . get_state ( )
msg_source_hash = message . _cached_source_hash
msg_method = message . _cached_method
2021-05-13 16:39:31 +02:00
time_format = app . time_format
message_time = datetime . fromtimestamp ( self . timestamp )
2021-05-14 19:38:21 +02:00
encryption_string = " "
if message . get_transport_encrypted ( ) :
2026-03-28 18:19:11 -04:00
encryption_string = " " + g [ " encrypted " ]
2021-05-14 19:38:21 +02:00
else :
2026-03-28 18:19:11 -04:00
encryption_string = " " + g [ " plaintext " ]
2021-05-13 16:39:31 +02:00
2026-03-28 18:19:11 -04:00
title_string = relative_time ( self . timestamp ) + " | " + message_time . strftime ( time_format ) + encryption_string
is_outbound = False
if msg_source_hash is None :
header_style = " msg_header_failed "
title_string = g [ " warning " ] + " " + title_string
elif app . lxmf_destination . hash == msg_source_hash :
is_outbound = True
if msg_state == LXMF . LXMessage . DELIVERED :
2021-05-13 16:39:31 +02:00
header_style = " msg_header_delivered "
2026-03-28 18:19:11 -04:00
title_string = g [ " check " ] + " " + g [ " arrow_r " ] + " " + title_string
elif msg_state == LXMF . LXMessage . FAILED :
header_style = " msg_header_failed "
title_string = g [ " cross " ] + " " + g [ " arrow_r " ] + " " + title_string
elif msg_state == LXMF . LXMessage . REJECTED :
2021-05-14 14:45:20 +02:00
header_style = " msg_header_failed "
2026-03-28 18:19:11 -04:00
title_string = g [ " cross " ] + " " + g [ " arrow_r " ] + " Rejected " + title_string
elif msg_method == LXMF . LXMessage . PROPAGATED and msg_state == LXMF . LXMessage . SENT :
2021-10-07 21:04:11 +02:00
header_style = " msg_header_propagated "
2026-03-28 18:19:11 -04:00
title_string = g [ " sent " ] + " " + g [ " arrow_r " ] + " " + title_string
elif msg_method == LXMF . LXMessage . PAPER and msg_state == LXMF . LXMessage . PAPER :
2022-11-19 20:04:01 +01:00
header_style = " msg_header_propagated "
2026-03-28 18:19:11 -04:00
title_string = g [ " papermsg " ] + " " + g [ " arrow_r " ] + " " + title_string
elif msg_state == LXMF . LXMessage . SENT :
2021-10-03 18:44:00 +02:00
header_style = " msg_header_sent "
2026-03-28 18:19:11 -04:00
title_string = g [ " sent " ] + " " + g [ " arrow_r " ] + " " + title_string
2021-05-13 16:39:31 +02:00
else :
header_style = " msg_header_sent "
2021-07-02 13:35:10 +02:00
title_string = g [ " arrow_r " ] + " " + title_string
2021-05-12 14:02:44 +02:00
else :
2021-05-13 16:39:31 +02:00
if message . signature_validated ( ) :
header_style = " msg_header_ok "
2026-03-28 18:19:11 -04:00
title_string = g [ " check " ] + " " + g [ " arrow_l " ] + " " + title_string
2021-05-13 16:39:31 +02:00
else :
header_style = " msg_header_caution "
2026-03-28 18:19:11 -04:00
title_string = g [ " warning " ] + " " + g [ " arrow_l " ] + " " + message . get_signature_description ( ) + " \n " + title_string
2021-05-13 16:39:31 +02:00
if message . get_title ( ) != " " :
title_string + = " | " + message . get_title ( )
2026-03-28 18:19:11 -04:00
has_attachments = message . has_attachments ( )
cached_names = message . _cached_attachment_names or [ ]
if has_attachments and cached_names :
attachment_strings = [ ]
for atype , aname , * arest in cached_names :
attachment_strings . append ( g [ atype if atype != " file " else " file " ] + " " + aname )
title_string + = " | " + " " . join ( attachment_strings )
2021-05-13 16:39:31 +02:00
title = urwid . AttrMap ( urwid . Text ( title_string ) , header_style )
2026-03-28 18:19:11 -04:00
self . progress_widget = urwid . Text ( " " )
self . progress_attr = urwid . AttrMap ( self . progress_widget , " progress_full " )
content_text = message . get_content ( )
content_lines = content_text . split ( " \n " )
indented = " \n " . join ( " " + line for line in content_lines )
pile_widgets = [ title ]
2021-05-13 16:39:31 +02:00
2026-03-28 18:19:11 -04:00
if is_outbound and msg_state is not None and msg_state < LXMF . LXMessage . SENT and msg_hash is not None :
try :
for pending in app . message_router . pending_outbound :
if pending . hash == msg_hash :
if pending . representation == LXMF . LXMessage . RESOURCE :
self . _live_lxm = pending
break
except Exception :
pass
if self . _live_lxm is not None :
pct = int ( self . _live_lxm . progress * 100 )
bar_width = 20
filled = int ( bar_width * self . _live_lxm . progress )
if app . ui . colormode > = 256 :
bar = " \u2588 " * filled + " \u2591 " * ( bar_width - filled )
else :
bar = " # " * filled + " - " * ( bar_width - filled )
self . progress_widget . set_text ( " [ " + bar + " ] " + str ( pct ) + " % " )
pile_widgets . append ( self . progress_attr )
self . _start_progress_poll ( )
pile_widgets . append ( urwid . Text ( indented ) )
if has_attachments and cached_names :
att_file_idx = 0
for atype , aname , * arest in cached_names :
glyph = g [ " file " ] if atype == " file " else g [ atype ]
asize = arest [ 0 ] if arest else 0
label = " " + glyph + " " + aname
if asize > 0 :
label + = " ( " + _format_size ( asize ) + " ) "
if atype == " file " :
pile_widgets . append ( ClickableAttachment ( label , aname , message , " file " , att_file_idx ) )
att_file_idx + = 1
else :
pile_widgets . append ( ClickableAttachment ( label , aname , message , atype ) )
pile_widgets . append ( urwid . Text ( " " ) )
super ( ) . __init__ ( urwid . Pile ( pile_widgets ) )
def _start_progress_poll ( self ) :
try :
loop = nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . loop
if loop :
loop . set_alarm_in ( 0.3 , self . _poll_progress )
except Exception :
pass
def _poll_progress ( self , loop = None , user_data = None ) :
if self . transfer_done :
return
if self . _live_lxm is None :
self . transfer_done = True
return
app = nomadnet . NomadNetworkApp . get_shared_instance ( )
g = app . ui . glyphs
progress = self . _live_lxm . progress
state = self . _live_lxm . state
pct = int ( progress * 100 )
if state == LXMF . LXMessage . FAILED :
self . progress_widget . set_text ( " " + g [ " cross " ] + " Transfer failed " )
self . transfer_done = True
self . _live_lxm = None
elif state == LXMF . LXMessage . REJECTED :
self . progress_widget . set_text ( " " + g [ " cross " ] + " Rejected: too large or not accepted " )
self . transfer_done = True
self . _live_lxm = None
elif state > = LXMF . LXMessage . SENT :
self . progress_widget . set_text ( " " )
self . transfer_done = True
self . _live_lxm = None
else :
bar_width = 20
filled = int ( bar_width * progress )
if app . ui . colormode > = 256 :
bar = " \u2588 " * filled + " \u2591 " * ( bar_width - filled )
else :
bar = " # " * filled + " - " * ( bar_width - filled )
self . progress_widget . set_text ( " [ " + bar + " ] " + str ( pct ) + " % " )
if not self . transfer_done :
try :
ui_loop = app . ui . loop
if ui_loop :
ui_loop . set_alarm_in ( 0.3 , self . _poll_progress )
ui_loop . draw_screen ( )
except Exception :
pass
class ClickableAttachment ( urwid . Text ) :
def __init__ ( self , label , filename , conv_message , field_type , field_index = 0 ) :
self . filename = filename
self . conv_message = conv_message
self . field_type = field_type
self . field_index = field_index
self . saved = False
super ( ) . __init__ ( label )
def mouse_event ( self , size , event , button , x , y , focus ) :
if button == 1 and urwid . util . is_mouse_press ( event ) :
self . _save ( )
return True
return False
def _save ( self ) :
if self . saved :
return
app = nomadnet . NomadNetworkApp . get_shared_instance ( )
g = app . ui . glyphs
try :
src_path = self . conv_message . get_attachment_file_path ( self . field_type , self . field_index )
if src_path and os . path . isfile ( src_path ) :
save_path = _copy_attachment_to_dest ( self . filename , src_path )
else :
if self . field_type == " file " :
attachments = self . conv_message . get_file_attachments ( )
if self . field_index < len ( attachments ) :
att = attachments [ self . field_index ]
if isinstance ( att , list ) and len ( att ) > = 2 :
data = att [ 1 ] if isinstance ( att [ 1 ] , bytes ) else b " "
else :
data = b " "
else :
data = b " "
elif self . field_type == " image " :
data = self . conv_message . get_image ( )
data = data if isinstance ( data , bytes ) else b " "
elif self . field_type == " audio " :
data = self . conv_message . get_audio ( )
data = data if isinstance ( data , bytes ) else b " "
else :
data = b " "
self . conv_message . unload ( )
if not data :
return
save_path = _save_attachment_to_disk ( self . filename , data )
self . saved = True
self . set_text ( " " + g [ " check " ] + " Copied to: " + save_path )
except Exception as e :
RNS . log ( " Error saving attachment: " + str ( e ) , RNS . LOG_ERROR )
self . set_text ( " " + g [ " cross " ] + " Save failed: " + str ( e ) )
2026-05-14 17:53:51 -04:00
def _resolve_attachment_save_path ( filename ) :
2026-03-28 18:19:11 -04:00
app = nomadnet . NomadNetworkApp . get_shared_instance ( )
save_dir = app . attachment_save_path if app . attachment_save_path else app . downloads_path
if not os . path . isdir ( save_dir ) :
os . makedirs ( save_dir )
2026-05-14 17:53:51 -04:00
safe_name = ConversationMessage . safe_attachment_name ( filename )
base_dir = os . path . realpath ( save_dir ) + os . sep
candidate = os . path . realpath ( os . path . join ( save_dir , safe_name ) )
if not ( candidate + os . sep ) . startswith ( base_dir ) :
raise OSError ( 13 , os . strerror ( 13 ) )
2026-03-28 18:19:11 -04:00
counter = 0
2026-05-14 17:53:51 -04:00
base , ext = os . path . splitext ( safe_name )
while os . path . isfile ( candidate ) :
2026-03-28 18:19:11 -04:00
counter + = 1
2026-05-14 17:53:51 -04:00
candidate = os . path . realpath ( os . path . join ( save_dir , base + " _ " + str ( counter ) + ext ) )
if not ( candidate + os . sep ) . startswith ( base_dir ) :
raise OSError ( 13 , os . strerror ( 13 ) )
return candidate
def _copy_attachment_to_dest ( filename , src_path ) :
save_path = _resolve_attachment_save_path ( filename )
2026-03-28 18:19:11 -04:00
shutil . copy2 ( src_path , save_path )
return save_path
def _save_attachment_to_disk ( filename , data ) :
2026-05-14 17:53:51 -04:00
save_path = _resolve_attachment_save_path ( filename )
2026-03-28 18:19:11 -04:00
with open ( save_path , " wb " ) as f :
f . write ( data )
return save_path
class FileBrowserEntry ( urwid . WidgetWrap ) :
signals = [ " click " ]
def __init__ ( self , name , full_path , is_dir = False , is_parent = False , selected = False ) :
self . full_path = full_path
self . name = name
self . is_dir = is_dir
self . is_parent = is_parent
self . selected = selected
g = nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . glyphs
if is_parent :
display = g [ " arrow_l " ] + " .. "
elif is_dir :
display = g [ " arrow_r " ] + " " + name + " / "
elif selected :
display = g [ " check " ] + " " + name
else :
display = " " + name
self . text_widget = urwid . SelectableIcon ( display , 0 )
if is_dir or is_parent :
style = " list_trusted "
focus_style = " list_focus "
elif selected :
style = " list_trusted "
focus_style = " list_focus_trusted "
else :
style = " list_unknown "
focus_style = " list_focus "
display_widget = urwid . AttrMap ( self . text_widget , style , focus_style )
2024-01-18 10:22:38 +01:00
super ( ) . __init__ ( display_widget )
2021-10-07 18:14:06 +02:00
2026-03-28 18:19:11 -04:00
def keypress ( self , size , key ) :
if key == " enter " :
self . _emit ( " click " )
else :
return key
def mouse_event ( self , size , event , button , x , y , focus ) :
if button == 1 and urwid . util . is_mouse_press ( event ) :
self . _emit ( " click " )
return True
return False
class FileBrowserDialog ( urwid . WidgetWrap ) :
def __init__ ( self , delegate ) :
self . delegate = delegate
app = nomadnet . NomadNetworkApp . get_shared_instance ( )
self . g = app . ui . glyphs
self . current_path = os . path . expanduser ( " ~ " )
self . path_label = urwid . Text ( " " )
self . status_label = urwid . Text ( " " )
self . file_walker = urwid . SimpleFocusListWalker ( [ ] )
self . file_listbox = urwid . ListBox ( self . file_walker )
self . button_columns = urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Done " , on_press = self . _dismiss ) ) ,
( urwid . WEIGHT , 0.1 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " Cancel " , on_press = self . _cancel ) ) ,
] )
header_pile = urwid . Pile ( [
self . path_label ,
self . status_label ,
urwid . Divider ( self . g [ " divider1 " ] ) ,
] )
footer_pile = urwid . Pile ( [
urwid . Divider ( self . g [ " divider1 " ] ) ,
self . button_columns ,
] )
self . _populate ( )
self . browser_frame = urwid . Frame (
self . file_listbox ,
header = header_pile ,
footer = footer_pile ,
)
linebox = urwid . LineBox ( self . browser_frame , title = " Attach File " )
super ( ) . __init__ ( linebox )
def _update_status ( self ) :
pending = self . delegate . pending_attachments
if pending :
names = [ os . path . basename ( p ) for p in pending ]
self . status_label . set_text ( " " + self . g [ " file " ] + " " + str ( len ( pending ) ) + " selected: " + " , " . join ( names ) )
else :
self . status_label . set_text ( " No files selected " )
def _populate ( self ) :
self . path_label . set_text ( " " + self . current_path )
self . _update_status ( )
focus_pos = None
try :
focus_pos = self . file_listbox . focus_position
except Exception :
pass
entries = [ ]
parent = os . path . dirname ( self . current_path )
if parent != self . current_path :
entry = FileBrowserEntry ( " .. " , parent , is_parent = True )
urwid . connect_signal ( entry , " click " , self . _entry_clicked , entry )
entries . append ( entry )
try :
items = sorted ( os . listdir ( self . current_path ) )
except PermissionError :
entries . append ( urwid . Text ( ( " error_text " , " Permission denied " ) ) )
self . file_walker [ : ] = entries
return
dirs = [ ]
files = [ ]
for item in items :
if item . startswith ( " . " ) :
continue
full = os . path . join ( self . current_path , item )
if os . path . isdir ( full ) :
dirs . append ( ( item , full ) )
elif os . path . isfile ( full ) :
files . append ( ( item , full ) )
for name , full in dirs :
entry = FileBrowserEntry ( name , full , is_dir = True )
urwid . connect_signal ( entry , " click " , self . _entry_clicked , entry )
entries . append ( entry )
for name , full in files :
is_selected = full in self . delegate . pending_attachments
entry = FileBrowserEntry ( name , full , selected = is_selected )
urwid . connect_signal ( entry , " click " , self . _entry_clicked , entry )
entries . append ( entry )
if not dirs and not files :
entries . append ( urwid . Text ( ( " inactive_text " , " (empty) " ) ) )
self . file_walker [ : ] = entries
if focus_pos is not None and focus_pos < len ( entries ) :
self . file_listbox . set_focus ( focus_pos )
elif entries :
self . file_listbox . set_focus ( 0 )
def _entry_clicked ( self , entry_widget , user_data = None ) :
entry = user_data if user_data else entry_widget
if entry . is_dir or entry . is_parent :
self . current_path = entry . full_path
self . _populate ( )
else :
if entry . full_path in self . delegate . pending_attachments :
self . delegate . pending_attachments . remove ( entry . full_path )
else :
self . delegate . pending_attachments . append ( entry . full_path )
self . delegate . frame . contents [ " footer " ] = ( self . delegate . _build_footer ( ) , None )
self . _populate ( )
def _dismiss ( self , sender ) :
self . delegate . file_browser_closed ( )
def _cancel ( self , sender ) :
self . delegate . pending_attachments . clear ( )
self . delegate . frame . contents [ " footer " ] = ( self . delegate . _build_footer ( ) , None )
self . delegate . file_browser_closed ( )
def keypress ( self , size , key ) :
if key == " esc " :
self . delegate . file_browser_closed ( )
return
result = super ( ) . keypress ( size , key )
if result == " down " and self . browser_frame . focus_position == " body " :
self . browser_frame . focus_position = " footer "
return
elif result == " up " and self . browser_frame . focus_position == " footer " :
self . browser_frame . focus_position = " body "
return
return result
2021-10-07 18:14:06 +02:00
class SyncProgressBar ( urwid . ProgressBar ) :
def get_text ( self ) :
status = nomadnet . NomadNetworkApp . get_shared_instance ( ) . get_sync_status ( )
show_percent = nomadnet . NomadNetworkApp . get_shared_instance ( ) . sync_status_show_percent ( )
if show_percent :
return status + " " + super ( ) . get_text ( )
else :
return status