2021-05-04 20:53:03 +02:00
import RNS
2026-05-20 16:16:24 -04:00
import RNS . vendor . umsgpack as msgpack
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-05-20 16:16:24 -04:00
from LXMF import pn_announce_data_is_valid , PN_META_NAME
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-05-19 23:20:19 +02:00
from RNS . Utilities . rngit . util import MarkdownToMicron
from RNS . Utilities . rngit . highlight import SyntaxHighlighter
from . MicronParser import markup_to_attrmaps
from nomadnet . util import strip_modifiers , strip_micron , strip_escaped_micron , unescape_micron , strip_non_formatting_tags
from nomadnet . ui import THEME_DARK , THEME_LIGHT
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-05-21 02:12:57 +02: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-p] My LXMF [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
2026-05-20 16:16:24 -04:00
class TabButton ( urwid . Button ) :
button_left = urwid . Text ( " [ " )
button_right = urwid . Text ( " ] " )
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 ( )
2026-05-21 02:10:10 +02:00
elif key == " ctrl p " :
2026-05-20 16:16:24 -04:00
self . delegate . show_my_qr ( )
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 "
2026-05-20 16:16:24 -04:00
elif key == " up " :
if self . delegate . ilb . body_is_empty ( ) :
self . delegate . app . ui . main_display . frame . focus_position = " header "
return None
result = super ( ConversationsArea , self ) . keypress ( size , key )
if result == " up " :
self . delegate . app . ui . main_display . frame . focus_position = " header "
return None
return result
2021-05-13 16:39:31 +02:00
else :
return super ( ConversationsArea , self ) . keypress ( size , key )
2021-05-04 11:08:16 +02:00
2026-05-20 16:16:24 -04:00
class PropNodePicker ( urwid . WidgetWrap ) :
def __init__ ( self , options , current_hash , on_change ) :
self . _options = list ( options )
self . _current = current_hash
self . _on_change = on_change
self . _pile = urwid . Pile ( [ ] )
super ( ) . __init__ ( self . _pile )
self . _show_collapsed ( )
def _label_for ( self , h ) :
for ph , label in self . _options :
if ph == h :
return label
if h is not None :
return " < " + RNS . hexrep ( h , delimit = False ) + " > "
return " (select propagation node) "
def _show_collapsed ( self ) :
btn = urwid . Button ( " ▾ " + self . _label_for ( self . _current ) )
urwid . connect_signal ( btn , " click " , self . _on_expand_click )
self . _pile . contents = [
( urwid . AttrMap ( btn , None , focus_map = " list_focus " ) , self . _pile . options ( ) ) ,
]
def _on_expand_click ( self , _btn ) :
self . _show_expanded ( )
def _on_back_click ( self , _btn ) :
self . _show_collapsed ( )
def _make_row_click ( self , picked_hash ) :
def _click ( _btn ) :
try :
self . _current = picked_hash
self . _on_change ( picked_hash )
except Exception as e :
RNS . log ( " Propagation node change handler failed: " + str ( e ) , RNS . LOG_ERROR )
self . _show_collapsed ( )
return _click
def _show_expanded ( self ) :
rows = [ ]
for ph , label in self . _options :
marker = " ● " if ph == self . _current else " ○ "
row = urwid . Button ( marker + label )
urwid . connect_signal ( row , " click " , self . _make_row_click ( ph ) )
rows . append ( urwid . AttrMap ( row , None , focus_map = " list_focus " ) )
if not rows :
rows . append ( urwid . Text ( " (no propagation nodes seen yet) " ) )
list_height = min ( 10 , max ( 3 , len ( rows ) ) )
listbox = urwid . ListBox ( urwid . SimpleFocusListWalker ( rows ) )
boxed = urwid . BoxAdapter ( listbox , list_height )
back = urwid . Button ( " ◀ Back " )
urwid . connect_signal ( back , " click " , self . _on_back_click )
self . _pile . contents = [
( urwid . AttrMap ( back , None , focus_map = " list_focus " ) , self . _pile . options ( ) ) ,
( boxed , self . _pile . options ( ) ) ,
]
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
2026-05-20 16:16:24 -04:00
LIST_FILTER_TRUSTED = " trusted "
LIST_FILTER_UNTRUSTED = " untrusted "
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
2026-05-20 16:16:24 -04:00
self . list_filter = ConversationsDisplay . LIST_FILTER_TRUSTED
self . show_blocked = False
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 ( )
2026-05-20 16:16:24 -04:00
self . _build_persistent_listbox ( )
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
2026-05-20 16:16:24 -04:00
self . widget = urwid . WidgetPlaceholder ( 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 )
2026-05-20 16:16:24 -04:00
try :
self . app . ui . loop . set_alarm_in ( 30.0 , self . _refresh_sync_status )
except Exception :
pass
2026-05-17 02:02:27 +02:00
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
2026-05-20 16:16:24 -04:00
def _conversation_filter_predicate ( self , conversation ) :
try :
trust_level = conversation [ 2 ]
except Exception :
return False
if self . list_filter == ConversationsDisplay . LIST_FILTER_UNTRUSTED :
return trust_level in ( DirectoryEntry . UNTRUSTED , DirectoryEntry . WARNING , DirectoryEntry . UNKNOWN )
return trust_level == DirectoryEntry . TRUSTED
2026-03-28 18:19:11 -04:00
2026-05-20 16:16:24 -04:00
def _set_filter ( self , key ) :
if self . list_filter == key :
return
self . list_filter = key
try :
self . update_conversation_list ( )
except Exception as e :
RNS . log ( " Failed to apply conversation filter: " + str ( e ) , RNS . LOG_ERROR )
def _on_show_blocked_change ( self , _cb , new_state ) :
self . show_blocked = new_state
try :
self . update_conversation_list ( )
except Exception as e :
RNS . log ( " Failed to toggle show-blocked: " + str ( e ) , RNS . LOG_ERROR )
def _apply_pile_layout ( self ) :
pack_opts = self . pile . options ( ' pack ' )
weight_opts = self . pile . options ( ' weight ' , 1 )
items = [ ( self . tab_bar , pack_opts ) ]
if self . list_filter == ConversationsDisplay . LIST_FILTER_UNTRUSTED :
items . append ( ( self . show_blocked_checkbox , pack_opts ) )
items . append ( ( self . ilb , weight_opts ) )
items . append ( ( self . sync_status_text , pack_opts ) )
try :
prev_focus = self . pile . focus_position
except Exception :
prev_focus = None
self . pile . contents = items
try :
if prev_focus is not None and prev_focus < len ( items ) :
self . pile . focus_position = prev_focus
except Exception :
pass
def _blocked_row_widget ( self , dest_hash ) :
g = self . app . ui . glyphs
entry = self . app . directory . find ( dest_hash )
display_name = None
if entry is not None and getattr ( entry , " display_name " , None ) :
display_name = strip_modifiers ( entry . display_name )
if not display_name :
display_name = RNS . prettyhexrep ( dest_hash )
label = " " + g [ " cross " ] + " [blocked] " + display_name + " < " + RNS . hexrep ( dest_hash , delimit = False ) + " > "
widget = ListEntry ( label )
urwid . connect_signal ( widget , " click " , self . _unblock_dialog , dest_hash )
attr = urwid . AttrMap ( widget , " list_untrusted " , " list_focus_untrusted " )
attr . blocked_dest_hash = dest_hash
return attr
def _unblock_dialog ( self , _sender , dest_hash ) :
self . dialog_open = True
def dismiss ( _b ) :
self . dialog_open = False
self . update_conversation_list ( )
def confirmed ( _b ) :
self . dialog_open = False
try :
self . app . unblock_destination ( dest_hash )
except Exception as e :
RNS . log ( " Unblock failed: " + str ( e ) , RNS . LOG_ERROR )
self . update_conversation_list ( )
try :
who = self . app . directory . simplest_display_str ( dest_hash )
except Exception :
who = RNS . hexrep ( dest_hash , delimit = False )
dialog = DialogLineBox (
urwid . Pile ( [
urwid . Text ( " " ) ,
urwid . Text ( " Unblock " + str ( who ) + " ? \n \n This lifts the RNS blackhole on the peer ' s identity \n and removes them from your ignored list. \n " , align = urwid . CENTER ) ,
urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Yes, unblock " , on_press = confirmed ) ) ,
( urwid . WEIGHT , 0.10 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " Cancel " , on_press = dismiss ) ) ,
] ) ,
] ) , title = " Confirm unblock "
)
dialog . delegate = self
bottom = self . listbox
overlay = urwid . Overlay ( dialog , bottom , align = urwid . CENTER , width = urwid . RELATIVE_100 , valign = urwid . MIDDLE , height = urwid . PACK , left = 2 , right = 2 )
try :
self . columns_widget . contents [ 0 ] = (
overlay ,
self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width ) ,
)
self . columns_widget . focus_position = 0
except Exception :
pass
def _build_persistent_listbox ( self ) :
self . tab_trusted = TabButton ( " Trusted (0) " , on_press = lambda _b : self . _set_filter ( ConversationsDisplay . LIST_FILTER_TRUSTED ) )
self . tab_untrusted = TabButton ( " Untrusted (0) " , on_press = lambda _b : self . _set_filter ( ConversationsDisplay . LIST_FILTER_UNTRUSTED ) )
self . tab_bar = urwid . Columns ( [
( ' weight ' , 1 , self . tab_trusted ) ,
( ' weight ' , 1 , self . tab_untrusted ) ,
] , dividechars = 1 )
self . show_blocked_checkbox = urwid . CheckBox ( " Show blocked (0) " , state = self . show_blocked )
urwid . connect_signal ( self . show_blocked_checkbox , " change " , self . _on_show_blocked_change )
2021-05-13 16:39:31 +02:00
self . ilb = IndicativeListBox (
2026-05-20 16:16:24 -04:00
[ urwid . Text ( " " ) ] ,
2021-05-13 16:39:31 +02:00
on_selection_change = self . conversation_list_selection ,
initialization_is_selection_change = False ,
2026-05-20 16:16:24 -04:00
highlight_offFocus = " list_off_focus " ,
2021-05-13 16:39:31 +02:00
)
2026-05-20 16:16:24 -04:00
self . sync_status_text = urwid . AttrMap ( urwid . Text ( self . _sync_status_line ( ) , align = urwid . LEFT ) , " shortcutbar " )
self . pile = urwid . Pile ( [
( ' pack ' , self . tab_bar ) ,
( ' weight ' , 1 , self . ilb ) ,
( ' pack ' , self . sync_status_text ) ,
] )
try : self . pile . focus_position = 1
except Exception : pass
self . listbox = ConversationsArea ( self . pile , title = " Conversations " )
2021-05-13 16:39:31 +02:00
self . listbox . delegate = self
2026-05-20 16:16:24 -04:00
def update_listbox ( self ) :
if not hasattr ( self , " pile " ) :
self . _build_persistent_listbox ( )
try :
conversations = self . app . conversations ( )
except Exception as e :
RNS . log ( " Failed to enumerate conversations: " + str ( e ) , RNS . LOG_ERROR )
conversations = [ ]
try :
if self . list_sort_mode == ConversationsDisplay . SORT_NAME :
conversations . sort ( key = lambda e : ( e [ 3 ] . lower ( ) , e [ 0 ] ) )
except Exception :
pass
def _is_pinned ( c ) :
try :
entry = self . app . directory . find ( bytes . fromhex ( c [ 0 ] ) )
return entry is not None and entry . sort_rank is not None
except Exception :
return False
try :
conversations = sorted ( conversations , key = lambda c : 0 if _is_pinned ( c ) else 1 )
except Exception :
pass
glyphs = self . app . ui . glyphs
def _alerts ( c ) :
return bool ( c [ 4 ] ) or ( len ( c ) > 6 and bool ( c [ 6 ] ) )
trusted_count = sum ( 1 for c in conversations if c [ 2 ] == DirectoryEntry . TRUSTED )
untrusted_count = sum ( 1 for c in conversations if c [ 2 ] in ( DirectoryEntry . UNTRUSTED , DirectoryEntry . WARNING , DirectoryEntry . UNKNOWN ) )
trusted_unread = sum ( 1 for c in conversations if c [ 2 ] == DirectoryEntry . TRUSTED and _alerts ( c ) )
untrusted_unread = sum ( 1 for c in conversations if c [ 2 ] in ( DirectoryEntry . UNTRUSTED , DirectoryEntry . WARNING , DirectoryEntry . UNKNOWN ) and _alerts ( c ) )
def _label ( name , total , unread ) :
if unread :
return f " { name } ( { total } ) { glyphs [ ' unread ' ] } { unread } "
return f " { name } ( { total } ) "
self . tab_trusted . set_label ( _label ( " Trusted " , trusted_count , trusted_unread ) )
self . tab_untrusted . set_label ( _label ( " Untrusted " , untrusted_count , untrusted_unread ) )
filtered = [ c for c in conversations if self . _conversation_filter_predicate ( c ) ]
conversation_list_widgets = [ ]
for conversation in filtered :
try :
conversation_list_widgets . append ( self . conversation_list_widget ( conversation ) )
except Exception as e :
try : hh = conversation [ 0 ]
except Exception : hh = " ? "
RNS . log ( " Skipping conversation row for " + str ( hh ) + " : " + str ( e ) , RNS . LOG_ERROR )
blocked_count = len ( self . app . ignored_list ) if hasattr ( self . app , " ignored_list " ) else 0
try :
self . show_blocked_checkbox . set_label ( f " Show blocked ( { blocked_count } ) " )
except Exception :
pass
if self . list_filter == ConversationsDisplay . LIST_FILTER_UNTRUSTED and self . show_blocked :
for blocked_hash in list ( self . app . ignored_list ) :
try :
conversation_list_widgets . append ( self . _blocked_row_widget ( blocked_hash ) )
except Exception as e :
RNS . log ( " Skipping blocked row: " + str ( e ) , RNS . LOG_ERROR )
empty_placeholder = False
if not conversation_list_widgets :
empty_label = {
ConversationsDisplay . LIST_FILTER_TRUSTED : " No trusted conversations " ,
ConversationsDisplay . LIST_FILTER_UNTRUSTED : " No untrusted conversations " ,
} . get ( self . list_filter , " No conversations " )
conversation_list_widgets = [ urwid . Text ( empty_label , align = ' center ' ) ]
empty_placeholder = True
self . list_widgets = conversation_list_widgets
try :
self . ilb . set_body ( conversation_list_widgets )
except Exception as e :
RNS . log ( " Failed to populate conversation list: " + str ( e ) , RNS . LOG_ERROR )
self . _apply_pile_layout ( )
if empty_placeholder :
try : self . pile . focus_position = 0
except Exception : pass
try :
self . sync_status_text . original_widget . set_text ( self . _sync_status_line ( ) )
except Exception :
pass
def _sync_status_line ( self ) :
try :
last = self . app . peer_settings . get ( " last_lxmf_sync " )
except Exception :
last = None
if not last :
when = " never "
else :
try :
when = relative_time ( float ( last ) )
except Exception :
when = " unknown "
node_label = None
try :
pn_hash = self . app . get_default_propagation_node ( )
if pn_hash is not None :
pn_ident = RNS . Identity . recall ( pn_hash )
if pn_ident is not None :
node_dest = RNS . Destination . hash_from_name_and_identity ( " nomadnetwork.node " , pn_ident )
entry = self . app . directory . find ( node_dest )
if entry is not None and getattr ( entry , " display_name " , None ) :
node_label = strip_modifiers ( str ( entry . display_name ) ) or None
if node_label is None :
node_label = " < " + RNS . hexrep ( pn_hash , delimit = False ) [ : 8 ] + " …> "
except Exception :
node_label = None
line = " Last sync: " + when
if node_label :
line + = " ( " + node_label + " ) "
return line
def _refresh_sync_status ( self , _loop = None , _data = None ) :
try :
if hasattr ( self , " sync_status_text " ) and self . sync_status_text is not None :
self . sync_status_text . original_widget . set_text ( self . _sync_status_line ( ) )
except Exception :
pass
try :
self . app . ui . loop . set_alarm_in ( 30.0 , self . _refresh_sync_status )
except Exception :
pass
2021-05-13 16:39:31 +02:00
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 )
2026-05-20 16:16:24 -04:00
def _refresh_open_conversation_widget ( self , source_hash_text ) :
widget = ConversationsDisplay . cached_conversation_widgets . get ( source_hash_text )
if widget is None :
return
try :
widget . _trust_banner_dismissed = False
except Exception :
pass
try :
widget . _refresh_trust_banner ( )
except Exception :
pass
try :
widget . _update_peer_info ( )
except Exception :
pass
try :
widget . update_message_widgets ( replace = True )
except Exception :
pass
def show_my_qr ( self ) :
try :
addr = RNS . hexrep ( self . app . lxmf_destination . hash , delimit = False )
except Exception :
return
try :
display = self . app . peer_settings . get ( " display_name " ) or " My LXMF "
except Exception :
display = " My LXMF "
self . show_qr_dialog ( addr , title = display )
def show_qr_dialog ( self , data , title = None ) :
qr_text = None
try :
import qrcode
try :
qr = qrcode . QRCode ( error_correction = qrcode . constants . ERROR_CORRECT_L , box_size = 1 , border = 1 )
qr . add_data ( data )
qr . make ( )
import io
buf = io . StringIO ( )
qr . print_ascii ( out = buf , invert = False )
qr_text = buf . getvalue ( ) . rstrip ( " \n " )
except Exception as e :
RNS . log ( " QR generation failed: " + str ( e ) , RNS . LOG_ERROR )
qr_text = None
except Exception :
qr_text = None
def dismiss ( _b ) :
self . _restore_listbox_pane ( )
rows = [ urwid . Text ( " " ) ]
if qr_text is not None :
rows . append ( urwid . Text ( qr_text , align = urwid . CENTER ) )
rows . append ( urwid . Text ( " " ) )
else :
rows . append ( urwid . Text ( " LXMF destination address: " , align = urwid . CENTER ) )
rows . append ( urwid . Text ( " " ) )
rows + = [
urwid . Text ( " < " + data + " > " , align = urwid . CENTER ) ,
urwid . Text ( " " ) ,
urwid . Columns ( [
( urwid . WEIGHT , 1 , urwid . Text ( " " ) ) ,
( 12 , urwid . Button ( " Close " , on_press = dismiss ) ) ,
( urwid . WEIGHT , 1 , urwid . Text ( " " ) ) ,
] ) ,
urwid . Text ( " " ) ,
]
dialog_title = " LXMF Address " if qr_text is None else " QR Code "
dialog = DialogLineBox ( urwid . Pile ( rows ) , title = dialog_title )
dialog . delegate = self
self . _overlay_dialog ( dialog )
def _overlay_dialog ( self , dialog ) :
overlay = urwid . Overlay (
dialog , self . columns_widget ,
align = urwid . CENTER , width = ( urwid . RELATIVE , 70 ) ,
valign = urwid . MIDDLE , height = urwid . PACK ,
min_width = 44 ,
)
try :
self . widget . original_widget = overlay
self . dialog_open = True
except Exception :
pass
def _restore_listbox_pane ( self ) :
try :
self . widget . original_widget = self . columns_widget
self . dialog_open = False
self . update_conversation_list ( )
except Exception :
pass
def _ping_peer_from_dialog ( self , source_hash_text , status_widget , ping_button ) :
try :
dest = bytes . fromhex ( source_hash_text )
except Exception :
status_widget . set_text ( ( " error_text " , " Invalid address " ) )
return
identity = RNS . Identity . recall ( dest )
if identity is None :
status_widget . set_text ( ( " error_text " , " Identity unknown; query first " ) )
return
if not RNS . Transport . has_path ( dest ) :
status_widget . set_text ( " No path; requesting… " )
try : RNS . Transport . request_path ( dest )
except Exception : pass
status_widget . set_text ( " Pinging… " )
ping_button . set_label ( " Pinging… " )
started_at = time . time ( )
def schedule_ui ( fn ) :
try :
self . app . ui . loop . set_alarm_in ( 0 , lambda * _ : fn ( ) )
except Exception :
try : fn ( )
except Exception : pass
def on_established ( link ) :
elapsed_ms = int ( ( time . time ( ) - started_at ) * 1000 )
try :
hops = RNS . Transport . hops_to ( dest )
if hops is None or hops > = RNS . Transport . PATHFINDER_M :
hops_str = " "
else :
hops_str = f " ( { hops } hop { ' s ' if hops != 1 else ' ' } ) "
except Exception :
hops_str = " "
def update ( ) :
status_widget . set_text ( f " Pong in { elapsed_ms } ms { hops_str } " )
ping_button . set_label ( " Ping " )
schedule_ui ( update )
try : link . teardown ( )
except Exception : pass
def on_closed ( link ) :
try :
if getattr ( link , " status " , None ) == RNS . Link . ACTIVE :
return
except Exception :
pass
def update ( ) :
if status_widget . text . strip ( ) in ( " Pinging… " , " " ) :
status_widget . set_text ( ( " error_text " , " Ping failed (no link) " ) )
ping_button . set_label ( " Ping " )
schedule_ui ( update )
try :
destination = RNS . Destination ( identity , RNS . Destination . OUT , RNS . Destination . SINGLE , " lxmf " , " delivery " )
RNS . Link ( destination , established_callback = on_established , closed_callback = on_closed )
except Exception as e :
status_widget . set_text ( ( " error_text " , f " Ping init failed: { e } " ) )
ping_button . set_label ( " Ping " )
def _block_peer_from_dialog ( self , source_hash_text ) :
try :
dest = bytes . fromhex ( source_hash_text )
except Exception :
return
def cancel_block ( _b ) :
self . update_conversation_list ( )
self . dialog_open = False
def confirm_block ( _b ) :
try :
self . app . block_destination ( dest , reason = " user-blocked from peer info dialog " )
except Exception as e :
RNS . log ( " Block failed: " + str ( e ) , RNS . LOG_ERROR )
try :
self . delete_conversation ( source_hash_text )
nomadnet . Conversation . delete_conversation ( source_hash_text , self . app )
except Exception :
pass
self . update_conversation_list ( )
self . dialog_open = False
try :
who = self . app . directory . simplest_display_str ( dest )
except Exception :
who = source_hash_text
confirm_dialog = DialogLineBox (
urwid . Pile ( [
urwid . Text ( " " ) ,
urwid . Text ( " Block " + str ( who ) + " ? \n \n This blackholes the peer ' s identity in Reticulum, \n adds them to your ignored list, and deletes any \n conversation with them. \n " , align = urwid . CENTER ) ,
urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Yes, block " , on_press = confirm_block ) ) ,
( urwid . WEIGHT , 0.10 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " Cancel " , on_press = cancel_block ) ) ,
] ) ,
] ) , title = " Confirm block "
)
confirm_dialog . delegate = self
bottom = self . listbox
overlay = urwid . Overlay ( confirm_dialog , bottom , align = urwid . CENTER , width = urwid . RELATIVE_100 , valign = urwid . MIDDLE , height = urwid . PACK , left = 2 , right = 2 )
try :
self . columns_widget . contents [ 0 ] = (
overlay ,
self . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width ) ,
)
self . columns_widget . focus_position = 0
self . dialog_open = True
except Exception :
pass
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 :
2026-05-20 22:28:32 -04:00
self . dialog_open = False
return
source_hash_text = getattr ( item , " source_hash " , None )
if source_hash_text is None :
blocked = getattr ( item , " blocked_dest_hash " , None )
if isinstance ( blocked , ( bytes , bytearray ) ) :
source_hash_text = RNS . hexrep ( blocked , delimit = False )
if source_hash_text is None :
self . dialog_open = False
2023-02-09 13:45:37 +02:00
return
2026-05-20 22:28:32 -04:00
display_name = getattr ( item , " display_name " , None )
if display_name is None :
try :
display_name = self . app . directory . display_name ( bytes . fromhex ( source_hash_text ) )
except Exception :
display_name = None
if display_name is None :
2021-05-14 22:12:42 +02:00
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 )
2026-05-20 16:16:24 -04:00
e_copy = urwid . Edit ( caption = " Copy : " , edit_text = source_hash_text )
2021-05-13 16:39:31 +02:00
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
2026-05-20 16:16:24 -04:00
pinned_initial = False
notes_initial = " "
2021-05-14 14:45:20 +02:00
try :
2026-05-20 16:16:24 -04:00
existing_entry = self . app . directory . find ( bytes . fromhex ( source_hash_text ) )
if existing_entry :
2021-05-14 14:45:20 +02:00
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
2026-05-20 16:16:24 -04:00
pinned_initial = existing_entry . sort_rank is not None
notes_initial = getattr ( existing_entry , " notes " , " " ) or " "
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
2026-05-20 16:16:24 -04:00
e_notes = urwid . Edit ( caption = " Notes: " , edit_text = notes_initial , multiline = True )
cb_pin = urwid . CheckBox ( " Pin to top " , state = pinned_initial )
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 . dialog_open = False
2026-05-20 22:28:32 -04:00
self . update_conversation_list ( )
2021-05-13 16:39:31 +02:00
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
2026-05-20 16:16:24 -04:00
sort_rank = 0 if cb_pin . state else None
notes_value = e_notes . get_edit_text ( )
entry = DirectoryEntry ( source_hash , display_name , trust_level , preferred_delivery = delivery , sort_rank = sort_rank , notes = notes_value )
2021-05-13 16:39:31 +02:00
self . app . directory . remember ( entry )
2026-05-20 16:16:24 -04:00
self . _refresh_open_conversation_widget ( source_hash_text )
2021-05-13 16:39:31 +02:00
self . dialog_open = False
2026-05-20 22:28:32 -04:00
self . update_conversation_list ( )
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
2026-05-20 16:16:24 -04:00
action_status = urwid . Text ( " " , align = urwid . CENTER )
ping_button = urwid . Button ( " Ping " )
block_button = urwid . Button ( " Block " )
qr_button = urwid . Button ( " LXMF " )
urwid . connect_signal ( ping_button , " click " , lambda _b : self . _ping_peer_from_dialog ( source_hash_text , action_status , ping_button ) )
urwid . connect_signal ( block_button , " click " , lambda _b : self . _block_peer_from_dialog ( source_hash_text ) )
urwid . connect_signal ( qr_button , " click " , lambda _b : self . show_qr_dialog ( source_hash_text , title = display_name or source_hash_text ) )
actions_row = urwid . Columns ( [
( urwid . WEIGHT , 0.32 , ping_button ) ,
( urwid . WEIGHT , 0.02 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.32 , block_button ) ,
( urwid . WEIGHT , 0.02 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.32 , qr_button ) ,
] )
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 ,
2026-05-20 16:16:24 -04:00
e_copy ,
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 ,
2026-05-20 16:16:24 -04:00
urwid . Divider ( g [ " divider1 " ] ) ,
cb_pin ,
e_notes ,
2021-05-14 22:12:42 +02:00
known_section ,
2026-05-20 16:16:24 -04:00
actions_row ,
action_status ,
urwid . Divider ( g [ " divider1 " ] ) ,
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 ( )
2026-05-20 16:16:24 -04:00
if trust_level != DirectoryEntry . TRUSTED :
if self . list_filter != ConversationsDisplay . LIST_FILTER_UNTRUSTED :
self . _set_filter ( ConversationsDisplay . LIST_FILTER_UNTRUSTED )
2021-05-14 14:45:20 +02:00
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 ( )
2026-05-20 16:16:24 -04:00
def _decode_pn_app_data ( self , app_data ) :
try :
if not pn_announce_data_is_valid ( app_data ) :
return None
data = msgpack . unpackb ( app_data )
enabled = bool ( data [ 2 ] )
name = None
try :
meta = data [ 6 ]
if isinstance ( meta , dict ) and PN_META_NAME in meta :
raw = meta [ PN_META_NAME ]
if isinstance ( raw , ( bytes , bytearray ) ) :
name = raw . decode ( " utf-8 " , errors = " replace " )
except Exception :
name = None
return { " enabled " : enabled , " name " : name }
except Exception :
return None
def _pn_dropdown_label ( self , pn_hash , meta ) :
name = ( meta or { } ) . get ( " name " )
if name :
label = strip_modifiers ( name ) or " "
label = " " . join ( label . split ( ) )
else :
label = " "
if not label :
label = RNS . prettyhexrep ( pn_hash )
max_len = 40
if len ( label ) > max_len :
label = label [ : max_len - 1 ] + " … "
tail = " < " + RNS . hexrep ( pn_hash , delimit = False ) + " > "
if tail not in label :
label = label + " " + tail
if meta is None :
status = " [?] "
elif meta . get ( " enabled " ) :
status = " [E] "
else :
status = " [D] "
return status + " " + label
def _build_pn_options ( self ) :
options = [ ]
seen = set ( )
try :
pn_announces = list ( self . app . directory . _pn_announces )
except Exception :
pn_announces = [ ]
for tup in pn_announces :
if len ( tup ) < 3 : continue
pn_hash = tup [ 1 ]
app_data = tup [ 2 ]
if pn_hash in seen : continue
seen . add ( pn_hash )
meta = self . _decode_pn_app_data ( app_data )
options . append ( ( pn_hash , self . _pn_dropdown_label ( pn_hash , meta ) ) )
for extra in ( self . app . get_user_selected_propagation_node ( ) , self . app . get_default_propagation_node ( ) ) :
if extra is None or extra in seen :
continue
seen . add ( extra )
meta = None
try :
cached = RNS . Identity . recall_app_data ( extra )
if cached is not None :
meta = self . _decode_pn_app_data ( cached )
except Exception :
meta = None
options . append ( ( extra , self . _pn_dropdown_label ( extra , meta ) ) )
return options
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
2026-05-20 16:16:24 -04:00
2021-10-07 18:14:06 +02:00
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
2026-05-20 16:16:24 -04:00
current_default = self . app . get_default_propagation_node ( )
user_selected = self . app . get_user_selected_propagation_node ( )
pn_options = self . _build_pn_options ( )
2021-10-08 13:34:04 +02:00
2026-05-20 16:16:24 -04:00
selected_target = user_selected if user_selected is not None else current_default
2021-10-08 13:34:04 +02:00
2026-05-20 16:16:24 -04:00
def on_pn_picked ( picked_hash ) :
try :
self . app . set_user_selected_propagation_node ( picked_hash )
except Exception as e :
RNS . log ( " Could not update propagation node: " + str ( e ) , RNS . LOG_ERROR )
if pn_options :
node_picker = PropNodePicker ( pn_options , selected_target , on_pn_picked )
node_selector = urwid . Pile ( [
urwid . Text ( " Propagation node: " ) ,
node_picker ,
] )
else :
node_selector = None
pn_ident = None
if current_default is not None :
pn_ident = RNS . Identity . recall ( current_default )
if pn_ident is None :
2021-10-08 13:34:04 +02:00
RNS . log ( " Propagation node identity is unknown, requesting from network... " , RNS . LOG_DEBUG )
2026-05-20 16:16:24 -04:00
RNS . Transport . request_path ( current_default )
if pn_ident is not None or node_selector is not None :
header_str = " "
if pn_ident is not None :
node_hash = RNS . Destination . hash_from_name_and_identity ( " nomadnetwork.node " , pn_ident )
pn_entry = self . app . directory . find ( node_hash )
if pn_entry is not None and getattr ( pn_entry , " display_name " , None ) :
header_str = " " + strip_modifiers ( str ( pn_entry . display_name ) )
else :
header_str = " " + RNS . prettyhexrep ( current_default )
2022-10-25 12:52:02 +02:00
else :
2026-05-20 16:16:24 -04:00
header_str = " (no default) "
2021-10-08 11:58:24 +02:00
2026-05-20 16:16:24 -04:00
pile_items = [
urwid . Text ( " " + g [ " node " ] + header_str , align = urwid . CENTER ) ,
urwid . Divider ( g [ " divider1 " ] ) ,
sync_progress ,
urwid . Divider ( g [ " divider1 " ] ) ,
]
if node_selector is not None :
pile_items + = [ node_selector , urwid . Divider ( g [ " divider1 " ] ) ]
pile_items + = [ r_mall , rbs , urwid . Text ( " " ) , button_columns ]
dialog = DialogLineBox ( urwid . Pile ( pile_items ) , title = " Message Sync " )
2021-10-08 09:12:20 +02:00
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 )
2026-05-20 16:16:24 -04:00
if not self . dialog_open :
2021-10-07 18:14:06 +02:00
self . columns_widget . contents [ 0 ] = ( self . listbox , options )
2026-05-20 16:16:24 -04:00
elif self . sync_dialog is not None :
2021-10-07 18:14:06 +02:00
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-05-20 16:16:24 -04:00
# else: another dialog (peer info, new conversation, block confirm, etc.) is
# open as an overlay in contents[0]; leave it alone so an incoming message
# doesn't dismiss it. The underlying listbox is a persistent widget and was
# already refreshed by update_listbox() above.
2021-10-07 18:14:06 +02:00
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
2026-05-20 16:16:24 -04:00
options = self . columns_widget . options ( urwid . WEIGHT , 1 )
self . columns_widget . contents [ 1 ] = ( self . make_conversation_widget ( source_hash ) , options )
2021-06-30 14:49:26 +02:00
if source_hash == None :
2026-05-20 16:16:24 -04:00
self . columns_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 ( )
2026-05-20 16:16:24 -04:00
self . columns_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 :
2026-05-20 09:23:10 -04:00
widget = ConversationWidget ( source_hash , delegate = self )
2021-05-13 16:39:31 +02:00
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 ]
2026-05-20 16:16:24 -04:00
failed = conversation [ 6 ] if len ( conversation ) > 6 else 0
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 " ]
2026-05-21 02:10:10 +02:00
style = " list_normal "
2026-05-20 16:16:24 -04:00
focus_style = " list_focus "
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
2026-05-20 16:16:24 -04:00
is_pinned = False
try :
entry = self . app . directory . find ( bytes . fromhex ( source_hash ) )
is_pinned = entry is not None and entry . sort_rank is not None
except Exception :
is_pinned = False
head = symbol
if is_pinned :
head = g . get ( " pin " , " * " ) + " " + head
2021-09-05 20:38:10 +02:00
2021-05-14 22:12:42 +02:00
if display_name != None and display_name != " " :
2026-05-20 16:16:24 -04:00
head + = " " + display_name
2021-05-13 16:39:31 +02:00
if trust_level != DirectoryEntry . TRUSTED :
2026-05-20 16:16:24 -04:00
head + = " < " + source_hash + " > "
2026-03-28 18:19:11 -04:00
2026-05-20 16:16:24 -04:00
markup = [ head ]
if failed and source_hash != self . currently_displayed_conversation :
badge_text = " " + g [ " warning " ] + " ( " + str ( failed ) + " ) "
2026-05-21 02:10:10 +02:00
# markup.append(("msg_notice_caution", badge_text))
markup . append ( badge_text )
2026-05-20 16:16:24 -04:00
elif unread and source_hash != self . currently_displayed_conversation :
badge_text = " " + g [ " unread " ] + " ( " + str ( unread ) + " ) "
2026-05-21 02:10:10 +02:00
# Good idea with having the badges here colored, but
# using the bg color for it is a bit much, I think.
# I set fg color attrmap styles, but that messes up
# the bg on list focus. If there's a way to handle
# that, we can re-enable this.
# markup.append(("msg_notice_unread", badge_text))
markup . append ( badge_text )
if trust_level == DirectoryEntry . TRUSTED and unread and source_hash != self . currently_displayed_conversation :
style = " msg_notice_unread "
2021-09-05 20:38:10 +02:00
2026-03-28 18:19:11 -04:00
if last_activity > 0 :
2026-05-20 16:16:24 -04:00
markup . append ( " \n " + relative_time ( last_activity ) )
2021-05-13 16:39:31 +02:00
2026-05-20 16:16:24 -04:00
widget = ListEntry ( markup )
2021-05-13 16:39:31 +02:00
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 ) :
2026-05-20 16:16:24 -04:00
try :
focus_path = self . columns_widget . get_focus_path ( )
except Exception :
return self . list_shortcuts
if not focus_path :
return self . list_shortcuts
2021-05-13 16:39:31 +02:00
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 ) :
2026-05-20 16:16:24 -04:00
if self . focus_position == " header " :
result = super ( ConversationFrame , self ) . keypress ( size , key )
if result == " up " :
nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . main_display . frame . focus_position = " header "
return None
if result == " down " :
self . focus_position = " body "
return None
return result
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 :
2026-05-20 16:16:24 -04:00
if getattr ( self . delegate , " has_visible_trust_banner " , lambda : False ) ( ) :
try :
self . delegate . _header_pile . focus_position = 1
self . focus_position = " header "
return None
except Exception :
pass
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 ) :
2026-05-20 09:23:10 -04:00
def __init__ ( self , source_hash , delegate ) :
2021-07-02 13:35:10 +02:00
self . app = nomadnet . NomadNetworkApp . get_shared_instance ( )
g = self . app . ui . glyphs
2026-05-20 09:23:10 -04:00
self . delegate = delegate
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
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 ( )
2026-05-20 16:16:24 -04:00
self . _trust_banner_dismissed = False
self . _header_pile = urwid . Pile ( [ self . peer_info_widget ] )
self . _refresh_trust_banner ( )
header = self . _header_pile
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-05-20 16:16:24 -04:00
def has_visible_trust_banner ( self ) :
if self . _trust_banner_dismissed :
return False
try :
tl = self . app . directory . trust_level ( bytes . fromhex ( self . source_hash ) )
except Exception :
tl = DirectoryEntry . UNKNOWN
return tl != DirectoryEntry . TRUSTED
def _refresh_trust_banner ( self ) :
contents = [ ( self . peer_info_widget , self . _header_pile . options ( ) ) ]
if self . has_visible_trust_banner ( ) :
banner = self . _build_trust_banner ( )
contents . append ( ( banner , self . _header_pile . options ( ) ) )
self . _header_pile . contents = contents
if len ( contents ) > 1 :
try : self . _header_pile . focus_position = 1
except Exception : pass
def _build_trust_banner ( self ) :
g = self . app . ui . glyphs
msg = urwid . Text ( " " + g [ " warning " ] + " This peer isn ' t trusted yet. " )
btn_trust = urwid . Button ( " Trust " , on_press = self . _on_trust_click )
btn_block = urwid . Button ( " Block " , on_press = self . _on_block_click )
btn_nothing = urwid . Button ( " Do nothing " , on_press = self . _on_ignore_click )
row = urwid . Columns ( [
( ' weight ' , 1 , msg ) ,
( ' pack ' , btn_trust ) ,
( 1 , urwid . Text ( " " ) ) ,
( ' pack ' , btn_block ) ,
( 1 , urwid . Text ( " " ) ) ,
( ' pack ' , btn_nothing ) ,
( 1 , urwid . Text ( " " ) ) ,
] , dividechars = 0 )
return urwid . AttrMap ( row , " msg_warning_untrusted " )
def _on_trust_click ( self , _btn ) :
try :
src = bytes . fromhex ( self . source_hash )
existing = self . app . directory . find ( src )
display_name = getattr ( existing , " display_name " , None ) if existing is not None else None
preferred = getattr ( existing , " preferred_delivery " , None ) if existing is not None else None
entry = DirectoryEntry ( src , display_name , DirectoryEntry . TRUSTED , preferred_delivery = preferred )
self . app . directory . remember ( entry )
except Exception as e :
RNS . log ( " Could not mark peer as trusted: " + str ( e ) , RNS . LOG_ERROR )
self . _refresh_trust_banner ( )
try :
self . frame . focus_position = " footer "
except Exception :
pass
try :
if self . delegate . list_filter != ConversationsDisplay . LIST_FILTER_TRUSTED :
self . delegate . _set_filter ( ConversationsDisplay . LIST_FILTER_TRUSTED )
else :
self . delegate . update_conversation_list ( )
except Exception as e :
RNS . log ( " Trust UI refresh failed: " + str ( e ) , RNS . LOG_ERROR )
def _on_ignore_click ( self , _btn ) :
self . _trust_banner_dismissed = True
self . _refresh_trust_banner ( )
def _on_block_click ( self , _btn ) :
def dismiss ( _b ) :
self . dialog_active = False
try : self . delegate . dialog_open = False
except Exception : pass
try : self . delegate . update_conversation_list ( )
except Exception : pass
def confirmed ( _b ) :
self . dialog_active = False
try : self . delegate . dialog_open = False
except Exception : pass
try :
self . _block_peer ( )
except Exception as e :
RNS . log ( " Block failed: " + str ( e ) , RNS . LOG_ERROR )
try : self . delegate . update_conversation_list ( )
except Exception : pass
try :
who = self . app . directory . simplest_display_str ( bytes . fromhex ( self . source_hash ) )
except Exception :
who = self . source_hash
dialog = DialogLineBox (
urwid . Pile ( [
urwid . Text ( " " ) ,
urwid . Text ( " Block " + str ( who ) + " ? \n \n This will blackhole the peer ' s identity in Reticulum, \n add them to your ignored list, and delete this conversation. \n " , align = urwid . CENTER ) ,
urwid . Columns ( [
( urwid . WEIGHT , 0.45 , urwid . Button ( " Yes, block " , on_press = confirmed ) ) ,
( urwid . WEIGHT , 0.10 , urwid . Text ( " " ) ) ,
( urwid . WEIGHT , 0.45 , urwid . Button ( " Cancel " , on_press = dismiss ) ) ,
] ) ,
] ) , title = " Confirm block "
)
dialog . delegate = self . delegate
bottom = self . delegate . listbox
overlay = urwid . Overlay ( dialog , bottom , align = urwid . CENTER , width = urwid . RELATIVE_100 , valign = urwid . MIDDLE , height = urwid . PACK , left = 2 , right = 2 )
try :
self . delegate . columns_widget . contents [ 0 ] = (
overlay ,
self . delegate . columns_widget . options ( urwid . GIVEN , ConversationsDisplay . given_list_width ) ,
)
self . delegate . columns_widget . focus_position = 0
self . dialog_active = True
try : self . delegate . dialog_open = True
except Exception : pass
except Exception :
pass
def _block_peer ( self ) :
try :
src = bytes . fromhex ( self . source_hash )
except Exception :
return
try :
self . app . block_destination ( src , reason = " user-blocked from nomadnet conversation " )
except Exception as e :
RNS . log ( " Block failed: " + str ( e ) , RNS . LOG_ERROR )
try :
self . delegate . delete_conversation ( self . source_hash )
nomadnet . Conversation . delete_conversation ( self . source_hash , self . app )
except Exception as e :
RNS . log ( " Could not delete blocked conversation: " + str ( e ) , RNS . LOG_ERROR )
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 ) :
2026-05-20 09:23:10 -04:00
self . delegate . _wake ( lambda : self . conversation_changed ( conversation ) )
2026-05-17 02:02:27 +02:00
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 ) :
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
2026-05-20 09:23:10 -04:00
try :
message_widget = LXMessageWidget ( message , theme = self . app . config [ " textui " ] [ " theme " ] )
except Exception as e :
2026-05-20 19:45:15 +02:00
RNS . log ( " Skipping message loading for " + str ( message . file_path ) + " due to error: " + str ( e ) , RNS . LOG_WARNING )
2026-05-20 09:23:10 -04:00
message . unload ( )
continue
2021-05-13 16:39:31 +02:00
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
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 }
2026-05-19 23:20:19 +02:00
if self . app . compose_markdown :
if not fields : fields = { }
fields [ LXMF . FIELD_RENDERER ] = LXMF . RENDERER_MARKDOWN
2026-03-28 18:19:11 -04:00
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 ) :
2026-05-19 23:20:19 +02:00
mdc = MarkdownToMicron ( max_width = 80 , syntax_highlighter = SyntaxHighlighter ( ) , url_scope = None )
def __init__ ( self , message , theme = THEME_DARK ) :
2021-05-13 16:39:31 +02:00
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 )
2026-05-19 23:20:19 +02:00
renderer = message . content_renderer ( )
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-05-20 16:16:24 -04:00
inbound_untrusted = False
if not is_outbound and msg_source_hash is not None :
try :
sender_trust = app . directory . trust_level ( msg_source_hash )
if sender_trust in ( DirectoryEntry . UNTRUSTED , DirectoryEntry . WARNING , DirectoryEntry . UNKNOWN ) :
inbound_untrusted = True
except Exception :
inbound_untrusted = False
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 " )
2026-05-19 23:20:19 +02:00
markdown = renderer == LXMF . RENDERER_MARKDOWN
default_fg = " bbb " if theme == THEME_DARK else " 444 "
if markdown :
formatted = self . mdc . format_block ( content_text )
message_body = strip_non_formatting_tags ( formatted )
rendered = markup_to_attrmaps ( strip_modifiers ( message_body ) , url_delegate = None , fg_color = default_fg , bg_color = None )
content_pile = urwid . Padding ( urwid . Pile ( rendered ) , left = 2 , right = 2 )
else : indented = " \n " . join ( " " + line for line in content_lines )
2026-03-28 18:19:11 -04:00
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 ( )
2026-05-19 23:20:19 +02:00
if markdown : pile_widgets . append ( content_pile )
else : pile_widgets . append ( urwid . Text ( indented ) )
2026-03-28 18:19:11 -04:00
if has_attachments and cached_names :
2026-05-20 16:16:24 -04:00
if inbound_untrusted :
pile_widgets . append ( urwid . AttrMap (
urwid . Text ( " " + g [ " warning " ] + " This attachment came from a peer that ' s untrusted. Be careful when opening it. " ) ,
" list_untrusted " ,
) )
2026-03-28 18:19:11 -04:00
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