mirror of
https://github.com/markqvist/NomadNet.git
synced 2026-10-06 14:37:34 +00:00
Merge dev branch into master
This commit is contained in:
@@ -70,6 +70,24 @@ class Conversation:
|
||||
|
||||
ingested_path = lxmessage.write_to_directory(conversation_path)
|
||||
|
||||
if not originator and ingested_path is not None:
|
||||
try:
|
||||
rssi = getattr(lxmessage, "rssi", None)
|
||||
snr = getattr(lxmessage, "snr", None)
|
||||
q = getattr(lxmessage, "q", None)
|
||||
if rssi is not None or snr is not None or q is not None:
|
||||
index = ConversationMessage.read_index(conversation_path)
|
||||
filename = os.path.basename(ingested_path)
|
||||
entry = index.get(filename) or {}
|
||||
if rssi is not None: entry["rssi"] = rssi
|
||||
if snr is not None: entry["snr"] = snr
|
||||
if q is not None: entry["q"] = q
|
||||
index[filename] = entry
|
||||
with open(os.path.join(conversation_path, ".index"), "wb") as index_file:
|
||||
index_file.write(msgpack.packb(index))
|
||||
except Exception as e:
|
||||
RNS.log("Error while storing reception stats for message: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
try:
|
||||
ConversationMessage.extract_attachments_from_lxm(lxmessage, app)
|
||||
except Exception as e:
|
||||
@@ -293,14 +311,26 @@ class Conversation:
|
||||
def register_changed_callback(self, callback):
|
||||
self.__changed_callback = callback
|
||||
|
||||
def ensure_send_destination(self):
|
||||
if self.send_destination is None:
|
||||
self.source_identity = RNS.Identity.recall(bytes.fromhex(self.source_hash))
|
||||
if self.source_identity:
|
||||
self.source_known = True
|
||||
self.send_destination = RNS.Destination(self.source_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
|
||||
return self.send_destination
|
||||
|
||||
def send(self, content="", title="", fields=None):
|
||||
self.ensure_send_destination()
|
||||
if self.send_destination:
|
||||
dest = self.send_destination
|
||||
source = self.app.lxmf_destination
|
||||
desired_method = LXMF.LXMessage.DIRECT
|
||||
if self.app.directory.preferred_delivery(dest.hash) == DirectoryEntry.PROPAGATED:
|
||||
preferred = self.app.directory.preferred_delivery(dest.hash)
|
||||
if preferred == DirectoryEntry.PROPAGATED:
|
||||
if self.app.message_router.get_outbound_propagation_node() != None:
|
||||
desired_method = LXMF.LXMessage.PROPAGATED
|
||||
elif preferred == DirectoryEntry.OPPORTUNISTIC:
|
||||
desired_method = LXMF.LXMessage.OPPORTUNISTIC
|
||||
else:
|
||||
if not self.app.message_router.delivery_link_available(dest.hash) and RNS.Identity.current_ratchet_id(dest.hash) != None:
|
||||
RNS.log(f"Have ratchet for {RNS.prettyhexrep(dest.hash)}, requesting opportunistic delivery of message", RNS.LOG_DEBUG)
|
||||
@@ -328,6 +358,7 @@ class Conversation:
|
||||
return False
|
||||
|
||||
def paper_output(self, content="", title="", mode="print_qr"):
|
||||
self.ensure_send_destination()
|
||||
if self.send_destination:
|
||||
try:
|
||||
dest = self.send_destination
|
||||
@@ -433,6 +464,9 @@ class ConversationMessage:
|
||||
self._cached_method = None
|
||||
self._cached_has_attachments = None
|
||||
self._cached_attachment_names = None
|
||||
self._cached_rssi = None
|
||||
self._cached_snr = None
|
||||
self._cached_q = None
|
||||
|
||||
self.sort_timestamp = os.path.getmtime(file_path) if os.path.isfile(file_path) else 0
|
||||
|
||||
@@ -476,6 +510,12 @@ class ConversationMessage:
|
||||
if self._cached_signature_validated is None:
|
||||
self._cached_signature_validated = self.lxm.signature_validated
|
||||
self._cached_method = self.lxm.method
|
||||
if self._cached_rssi is None:
|
||||
self._cached_rssi = getattr(self.lxm, "rssi", None)
|
||||
if self._cached_snr is None:
|
||||
self._cached_snr = getattr(self.lxm, "snr", None)
|
||||
if self._cached_q is None:
|
||||
self._cached_q = getattr(self.lxm, "q", None)
|
||||
if hasattr(self.lxm, "get_fields"):
|
||||
_fields = self.lxm.get_fields()
|
||||
if _fields and isinstance(_fields, dict) and LXMF.FIELD_RENDERER in _fields:
|
||||
@@ -596,6 +636,22 @@ class ConversationMessage:
|
||||
self.load()
|
||||
return self._cached_state
|
||||
|
||||
def get_method(self):
|
||||
if self._cached_method is not None:
|
||||
return self._cached_method
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._cached_method
|
||||
|
||||
def get_rssi(self):
|
||||
return self._cached_rssi
|
||||
|
||||
def get_snr(self):
|
||||
return self._cached_snr
|
||||
|
||||
def get_q(self):
|
||||
return self._cached_q
|
||||
|
||||
def get_transport_encryption(self):
|
||||
if self._cached_transport_encryption is not None:
|
||||
return self._cached_transport_encryption
|
||||
@@ -957,6 +1013,9 @@ class ConversationMessage:
|
||||
"renderer": None if self._cached_renderer is ConversationMessage._RENDERER_UNSET else self._cached_renderer,
|
||||
"has_attachments": self._cached_has_attachments,
|
||||
"attachment_names": self._cached_attachment_names,
|
||||
"rssi": self._cached_rssi,
|
||||
"snr": self._cached_snr,
|
||||
"q": self._cached_q,
|
||||
}
|
||||
|
||||
def restore_from_index(self, entry):
|
||||
@@ -974,6 +1033,9 @@ class ConversationMessage:
|
||||
self._cached_renderer = entry.get("renderer", ConversationMessage._RENDERER_UNSET)
|
||||
self._cached_has_attachments = entry.get("has_attachments")
|
||||
self._cached_attachment_names = entry.get("attachment_names")
|
||||
self._cached_rssi = entry.get("rssi")
|
||||
self._cached_snr = entry.get("snr")
|
||||
self._cached_q = entry.get("q")
|
||||
|
||||
@staticmethod
|
||||
def read_index(conversation_path):
|
||||
|
||||
@@ -412,8 +412,9 @@ class DirectoryEntry:
|
||||
UNKNOWN = 0x02
|
||||
TRUSTED = 0xFF
|
||||
|
||||
DIRECT = 0x01
|
||||
PROPAGATED = 0x02
|
||||
DIRECT = 0x01
|
||||
PROPAGATED = 0x02
|
||||
OPPORTUNISTIC = 0x03
|
||||
|
||||
def __init__(self, source_hash, display_name=None, trust_level=UNKNOWN, hosts_node=False, preferred_delivery=None, identify_on_connect=False, sort_rank=None, notes=None):
|
||||
if len(source_hash) == RNS.Identity.TRUNCATED_HASHLENGTH//8:
|
||||
|
||||
@@ -165,6 +165,7 @@ CONFIG_SECTIONS = [
|
||||
"type": "edit",
|
||||
"default": "None",
|
||||
"placeholder": "None",
|
||||
"validation": ["stamp_cost"],
|
||||
"help": "Required inbound stamp cost 1-255, or None to disable",
|
||||
},
|
||||
{
|
||||
@@ -571,8 +572,9 @@ CONFIG_SECTIONS = [
|
||||
{
|
||||
"config_key": "print_from",
|
||||
"label": "Print from: ",
|
||||
"type": "edit",
|
||||
"placeholder": "everywhere, trusted, or hashes",
|
||||
"type": "list",
|
||||
"default": [],
|
||||
"placeholder": "everywhere, trusted, or hash",
|
||||
"help": "Which senders may trigger automatic printing",
|
||||
},
|
||||
{
|
||||
@@ -704,8 +706,7 @@ class ConfigDisplayShortcuts():
|
||||
class ConfigFiller(urwid.WidgetWrap):
|
||||
def __init__(self, widget, app):
|
||||
self.app = app
|
||||
self.filler = urwid.Filler(widget, urwid.TOP)
|
||||
super().__init__(self.filler)
|
||||
super().__init__(widget)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "ctrl w":
|
||||
@@ -963,7 +964,10 @@ class ConfigSectionView(urwid.WidgetWrap):
|
||||
if value:
|
||||
widget.set_value(value if isinstance(value, list) else [value])
|
||||
else:
|
||||
widget.edit_text = str(value)
|
||||
if isinstance(value, (list, tuple)):
|
||||
widget.edit_text = ", ".join(str(item) for item in value)
|
||||
else:
|
||||
widget.edit_text = str(value)
|
||||
|
||||
def validate_all(self):
|
||||
all_valid = True
|
||||
@@ -974,8 +978,14 @@ class ConfigSectionView(urwid.WidgetWrap):
|
||||
|
||||
def on_save(self, button):
|
||||
if not self.validate_all():
|
||||
self.show_message("Some fields have invalid values.\nCorrect the marked fields and try again.", title="Not saved", back_to_list=False)
|
||||
return
|
||||
|
||||
try:
|
||||
self.file_config.reload()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.section_key not in self.file_config:
|
||||
self.file_config[self.section_key] = {}
|
||||
|
||||
@@ -1004,10 +1014,15 @@ class ConfigSectionView(urwid.WidgetWrap):
|
||||
|
||||
try:
|
||||
self.file_config.write()
|
||||
if self.section.get("configfile") == "reticulum":
|
||||
try:
|
||||
self.parent.app.rns.config.reload()
|
||||
except Exception:
|
||||
pass
|
||||
self.parent.mark_restart_pending()
|
||||
self.show_message(f"{self.section['name']} configuration saved.\nRestart Nomad Network for changes to take effect.")
|
||||
except Exception as e:
|
||||
self.show_message(f"Error saving configuration: {str(e)}", title="Error")
|
||||
self.show_message(f"Error saving configuration: {str(e)}", title="Error", back_to_list=False)
|
||||
|
||||
def on_reset(self, button=None):
|
||||
self._confirm(
|
||||
@@ -1081,9 +1096,12 @@ class ConfigSectionView(urwid.WidgetWrap):
|
||||
self.parent.widget = self
|
||||
self.parent.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
def show_message(self, message, title="Notice"):
|
||||
def dismiss_dialog(button):
|
||||
self.parent.switch_to_list()
|
||||
def show_message(self, message, title="Notice", back_to_list=True):
|
||||
def dismiss_dialog(button=None):
|
||||
if back_to_list:
|
||||
self.parent.switch_to_list()
|
||||
else:
|
||||
self._dismiss_overlay()
|
||||
|
||||
dialog = DialogLineBox(
|
||||
urwid.Pile([
|
||||
@@ -1091,22 +1109,11 @@ class ConfigSectionView(urwid.WidgetWrap):
|
||||
urwid.Divider(),
|
||||
urwid.Button("OK", on_press=dismiss_dialog)
|
||||
]),
|
||||
parent=self,
|
||||
title=title
|
||||
)
|
||||
|
||||
overlay = urwid.Overlay(
|
||||
dialog,
|
||||
self.parent.list_view,
|
||||
align=urwid.CENTER,
|
||||
width=60,
|
||||
valign=urwid.MIDDLE,
|
||||
height=10,
|
||||
min_width=1,
|
||||
min_height=1
|
||||
)
|
||||
|
||||
self.parent.widget = overlay
|
||||
self.parent.app.ui.main_display.update_active_sub_display()
|
||||
self._show_overlay(dialog, height=10, width=60)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "ctrl s":
|
||||
@@ -1153,8 +1160,6 @@ class ConfigDisplay():
|
||||
self.widget = self.list_view
|
||||
|
||||
def _build_list_view(self):
|
||||
self.terminal_cols, self.terminal_rows = _get_cols_rows()
|
||||
|
||||
self.section_items = []
|
||||
for section in CONFIG_SECTIONS:
|
||||
icon = _get_section_icon(self.glyphset, section["glyph"])
|
||||
@@ -1169,12 +1174,9 @@ class ConfigDisplay():
|
||||
self.list_walker = urwid.SimpleFocusListWalker(list_contents)
|
||||
self.list_box = urwid.ListBox(self.list_walker)
|
||||
|
||||
self.list_rows = max(5, self.terminal_rows - 7)
|
||||
self.box_adapter = urwid.BoxAdapter(self.list_box, self.list_rows)
|
||||
|
||||
self.header_text = urwid.Text("")
|
||||
self.list_divider = urwid.Divider("─")
|
||||
self.list_pile = urwid.Pile([self.box_adapter])
|
||||
self.list_pile = urwid.Pile([('weight', 1, self.list_box)])
|
||||
self._update_header()
|
||||
|
||||
self.list_view = ConfigFiller(self.list_pile, self.app)
|
||||
@@ -1185,7 +1187,7 @@ class ConfigDisplay():
|
||||
self.header_text.set_text(("warning_text", "Changes saved. Restart NomadNet for them to take effect"))
|
||||
contents.append((self.header_text, self.list_pile.options('pack')))
|
||||
contents.append((self.list_divider, self.list_pile.options('pack')))
|
||||
contents.append((self.box_adapter, self.list_pile.options('weight', 1)))
|
||||
contents.append((self.list_box, self.list_pile.options('weight', 1)))
|
||||
self.list_pile.contents = contents
|
||||
|
||||
def mark_restart_pending(self):
|
||||
@@ -1200,6 +1202,7 @@ class ConfigDisplay():
|
||||
|
||||
def switch_to_list(self):
|
||||
self.shortcuts_display.reset_shortcuts()
|
||||
self.editor_term = None
|
||||
self._update_header()
|
||||
self.widget = self.list_view
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
@@ -72,13 +72,13 @@ class ConversationDisplayShortcuts():
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-p] Paper Msg [C-t] Title [C-f] Attach [C-s] Save [Tab] ↑ Messages"), "shortcutbar")
|
||||
self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-p] Paper Msg [C-t] Title [C-f] Attach [C-v] Delivery [C-r] Details [C-s] Save [Tab] ↑ Messages"), "shortcutbar")
|
||||
|
||||
class ConversationBodyShortcuts():
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
self.widget = urwid.AttrMap(urwid.Text("[C-s] Save [C-u] Purge [C-o] Sort [C-x] Clear History [C-g] Fullscreen [C-w] Close [Tab] ↓ Editor"), "shortcutbar")
|
||||
self.widget = urwid.AttrMap(urwid.Text("[C-r] Details [C-v] Delivery [C-o] Sort [C-s] Save [C-u] Purge [C-k] Cancel Send [C-x] Clear History [C-g] Fullscreen [C-w] Close [Tab] ↓ Editor"), "shortcutbar")
|
||||
|
||||
class TabButton(urwid.Button):
|
||||
button_left = urwid.Text("[")
|
||||
@@ -562,9 +562,10 @@ class ConversationsDisplay():
|
||||
def delete_selected_conversation(self):
|
||||
self.dialog_open = True
|
||||
item = self.ilb.get_selected_item()
|
||||
if item == None:
|
||||
source_hash = getattr(item, "source_hash", None) if item is not None else None
|
||||
if source_hash is None:
|
||||
self.dialog_open = False
|
||||
return
|
||||
source_hash = item.source_hash
|
||||
|
||||
def dismiss_dialog(sender):
|
||||
self.dialog_open = False
|
||||
@@ -839,8 +840,9 @@ class ConversationsDisplay():
|
||||
unknown_selected = True
|
||||
trusted_selected = False
|
||||
|
||||
direct_selected = True
|
||||
propagated_selected = False
|
||||
direct_selected = True
|
||||
propagated_selected = False
|
||||
opportunistic_selected = False
|
||||
|
||||
pinned_initial = False
|
||||
notes_initial = ""
|
||||
@@ -862,9 +864,13 @@ class ConversationsDisplay():
|
||||
unknown_selected = False
|
||||
trusted_selected = True
|
||||
|
||||
if self.app.directory.preferred_delivery(bytes.fromhex(source_hash_text)) == DirectoryEntry.PROPAGATED:
|
||||
preferred = self.app.directory.preferred_delivery(bytes.fromhex(source_hash_text))
|
||||
if preferred == DirectoryEntry.PROPAGATED:
|
||||
direct_selected = False
|
||||
propagated_selected = True
|
||||
elif preferred == DirectoryEntry.OPPORTUNISTIC:
|
||||
direct_selected = False
|
||||
opportunistic_selected = True
|
||||
|
||||
pinned_initial = existing_entry.sort_rank is not None
|
||||
notes_initial = getattr(existing_entry, "notes", "") or ""
|
||||
@@ -881,8 +887,9 @@ class ConversationsDisplay():
|
||||
r_trusted = urwid.RadioButton(trust_button_group, "Trusted", state=trusted_selected)
|
||||
|
||||
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)
|
||||
r_direct = urwid.RadioButton(method_button_group, "Deliver over direct link", state=direct_selected)
|
||||
r_opportunistic = urwid.RadioButton(method_button_group, "Deliver opportunistically", state=opportunistic_selected)
|
||||
r_propagated = urwid.RadioButton(method_button_group, "Use propagation nodes", state=propagated_selected)
|
||||
|
||||
def dismiss_dialog(sender):
|
||||
self.dialog_open = False
|
||||
@@ -901,6 +908,8 @@ class ConversationsDisplay():
|
||||
delivery = DirectoryEntry.DIRECT
|
||||
if r_propagated.state == True:
|
||||
delivery = DirectoryEntry.PROPAGATED
|
||||
elif r_opportunistic.state == True:
|
||||
delivery = DirectoryEntry.OPPORTUNISTIC
|
||||
|
||||
sort_rank = 0 if cb_pin.state else None
|
||||
notes_value = e_notes.get_edit_text()
|
||||
@@ -972,6 +981,7 @@ class ConversationsDisplay():
|
||||
r_trusted,
|
||||
urwid.Divider(g["divider1"]),
|
||||
r_direct,
|
||||
r_opportunistic,
|
||||
r_propagated,
|
||||
urwid.Divider(g["divider1"]),
|
||||
cb_pin,
|
||||
@@ -1037,7 +1047,7 @@ class ConversationsDisplay():
|
||||
elif r_trusted.state == True:
|
||||
trust_level = DirectoryEntry.TRUSTED
|
||||
|
||||
if not source_hash in [c[0] for c in existing_conversations]:
|
||||
if not source_hash_text in [c[0] for c in existing_conversations]:
|
||||
entry = DirectoryEntry(source_hash, display_name, trust_level)
|
||||
self.app.directory.remember(entry)
|
||||
|
||||
@@ -1048,7 +1058,7 @@ class ConversationsDisplay():
|
||||
if trust_level != DirectoryEntry.TRUSTED:
|
||||
if self.list_filter != ConversationsDisplay.LIST_FILTER_UNTRUSTED:
|
||||
self._set_filter(ConversationsDisplay.LIST_FILTER_UNTRUSTED)
|
||||
self.display_conversation(source_hash_text)
|
||||
self.display_conversation(None, source_hash_text)
|
||||
self.dialog_open = False
|
||||
self.update_conversation_list()
|
||||
|
||||
@@ -1595,7 +1605,7 @@ class ConversationsDisplay():
|
||||
|
||||
if selected_hash is not None:
|
||||
for idx, widget in enumerate(self.list_widgets):
|
||||
if widget.source_hash == selected_hash:
|
||||
if getattr(widget, "source_hash", None) == selected_hash:
|
||||
self.ilb.select_item(idx)
|
||||
break
|
||||
nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.draw_screen()
|
||||
@@ -1632,7 +1642,7 @@ class ConversationsDisplay():
|
||||
conversation_position = None
|
||||
index = 0
|
||||
for widget in self.list_widgets:
|
||||
if widget.source_hash == source_hash:
|
||||
if getattr(widget, "source_hash", None) == source_hash:
|
||||
conversation_position = index
|
||||
index += 1
|
||||
|
||||
@@ -1790,6 +1800,24 @@ class ListEntry(urwid.Text):
|
||||
self._emit('click')
|
||||
return True
|
||||
|
||||
LXM_STATE_NAMES = {
|
||||
LXMF.LXMessage.GENERATING: "Generating",
|
||||
LXMF.LXMessage.OUTBOUND: "Outbound",
|
||||
LXMF.LXMessage.SENDING: "Sending",
|
||||
LXMF.LXMessage.SENT: "Sent",
|
||||
LXMF.LXMessage.DELIVERED: "Delivered",
|
||||
LXMF.LXMessage.REJECTED: "Rejected",
|
||||
LXMF.LXMessage.CANCELLED: "Cancelled",
|
||||
LXMF.LXMessage.FAILED: "Failed",
|
||||
}
|
||||
|
||||
LXM_METHOD_NAMES = {
|
||||
LXMF.LXMessage.OPPORTUNISTIC: "Opportunistic",
|
||||
LXMF.LXMessage.DIRECT: "Direct link",
|
||||
LXMF.LXMessage.PROPAGATED: "Propagation node",
|
||||
LXMF.LXMessage.PAPER: "Paper",
|
||||
}
|
||||
|
||||
class MessageEdit(ReadlineMixin, urwid.Edit):
|
||||
def keypress(self, size, key):
|
||||
if key == "ctrl d":
|
||||
@@ -1800,6 +1828,13 @@ class MessageEdit(ReadlineMixin, urwid.Edit):
|
||||
self.delegate.attach_file()
|
||||
elif key == "ctrl s":
|
||||
self.delegate.save_focused_attachments()
|
||||
elif key == "ctrl v":
|
||||
self.delegate.cycle_delivery_method()
|
||||
elif key == "ctrl r":
|
||||
self.delegate.toggle_message_details()
|
||||
elif key == "ctrl k":
|
||||
if not self.delegate.cancel_pending_send():
|
||||
return super(MessageEdit, self).keypress(size, key)
|
||||
elif key == "up":
|
||||
y = self.get_cursor_coords(size)[1]
|
||||
if y == 0:
|
||||
@@ -1858,6 +1893,8 @@ class ConversationFrame(urwid.Frame):
|
||||
return super(ConversationFrame, self).keypress(size, key)
|
||||
|
||||
class ConversationWidget(urwid.WidgetWrap):
|
||||
MAX_ATTACHMENT_SIZE = 100 * 1024 * 1024
|
||||
|
||||
def __init__(self, source_hash, delegate):
|
||||
self.app = nomadnet.NomadNetworkApp.get_shared_instance()
|
||||
g = self.app.ui.glyphs
|
||||
@@ -1876,6 +1913,7 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
self.sort_by_timestamp = False
|
||||
self.pending_attachments = []
|
||||
self.dialog_active = False
|
||||
self.show_message_details = False
|
||||
|
||||
self.update_message_widgets()
|
||||
|
||||
@@ -2096,9 +2134,18 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
else:
|
||||
hops_str = str(hops)+" hop" + ("s" if hops != 1 else "")
|
||||
|
||||
preferred = self.app.directory.preferred_delivery(source_hash_bytes)
|
||||
if preferred == DirectoryEntry.PROPAGATED:
|
||||
via_str = "Propagation"
|
||||
elif preferred == DirectoryEntry.OPPORTUNISTIC:
|
||||
via_str = "Opportunistic"
|
||||
else:
|
||||
via_str = "Link"
|
||||
|
||||
right_parts = []
|
||||
if stamp_cost is not None:
|
||||
right_parts.append("Stamp: "+str(stamp_cost))
|
||||
right_parts.append("Via: "+via_str)
|
||||
right_parts.append(g["speed"]+hops_str)
|
||||
|
||||
left = " "+display_name
|
||||
@@ -2174,13 +2221,14 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
if self.frame:
|
||||
allowed = nomadnet.NomadNetworkApp.get_shared_instance().directory.is_known(bytes.fromhex(self.source_hash))
|
||||
if allowed:
|
||||
self.conversation.ensure_send_destination()
|
||||
self.frame.contents["footer"] = (self._build_footer(), None)
|
||||
else:
|
||||
warning = urwid.AttrMap(
|
||||
urwid.Padding(urwid.Text(
|
||||
"\n"+g["info"]+"\n\nYou cannot currently message this peer, since its identity keys are not known. "
|
||||
"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"
|
||||
"The message editor will appear here as soon as the keys arrive.\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,
|
||||
@@ -2188,6 +2236,34 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
"msg_header_caution",
|
||||
)
|
||||
self.frame.contents["footer"] = (warning, None)
|
||||
self._schedule_editor_recheck()
|
||||
|
||||
def _schedule_editor_recheck(self):
|
||||
if getattr(self, "_editor_recheck_pending", False):
|
||||
return
|
||||
try:
|
||||
loop = nomadnet.NomadNetworkApp.get_shared_instance().ui.loop
|
||||
if loop is None:
|
||||
return
|
||||
self._editor_recheck_pending = True
|
||||
loop.set_alarm_in(1.0, self._editor_recheck)
|
||||
except Exception:
|
||||
self._editor_recheck_pending = False
|
||||
|
||||
def _editor_recheck(self, loop=None, user_data=None):
|
||||
self._editor_recheck_pending = False
|
||||
if not self.frame:
|
||||
return
|
||||
if getattr(self.delegate, "currently_displayed_conversation", None) != self.source_hash:
|
||||
return
|
||||
if nomadnet.NomadNetworkApp.get_shared_instance().directory.is_known(bytes.fromhex(self.source_hash)):
|
||||
self.check_editor_allowed()
|
||||
try:
|
||||
loop.draw_screen()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self._schedule_editor_recheck()
|
||||
|
||||
def toggle_focus_area(self):
|
||||
name = ""
|
||||
@@ -2213,6 +2289,8 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
elif key == "ctrl u":
|
||||
self.conversation.purge_failed()
|
||||
self.conversation_changed(None)
|
||||
elif key == "ctrl k":
|
||||
self.cancel_pending_send()
|
||||
elif key == "ctrl t":
|
||||
self.toggle_editor()
|
||||
elif key == "ctrl x":
|
||||
@@ -2226,9 +2304,61 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
self.attach_file()
|
||||
elif key == "ctrl s":
|
||||
self.save_focused_attachments()
|
||||
elif key == "ctrl v":
|
||||
self.cycle_delivery_method()
|
||||
elif key == "ctrl r":
|
||||
self.toggle_message_details()
|
||||
else:
|
||||
return key
|
||||
|
||||
def toggle_message_details(self):
|
||||
self.show_message_details = not self.show_message_details
|
||||
self.update_message_widgets(replace=True)
|
||||
|
||||
def _message_is_pending(self, conv_message):
|
||||
try:
|
||||
src = conv_message._cached_source_hash
|
||||
state = conv_message.get_state()
|
||||
except Exception:
|
||||
return False
|
||||
is_outbound = src is not None and self.app.lxmf_destination.hash == src
|
||||
return is_outbound and state is not None and state < LXMF.LXMessage.SENT
|
||||
|
||||
def cancel_message(self, conv_message):
|
||||
if conv_message is None:
|
||||
return
|
||||
try:
|
||||
self.app.message_router.cancel_outbound(conv_message.get_hash())
|
||||
conv_message._cached_state = LXMF.LXMessage.FAILED
|
||||
except Exception as e:
|
||||
RNS.log("Could not cancel outbound message: "+str(e), RNS.LOG_ERROR)
|
||||
self.conversation_changed(None)
|
||||
|
||||
def cancel_pending_send(self):
|
||||
for widget in reversed(self.message_widgets):
|
||||
conv_message = getattr(widget, "conv_message", None)
|
||||
if conv_message is not None and self._message_is_pending(conv_message):
|
||||
self.cancel_message(conv_message)
|
||||
return True
|
||||
return False
|
||||
|
||||
def cycle_delivery_method(self):
|
||||
source_hash_bytes = bytes.fromhex(self.source_hash)
|
||||
order = [DirectoryEntry.DIRECT, DirectoryEntry.OPPORTUNISTIC, DirectoryEntry.PROPAGATED]
|
||||
current = self.app.directory.preferred_delivery(source_hash_bytes)
|
||||
try:
|
||||
selected = order[(order.index(current) + 1) % len(order)]
|
||||
except ValueError:
|
||||
selected = order[0]
|
||||
entry = self.app.directory.find(source_hash_bytes)
|
||||
if entry is None:
|
||||
display_name = self.app.directory.display_name(source_hash_bytes)
|
||||
entry = DirectoryEntry(source_hash_bytes, display_name, DirectoryEntry.UNKNOWN, preferred_delivery=selected)
|
||||
else:
|
||||
entry.preferred_delivery = selected
|
||||
self.app.directory.remember(entry)
|
||||
self._update_peer_info()
|
||||
|
||||
def _on_conversation_changed_from_callback(self, conversation):
|
||||
self.delegate._wake(lambda: self.conversation_changed(conversation))
|
||||
|
||||
@@ -2238,6 +2368,14 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
self.update_message_widgets(replace = True)
|
||||
|
||||
def update_message_widgets(self, replace = False):
|
||||
prev_pos = None
|
||||
prev_count = len(self.message_widgets) if getattr(self, "message_widgets", None) else 0
|
||||
if getattr(self, "messagelist", None) is not None:
|
||||
try:
|
||||
prev_pos = self.messagelist.get_selected_position()
|
||||
except Exception:
|
||||
prev_pos = None
|
||||
|
||||
self.message_widgets = []
|
||||
added_hashes = set()
|
||||
needs_index = []
|
||||
@@ -2270,10 +2408,19 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
self.message_widgets.sort(key=lambda m: m.sort_timestamp, reverse=False)
|
||||
|
||||
from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox
|
||||
self.messagelist = IndicativeListBox(self.message_widgets, position = len(self.message_widgets)-1)
|
||||
new_count = len(self.message_widgets)
|
||||
if prev_pos is None or prev_count == 0 or prev_pos >= prev_count - 1:
|
||||
position = max(0, new_count - 1)
|
||||
else:
|
||||
position = min(prev_pos, max(0, new_count - 1))
|
||||
self.messagelist = IndicativeListBox(self.message_widgets, position = position)
|
||||
self.messagelist.name = "messagelist"
|
||||
if replace:
|
||||
self.frame.contents["body"] = (self.messagelist, None)
|
||||
current_body = self.frame.contents["body"][0]
|
||||
if self.dialog_active and isinstance(current_body, urwid.Overlay):
|
||||
current_body.bottom_w = self.messagelist
|
||||
else:
|
||||
self.frame.contents["body"] = (self.messagelist, None)
|
||||
nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.draw_screen()
|
||||
|
||||
|
||||
@@ -2423,6 +2570,7 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
|
||||
def attach_file(self):
|
||||
self.dialog_active = True
|
||||
initial_attachments = list(self.pending_attachments)
|
||||
|
||||
def on_change(selected):
|
||||
self.pending_attachments[:] = selected
|
||||
@@ -2434,7 +2582,7 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
self.file_browser_closed()
|
||||
|
||||
def on_cancel():
|
||||
self.pending_attachments.clear()
|
||||
self.pending_attachments[:] = initial_attachments
|
||||
self.frame.contents["footer"] = (self._build_footer(), None)
|
||||
self.file_browser_closed()
|
||||
|
||||
@@ -2445,6 +2593,7 @@ class ConversationWidget(urwid.WidgetWrap):
|
||||
title="Attach File",
|
||||
multi_select=True,
|
||||
selected=self.pending_attachments,
|
||||
max_file_size=ConversationWidget.MAX_ATTACHMENT_SIZE,
|
||||
)
|
||||
bottom = self.messagelist
|
||||
overlay = urwid.Overlay(browser, bottom, align=urwid.CENTER, width=("relative", 90), valign=urwid.MIDDLE, height=("relative", 80), left=2, right=2)
|
||||
@@ -2588,6 +2737,7 @@ class LXMessageWidget(urwid.WidgetWrap):
|
||||
app = nomadnet.NomadNetworkApp.get_shared_instance()
|
||||
g = app.ui.glyphs
|
||||
self._conversation_widget = conversation_widget
|
||||
self.conv_message = message
|
||||
self.timestamp = message.get_timestamp()
|
||||
self.sort_timestamp = message.sort_timestamp
|
||||
self.transfer_done = False
|
||||
@@ -2719,12 +2869,58 @@ class LXMessageWidget(urwid.WidgetWrap):
|
||||
|
||||
pile_widgets = [title]
|
||||
|
||||
if conversation_widget is not None and getattr(conversation_widget, "show_message_details", False):
|
||||
def _safe(fn):
|
||||
try: return fn()
|
||||
except Exception: return None
|
||||
|
||||
def _detail_row(glyph, values):
|
||||
return urwid.Columns([
|
||||
(3, urwid.Text(("body_text", glyph), align=urwid.RIGHT)),
|
||||
urwid.Text(("inactive_text", " · ".join(values))),
|
||||
], dividechars=1)
|
||||
|
||||
detail_rows = []
|
||||
|
||||
delivery = []
|
||||
if msg_method is not None:
|
||||
delivery.append(LXM_METHOD_NAMES.get(msg_method, "Unknown"))
|
||||
if is_outbound and msg_state is not None:
|
||||
delivery.append(LXM_STATE_NAMES.get(msg_state, "Unknown"))
|
||||
if delivery:
|
||||
detail_rows.append(_detail_row(g.get("sent" if is_outbound else "peer", ">"), delivery))
|
||||
|
||||
signal = []
|
||||
snr = _safe(message.get_snr)
|
||||
rssi = _safe(message.get_rssi)
|
||||
q = _safe(message.get_q)
|
||||
if snr is not None: signal.append("SNR "+str(round(snr, 1))+" dB")
|
||||
if rssi is not None: signal.append("RSSI "+str(round(rssi, 1))+" dBm")
|
||||
if q is not None: signal.append("Q "+str(round(q))+"%")
|
||||
if signal:
|
||||
detail_rows.append(_detail_row(g.get("speed", "~"), signal))
|
||||
|
||||
security = []
|
||||
sig = _safe(message.get_signature_description)
|
||||
if sig: security.append(sig)
|
||||
enc = _safe(message.get_transport_encryption)
|
||||
if enc: security.append(str(enc))
|
||||
if security:
|
||||
detail_rows.append(_detail_row(g.get("encrypted", "#"), security))
|
||||
|
||||
if msg_hash is not None:
|
||||
short_hash = _safe(lambda: RNS.hexrep(msg_hash, delimit=False))
|
||||
if short_hash:
|
||||
detail_rows.append(_detail_row(g.get("info", "i"), [short_hash]))
|
||||
|
||||
if detail_rows:
|
||||
pile_widgets.append(urwid.Padding(urwid.Pile(detail_rows), left=2, right=2))
|
||||
|
||||
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
|
||||
self._live_lxm = pending
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
@@ -2738,7 +2934,16 @@ class LXMessageWidget(urwid.WidgetWrap):
|
||||
else:
|
||||
bar = "#" * filled + "-" * (bar_width - filled)
|
||||
self.progress_widget.set_text(" ["+bar+"] "+str(pct)+"%")
|
||||
pile_widgets.append(self.progress_attr)
|
||||
if conversation_widget is not None:
|
||||
cancel_icon = ClickableIcon(" "+g["cross"]+" Cancel ",
|
||||
on_click=lambda cw=conversation_widget, cm=message: cw.cancel_message(cm))
|
||||
progress_row = urwid.Columns([
|
||||
("weight", 1, self.progress_attr),
|
||||
("pack", urwid.AttrMap(cancel_icon, "msg_notice_caution")),
|
||||
], dividechars=1)
|
||||
pile_widgets.append(progress_row)
|
||||
else:
|
||||
pile_widgets.append(self.progress_attr)
|
||||
self._start_progress_poll()
|
||||
|
||||
if markdown: pile_widgets.append(content_pile)
|
||||
|
||||
@@ -149,7 +149,10 @@ class MicronEdit(ReadlineEdit):
|
||||
class GutterEdit(MicronEdit):
|
||||
# MicronEdit with a wrap-aware line-number gutter on the left
|
||||
def _gutter_width(self, maxcol):
|
||||
return max(3, len(str(self.edit_text.count("\n") + 1)) + 2)
|
||||
gw = max(3, len(str(self.edit_text.count("\n") + 1)) + 2)
|
||||
if maxcol - gw < 1:
|
||||
return 0
|
||||
return gw
|
||||
|
||||
def rows(self, size, focus=False):
|
||||
gw = self._gutter_width(size[0])
|
||||
@@ -164,18 +167,24 @@ class GutterEdit(MicronEdit):
|
||||
|
||||
def move_cursor_to_coords(self, size, x, y):
|
||||
gw = self._gutter_width(size[0])
|
||||
return super().move_cursor_to_coords((max(1, size[0] - gw),), max(0, x - gw), y)
|
||||
if isinstance(x, int):
|
||||
x = max(0, x - gw)
|
||||
return super().move_cursor_to_coords((max(1, size[0] - gw),), x, y)
|
||||
|
||||
def render(self, size, focus=False):
|
||||
maxcol = size[0]
|
||||
gw = self._gutter_width(maxcol)
|
||||
if gw == 0:
|
||||
return super().render(size, focus)
|
||||
tw = max(1, maxcol - gw)
|
||||
|
||||
|
||||
cheap = len(self.edit_text) > MAX_HIGHLIGHT_CHARS
|
||||
wrap = "any" if cheap else "space"
|
||||
if self.wrap != wrap:
|
||||
self.set_wrap_mode(wrap)
|
||||
|
||||
edit_canv = super().render((tw,), focus=False)
|
||||
h = edit_canv.rows()
|
||||
cheap = len(self.edit_text) > MAX_HIGHLIGHT_CHARS
|
||||
rows = []
|
||||
for i, line in enumerate(self.edit_text.split("\n"), 1):
|
||||
rows.append(str(i).rjust(gw - 1) + " ")
|
||||
@@ -578,11 +587,19 @@ class PageEditorDisplay():
|
||||
d = os.path.dirname(path) or "."
|
||||
tmp = os.path.join(d, "." + os.path.basename(path) + ".tmp")
|
||||
try:
|
||||
existing_mode = None
|
||||
try:
|
||||
existing_mode = os.stat(path).st_mode
|
||||
except OSError:
|
||||
existing_mode = None
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
f.flush()
|
||||
try: os.fsync(f.fileno())
|
||||
except Exception: pass
|
||||
if existing_mode is not None:
|
||||
try: os.chmod(tmp, existing_mode)
|
||||
except Exception: pass
|
||||
os.replace(tmp, path)
|
||||
except Exception:
|
||||
try: os.remove(tmp)
|
||||
@@ -716,10 +733,13 @@ class PageEditorDisplay():
|
||||
if self.current_path and self.editable:
|
||||
self.save_file(self.current_path)
|
||||
proceed()
|
||||
name = os.path.basename(self.current_path) if self.current_path else "this file"
|
||||
name = os.path.basename(self.current_path) if self.current_path else "this buffer"
|
||||
if self.current_path and self.editable:
|
||||
buttons = [("Save", save_then), ("Discard", lambda: (self.close_dialog(), proceed())), ("Cancel", self.close_dialog)]
|
||||
else:
|
||||
buttons = [("Discard", lambda: (self.close_dialog(), proceed())), ("Cancel", self.close_dialog)]
|
||||
self.show_dialog(ConfirmDialog("Unsaved changes", "Discard unsaved changes to %s?" % name,
|
||||
[("Save", save_then), ("Discard", lambda: (self.close_dialog(), proceed())), ("Cancel", self.close_dialog)],
|
||||
on_cancel=self.close_dialog))
|
||||
buttons, on_cancel=self.close_dialog))
|
||||
|
||||
def load_file(self, path):
|
||||
try:
|
||||
@@ -839,10 +859,18 @@ class PageEditorDisplay():
|
||||
try:
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
os.rename(old, target)
|
||||
if os.path.isfile(old + ".allowed"):
|
||||
try: os.replace(old + ".allowed", target + ".allowed")
|
||||
except Exception: pass
|
||||
old_prefix = old + os.sep
|
||||
if self.current_path == old:
|
||||
self.current_path = target; self.update_title()
|
||||
if old in self.expanded:
|
||||
self.expanded.discard(old); self.expanded.add(target)
|
||||
elif self.current_path and self.current_path.startswith(old_prefix):
|
||||
self.current_path = target + self.current_path[len(old):]; self.update_title()
|
||||
self.expanded = set(
|
||||
target if p == old else (target + p[len(old):] if p.startswith(old_prefix) else p)
|
||||
for p in self.expanded
|
||||
)
|
||||
self.refresh_tree(); self.save_state()
|
||||
self.set_status("renamed to " + os.path.basename(target))
|
||||
except Exception as e:
|
||||
@@ -866,11 +894,16 @@ class PageEditorDisplay():
|
||||
def _do_delete(self, path):
|
||||
self.close_dialog()
|
||||
try:
|
||||
prefix = path + os.sep
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path); self.expanded.discard(path)
|
||||
shutil.rmtree(path)
|
||||
self.expanded = set(p for p in self.expanded if p != path and not p.startswith(prefix))
|
||||
else:
|
||||
os.remove(path)
|
||||
if self.current_path == path:
|
||||
if os.path.isfile(path + ".allowed"):
|
||||
try: os.remove(path + ".allowed")
|
||||
except Exception: pass
|
||||
if self.current_path is not None and (self.current_path == path or self.current_path.startswith(prefix)):
|
||||
self.current_path = None; self.editable = False; self.is_exec = False; self.dirty = False
|
||||
self._loading = True; self.editor.set_edit_text(""); self._loading = False
|
||||
self.update_title(); self.set_preview_note("")
|
||||
@@ -1016,7 +1049,7 @@ class PageEditorDisplay():
|
||||
self.body.original_widget = self.columns
|
||||
|
||||
def on_edit_change(self, widget, old_text):
|
||||
if self._loading or not self.editable:
|
||||
if self._loading or (self.current_path is not None and not self.editable):
|
||||
return
|
||||
self.dirty = True
|
||||
self.set_modified(True)
|
||||
|
||||
@@ -46,11 +46,12 @@ class FileBrowserEntry(urwid.WidgetWrap):
|
||||
return False
|
||||
|
||||
class FileBrowser(urwid.WidgetWrap):
|
||||
def __init__(self, on_done=None, on_cancel=None, on_change=None, title="Select File", multi_select=True, start_path=None, selected=None):
|
||||
def __init__(self, on_done=None, on_cancel=None, on_change=None, title="Select File", multi_select=True, start_path=None, selected=None, max_file_size=None):
|
||||
self.on_done_cb = on_done
|
||||
self.on_cancel_cb = on_cancel
|
||||
self.on_change_cb = on_change
|
||||
self.multi_select = multi_select
|
||||
self.max_file_size = max_file_size
|
||||
self.selected = list(selected) if selected else []
|
||||
|
||||
app = nomadnet.NomadNetworkApp.get_shared_instance()
|
||||
@@ -81,7 +82,26 @@ class FileBrowser(urwid.WidgetWrap):
|
||||
self._populate()
|
||||
|
||||
self.browser_frame = urwid.Frame(self.file_listbox, header=header_pile, footer=footer_pile)
|
||||
super().__init__(urwid.LineBox(self.browser_frame, title=title))
|
||||
self._main_widget = urwid.LineBox(self.browser_frame, title=title)
|
||||
self._notice_open = False
|
||||
super().__init__(self._main_widget)
|
||||
|
||||
def _show_notice(self, message, title="Notice"):
|
||||
def dismiss(_b=None):
|
||||
self._notice_open = False
|
||||
self._w = self._main_widget
|
||||
|
||||
box = urwid.LineBox(urwid.Pile([
|
||||
urwid.Text(message, align=urwid.CENTER),
|
||||
urwid.Divider(),
|
||||
urwid.Padding(urwid.Button("OK", on_press=dismiss), align=urwid.CENTER, width=8),
|
||||
]), title=title)
|
||||
self._notice_open = True
|
||||
self._w = urwid.Overlay(box, self._main_widget, align=urwid.CENTER, width=("relative", 75), valign=urwid.MIDDLE, height=urwid.PACK)
|
||||
|
||||
def _dismiss_notice(self):
|
||||
self._notice_open = False
|
||||
self._w = self._main_widget
|
||||
|
||||
def _update_status(self):
|
||||
if self.selected:
|
||||
@@ -157,6 +177,20 @@ class FileBrowser(urwid.WidgetWrap):
|
||||
self._populate()
|
||||
return
|
||||
|
||||
if self.max_file_size is not None and entry.full_path not in self.selected:
|
||||
try:
|
||||
file_size = os.path.getsize(entry.full_path)
|
||||
except Exception:
|
||||
file_size = 0
|
||||
if file_size > self.max_file_size:
|
||||
limit_mb = self.max_file_size / (1024 * 1024)
|
||||
size_mb = file_size / (1024 * 1024)
|
||||
self._show_notice(
|
||||
"%s is %.1f MB.\nAttachments are limited to %.0f MB." % (entry.name, size_mb, limit_mb),
|
||||
title="File too large",
|
||||
)
|
||||
return
|
||||
|
||||
if self.multi_select:
|
||||
if entry.full_path in self.selected:
|
||||
self.selected.remove(entry.full_path)
|
||||
@@ -182,6 +216,11 @@ class FileBrowser(urwid.WidgetWrap):
|
||||
self.on_cancel_cb()
|
||||
|
||||
def keypress(self, size, key):
|
||||
if self._notice_open:
|
||||
if key in ("esc", "enter"):
|
||||
self._dismiss_notice()
|
||||
return None
|
||||
return super().keypress(size, key)
|
||||
if key == "esc":
|
||||
self._cancel(None)
|
||||
return None
|
||||
|
||||
@@ -201,7 +201,10 @@ class MainDisplay():
|
||||
return
|
||||
|
||||
active = self.sub_displays.active_display
|
||||
if active in (self.sub_displays.log_display, self.sub_displays.config_display):
|
||||
if active is self.sub_displays.log_display:
|
||||
self.do_quit()
|
||||
return
|
||||
if active is self.sub_displays.config_display and self.sub_displays.config_display.editor_term is not None:
|
||||
self.do_quit()
|
||||
return
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ class AnnounceInfo(urwid.WidgetWrap):
|
||||
display_name = op_str
|
||||
|
||||
if not source_hash_text in [c[0] for c in existing_conversations]:
|
||||
entry = DirectoryEntry(source_hash, display_name, trust_level)
|
||||
entry = DirectoryEntry(op_hash, display_name, trust_level)
|
||||
self.app.directory.remember(entry)
|
||||
|
||||
new_conversation = nomadnet.Conversation(source_hash_text, nomadnet.NomadNetworkApp.get_shared_instance(), initiator=True)
|
||||
@@ -716,7 +716,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
|
||||
display_name = op_str
|
||||
|
||||
if not source_hash_text in [c[0] for c in existing_conversations]:
|
||||
entry = DirectoryEntry(source_hash, display_name, trust_level)
|
||||
entry = DirectoryEntry(op_hash, display_name, trust_level)
|
||||
self.app.directory.remember(entry)
|
||||
|
||||
new_conversation = nomadnet.Conversation(source_hash_text, nomadnet.NomadNetworkApp.get_shared_instance(), initiator=True)
|
||||
@@ -1532,13 +1532,19 @@ class NodeInfo(urwid.WidgetWrap):
|
||||
self.t_total_pages,
|
||||
self.t_total_files,
|
||||
urwid.Divider(g["divider1"]),
|
||||
urwid.Pile([
|
||||
urwid.Button("Peer Info", on_press=show_peer_info),
|
||||
connect_button,
|
||||
edit_button,
|
||||
reset_button,
|
||||
announce_button,
|
||||
qr_button,
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 5, urwid.Button("Back", on_press=show_peer_info)),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 6, connect_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 8, reset_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 7, announce_button),
|
||||
]),
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 4, edit_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 9, qr_button),
|
||||
])
|
||||
])
|
||||
else:
|
||||
@@ -1553,13 +1559,19 @@ class NodeInfo(urwid.WidgetWrap):
|
||||
self.t_total_pages,
|
||||
self.t_total_files,
|
||||
urwid.Divider(g["divider1"]),
|
||||
urwid.Pile([
|
||||
urwid.Button("Peer Info", on_press=show_peer_info),
|
||||
connect_button,
|
||||
edit_button,
|
||||
reset_button,
|
||||
announce_button,
|
||||
qr_button,
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 5, urwid.Button("Back", on_press=show_peer_info)),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 6, connect_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 8, reset_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 7, announce_button),
|
||||
]),
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 4, edit_button),
|
||||
(urwid.WEIGHT, 0.5, urwid.Text("")),
|
||||
(urwid.WEIGHT, 9, qr_button),
|
||||
])
|
||||
])
|
||||
else:
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import RNS
|
||||
import time
|
||||
import nomadnet
|
||||
import urwid
|
||||
|
||||
def _get_cols_rows():
|
||||
return nomadnet.NomadNetworkApp.get_shared_instance().ui.screen.get_cols_rows()
|
||||
|
||||
def _fmt_secs(secs):
|
||||
secs = int(secs)
|
||||
if secs < 0:
|
||||
@@ -142,7 +138,7 @@ class LazyTableWalker(urwid.ListWalker):
|
||||
class TransportFiller(urwid.WidgetWrap):
|
||||
def __init__(self, widget, display):
|
||||
self.display = display
|
||||
super().__init__(urwid.Filler(widget, urwid.TOP))
|
||||
super().__init__(widget)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "tab":
|
||||
@@ -173,7 +169,7 @@ class TransportDisplay:
|
||||
|
||||
HASH_W = 34
|
||||
HOPS_W = 6
|
||||
VIA_W = 20
|
||||
VIA_W = 34
|
||||
EXP_W = 12
|
||||
LAST_W = 16
|
||||
VIOL_W = 11
|
||||
@@ -215,16 +211,13 @@ class TransportDisplay:
|
||||
urwid.AttrMap(self.filter_edit, "list_normal", focus_map="list_focus"),
|
||||
], dividechars=1)
|
||||
|
||||
_cols, rows = _get_cols_rows()
|
||||
self.box_adapter = urwid.BoxAdapter(self.list_box, max(3, rows - 10))
|
||||
|
||||
self.pile = urwid.Pile([
|
||||
('pack', self.summary_text),
|
||||
('pack', self._tab_bar()),
|
||||
('pack', self.filter_row),
|
||||
('pack', self._header_row()),
|
||||
('pack', urwid.Divider("─")),
|
||||
self.box_adapter,
|
||||
('weight', 1, self.list_box),
|
||||
])
|
||||
|
||||
self.transport_view = TransportFiller(self.pile, self)
|
||||
@@ -429,12 +422,6 @@ class TransportDisplay:
|
||||
empty = urwid.Text(("inactive_text", empty_msg), align=urwid.CENTER)
|
||||
self.list_walker.configure(data, factory, empty)
|
||||
|
||||
try:
|
||||
_cols, rows = _get_cols_rows()
|
||||
self.box_adapter.height = max(3, rows - 10)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def cycle_tab(self):
|
||||
order = ["paths", "rates", "interfaces"]
|
||||
i = order.index(self.active_tab) if self.active_tab in order else 0
|
||||
@@ -492,12 +479,13 @@ class TransportDisplay:
|
||||
self.app.ui.loop.set_alarm_in(self.POLL_INTERVAL, self._poll)
|
||||
|
||||
def _poll(self, loop, user_data):
|
||||
if self._is_visible():
|
||||
self._refresh_data()
|
||||
self._rebuild()
|
||||
try:
|
||||
loop.draw_screen()
|
||||
except Exception:
|
||||
pass
|
||||
if self.started:
|
||||
loop.set_alarm_in(self.POLL_INTERVAL, self._poll)
|
||||
if not self.started or not self._is_visible():
|
||||
self.poll_scheduler = False
|
||||
return
|
||||
self._refresh_data()
|
||||
self._rebuild()
|
||||
try:
|
||||
loop.draw_screen()
|
||||
except Exception:
|
||||
pass
|
||||
loop.set_alarm_in(self.POLL_INTERVAL, self._poll)
|
||||
|
||||
@@ -6,7 +6,7 @@ import threading
|
||||
import nomadnet
|
||||
import urwid
|
||||
|
||||
from .Transport import TransportDisplay, _get_cols_rows
|
||||
from .Transport import TransportDisplay
|
||||
from .ReadlineEdit import ReadlineEdit
|
||||
from .FileBrowser import FileBrowser
|
||||
from nomadnet.vendor.additional_urwid_widgets.FormWidgets import Dropdown
|
||||
@@ -145,15 +145,11 @@ class UtilitiesDisplay:
|
||||
]
|
||||
self.landing_listbox = urwid.ListBox(urwid.SimpleFocusListWalker(items))
|
||||
|
||||
_cols, rows = _get_cols_rows()
|
||||
box = urwid.BoxAdapter(self.landing_listbox, max(4, rows - 6))
|
||||
|
||||
pile = urwid.Pile([
|
||||
self.landing_view = urwid.Pile([
|
||||
('pack', urwid.Text(("body_text", "Select a utility to open."), align=urwid.CENTER)),
|
||||
('pack', urwid.Divider("─")),
|
||||
box,
|
||||
('weight', 1, self.landing_listbox),
|
||||
])
|
||||
self.landing_view = urwid.Filler(pile, urwid.TOP)
|
||||
|
||||
def shortcuts(self):
|
||||
if self.active == "transport" and self.transport is not None:
|
||||
@@ -467,17 +463,21 @@ class ProbeView:
|
||||
receipt.set_delivery_callback(self._on_delivered)
|
||||
except Exception:
|
||||
pass
|
||||
self.app.ui.loop.set_alarm_in(self._timeout(), self._on_timeout)
|
||||
self.app.ui.loop.set_alarm_in(self._timeout(), self._on_timeout, user_data=receipt)
|
||||
|
||||
def _on_delivered(self, receipt):
|
||||
self._schedule(lambda: self._conclude(receipt, True))
|
||||
|
||||
def _on_timeout(self, loop, user_data):
|
||||
self._conclude(self._cur_receipt, False)
|
||||
if user_data is not self._cur_receipt:
|
||||
return
|
||||
self._conclude(user_data, False)
|
||||
|
||||
def _conclude(self, receipt, delivered):
|
||||
if self._concluded:
|
||||
return
|
||||
if receipt is not self._cur_receipt:
|
||||
return
|
||||
self._concluded = True
|
||||
|
||||
is_delivered = delivered
|
||||
@@ -851,7 +851,7 @@ class IdentityView:
|
||||
loop.set_alarm_in(0.25, self._await_identity)
|
||||
|
||||
def _show_identity(self, identity):
|
||||
self._log("kno wn %s resolves to identity %s" % (
|
||||
self._log("%s resolves to identity %s" % (
|
||||
RNS.prettyhexrep(self._dest_hash), RNS.prettyhexrep(identity.hash)), "connected_status")
|
||||
|
||||
RNCP_APP_NAME = "rncp"
|
||||
@@ -1103,7 +1103,7 @@ class SendFileView:
|
||||
self.progress_text.set_text(("connected_status", "Transferring… 100%"))
|
||||
self._log("Transfer complete", "connected_status")
|
||||
else:
|
||||
self._log("Transfer failed or the file was not accepted ,", "warning_text")
|
||||
self._log("Transfer failed or the file was not accepted", "warning_text")
|
||||
try:
|
||||
if self._link is not None:
|
||||
self._link.teardown()
|
||||
@@ -1127,6 +1127,7 @@ class SendFileView:
|
||||
|
||||
SPEEDTEST_APP_NAME = "nomadnetwork"
|
||||
SPEEDTEST_ASPECTS = ("utilities", "speedtest")
|
||||
SPEEDTEST_FULL_NAME = ".".join((SPEEDTEST_APP_NAME,) + SPEEDTEST_ASPECTS)
|
||||
SPEEDTEST_INMEM_MAX = 8 * 1024 * 1024
|
||||
|
||||
SPEEDTEST_SIZES = [
|
||||
@@ -1192,6 +1193,7 @@ class SpeedTestView:
|
||||
self._client_resource = None
|
||||
self._send_started = None
|
||||
self._send_bytes = 0
|
||||
self._send_seq = 0
|
||||
self._tmppath = None
|
||||
|
||||
self.hash_edit = ReadlineEdit("", "")
|
||||
@@ -1257,9 +1259,9 @@ class SpeedTestView:
|
||||
self.listen_status.set_text(("connected_status", "%s %s Receiving test…" % (dot, self.g.get("arrow_d", "v"))))
|
||||
elif self._listening and self.server_destination is not None:
|
||||
if self._peer_hash_entered():
|
||||
self.listen_status.set_text(("connected_status", "%s Listening and ready to receive or send to from entered peer" % dot))
|
||||
self.listen_status.set_text(("connected_status", "%s Listening, ready to receive from or send to the entered peer" % dot))
|
||||
else:
|
||||
self.listen_status.set_text(("warning_text", "%s Listening enter the peer's hash to accept their test" % dot))
|
||||
self.listen_status.set_text(("warning_text", "%s Listening, enter the peer's hash to accept their test" % dot))
|
||||
else:
|
||||
self.listen_status.set_text(("inactive_text", "Not listening"))
|
||||
|
||||
@@ -1361,13 +1363,13 @@ class SpeedTestView:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _peer_identity(self):
|
||||
def _entered_peer_hash(self):
|
||||
hexhash = self.hash_edit.edit_text.strip()
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
||||
if len(hexhash) != dest_len:
|
||||
return None
|
||||
try:
|
||||
return RNS.Identity.recall(bytes.fromhex(hexhash))
|
||||
return bytes.fromhex(hexhash)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -1414,8 +1416,12 @@ class SpeedTestView:
|
||||
self._schedule(lambda: self._verify_incoming(link, identity))
|
||||
|
||||
def _verify_incoming(self, link, identity):
|
||||
peer_identity = self._peer_identity()
|
||||
if peer_identity is None or peer_identity.hash != identity.hash:
|
||||
expected_hash = self._entered_peer_hash()
|
||||
try:
|
||||
identified_hash = RNS.Destination.hash_from_name_and_identity(SPEEDTEST_FULL_NAME, identity)
|
||||
except Exception:
|
||||
identified_hash = None
|
||||
if expected_hash is None or identified_hash is None or identified_hash != expected_hash:
|
||||
self._log("Rejected speed test from unauthorized peer", "warning_text")
|
||||
try:
|
||||
link.teardown()
|
||||
@@ -1437,15 +1443,28 @@ class SpeedTestView:
|
||||
|
||||
def _recv_started_now(self, resource):
|
||||
self._recv_resource = resource
|
||||
self._recv_started = time.time()
|
||||
self._recv_polling = True
|
||||
if self._recv_started is None:
|
||||
self._recv_started = time.time()
|
||||
self.rate_text.set_text(("body_text", "Receiving…"))
|
||||
self._update_listen_status()
|
||||
self.app.ui.loop.set_alarm_in(0.3, self._recv_poll)
|
||||
if not self._recv_polling:
|
||||
self._recv_polling = True
|
||||
self.app.ui.loop.set_alarm_in(0.3, self._recv_poll)
|
||||
|
||||
def _recv_poll(self, loop, user_data):
|
||||
if not self._recv_polling or self._recv_resource is None:
|
||||
return
|
||||
failed = False
|
||||
try:
|
||||
if self._recv_resource.status in (RNS.Resource.FAILED, RNS.Resource.CORRUPT):
|
||||
failed = True
|
||||
elif self._recv_link is not None and self._recv_link.status == RNS.Link.CLOSED:
|
||||
failed = True
|
||||
except Exception:
|
||||
pass
|
||||
if failed:
|
||||
self._recv_failed()
|
||||
return
|
||||
try:
|
||||
pct = int(self._recv_resource.get_progress() * 100)
|
||||
except Exception:
|
||||
@@ -1459,16 +1478,28 @@ class SpeedTestView:
|
||||
def _on_recv_concluded(self, resource):
|
||||
self._schedule(lambda: self._recv_concluded(resource))
|
||||
|
||||
def _recv_concluded(self, resource):
|
||||
def _recv_failed(self):
|
||||
self._recv_polling = False
|
||||
self._log("Incoming transfer failed", "warning_text")
|
||||
self.rate_text.set_text("")
|
||||
self._recv_resource = None
|
||||
self._recv_link = None
|
||||
self._update_listen_status()
|
||||
|
||||
def _recv_concluded(self, resource):
|
||||
try:
|
||||
complete = resource.status == RNS.Resource.COMPLETE
|
||||
except Exception:
|
||||
complete = False
|
||||
if not complete:
|
||||
self._log("Incoming transfer failed", "warning_text")
|
||||
self.rate_text.set_text("")
|
||||
self._recv_failed()
|
||||
return
|
||||
try:
|
||||
if resource.segment_index < resource.total_segments:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self._recv_polling = False
|
||||
size = self._resource_size(resource)
|
||||
elapsed = max(0.001, time.time() - (self._recv_started or time.time()))
|
||||
self._set_progress(100)
|
||||
@@ -1480,7 +1511,7 @@ class SpeedTestView:
|
||||
self._update_listen_status()
|
||||
|
||||
def _resource_size(self, resource):
|
||||
for getter in ("get_transfer_size", "get_data_size"):
|
||||
for getter in ("get_data_size", "get_transfer_size"):
|
||||
try:
|
||||
fn = getattr(resource, getter, None)
|
||||
if fn:
|
||||
@@ -1500,7 +1531,6 @@ class SpeedTestView:
|
||||
return
|
||||
self.update_interface_status()
|
||||
|
||||
peer_identity = self._peer_identity()
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
||||
hexhash = self.hash_edit.edit_text.strip()
|
||||
if len(hexhash) != dest_len:
|
||||
@@ -1515,6 +1545,7 @@ class SpeedTestView:
|
||||
label = self.size_dropdown.get_value()
|
||||
self._send_bytes = SPEEDTEST_BYTES_BY_LABEL.get(label, 1024 * 1024)
|
||||
|
||||
self._send_seq += 1
|
||||
self._sending = True
|
||||
self.start_button.original_widget.set_label("Cancel")
|
||||
self._set_progress(0)
|
||||
@@ -1596,14 +1627,15 @@ class SpeedTestView:
|
||||
except Exception:
|
||||
pass
|
||||
self._log_link_mtu(link)
|
||||
self.app.ui.loop.set_alarm_in(0.6, lambda l, u: self._prepare_and_send(link))
|
||||
seq = self._send_seq
|
||||
self.app.ui.loop.set_alarm_in(0.6, lambda l, u: self._prepare_and_send(link, seq))
|
||||
|
||||
def _prepare_and_send(self, link):
|
||||
if not self._sending or self._client_resource is not None:
|
||||
def _prepare_and_send(self, link, seq):
|
||||
if not self._sending or seq != self._send_seq or self._client_resource is not None:
|
||||
return
|
||||
nbytes = self._send_bytes
|
||||
if nbytes <= SPEEDTEST_INMEM_MAX:
|
||||
self._begin_resource(link, os.urandom(nbytes), None)
|
||||
self._begin_resource(link, os.urandom(nbytes), None, seq)
|
||||
return
|
||||
self._log("Preparing %s payload …" % _size_str(nbytes))
|
||||
|
||||
@@ -1613,7 +1645,7 @@ class SpeedTestView:
|
||||
fd, path = tempfile.mkstemp(prefix="nn_speedtest_")
|
||||
written = 0
|
||||
with os.fdopen(fd, "wb") as f:
|
||||
while written < nbytes and self._sending:
|
||||
while written < nbytes and self._sending and seq == self._send_seq:
|
||||
chunk = min(1024 * 1024, nbytes - written)
|
||||
f.write(os.urandom(chunk))
|
||||
written += chunk
|
||||
@@ -1625,19 +1657,19 @@ class SpeedTestView:
|
||||
pass
|
||||
self._schedule(lambda: (self._log("Could not prepare payload: %s" % str(e), "error_text"), self._finish()))
|
||||
return
|
||||
if not self._sending:
|
||||
if not self._sending or seq != self._send_seq:
|
||||
if path:
|
||||
try:
|
||||
os.remove(path)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
self._schedule(lambda: self._begin_resource(link, None, path))
|
||||
self._schedule(lambda: self._begin_resource(link, None, path, seq))
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _begin_resource(self, link, data_bytes, tmppath):
|
||||
if not self._sending:
|
||||
def _begin_resource(self, link, data_bytes, tmppath, seq):
|
||||
if not self._sending or seq != self._send_seq:
|
||||
if tmppath:
|
||||
try:
|
||||
os.remove(tmppath)
|
||||
@@ -1670,6 +1702,8 @@ class SpeedTestView:
|
||||
self._schedule(lambda: self._send_done(resource))
|
||||
|
||||
def _send_done(self, resource):
|
||||
if not self._sending:
|
||||
return
|
||||
try:
|
||||
complete = resource.status == RNS.Resource.COMPLETE
|
||||
except Exception:
|
||||
@@ -1702,6 +1736,7 @@ class SpeedTestView:
|
||||
def cancel(self, silent=False):
|
||||
if not self._sending:
|
||||
return
|
||||
self._send_seq += 1
|
||||
self._sending = False
|
||||
try:
|
||||
if self._client_resource is not None:
|
||||
@@ -1719,13 +1754,13 @@ class SpeedTestView:
|
||||
self._log("Speed test cancelled", "warning_text")
|
||||
self.rate_text.set_text("")
|
||||
self._set_progress(0)
|
||||
self.start_button.original_widget.set_label("Start test")
|
||||
self.start_button.original_widget.set_label("Send test file")
|
||||
self._update_listen_status()
|
||||
self._draw()
|
||||
|
||||
def _finish(self):
|
||||
self._sending = False
|
||||
self._client_resource = None
|
||||
self.start_button.original_widget.set_label("Start test")
|
||||
self.start_button.original_widget.set_label("Send test file")
|
||||
self._update_listen_status()
|
||||
self._draw()
|
||||
|
||||
+14
-3
@@ -139,9 +139,20 @@ class FormEdit(Placeholder, FormField):
|
||||
self.error = "This field is required"
|
||||
break
|
||||
elif validation == "number":
|
||||
if value and not value.replace('-', '').replace('.', '').isdigit():
|
||||
self.error = "This field must be a number"
|
||||
try:
|
||||
if value:
|
||||
int(value)
|
||||
except ValueError:
|
||||
self.error = "This field must be a whole number"
|
||||
break
|
||||
elif validation == "stamp_cost":
|
||||
if value and value.lower() != "none":
|
||||
try:
|
||||
if not 1 <= int(value) <= 255:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.error = "This field must be 1-255 or None"
|
||||
break
|
||||
elif validation == "float":
|
||||
try:
|
||||
if value:
|
||||
@@ -172,7 +183,7 @@ class FormCheckbox(urwid.CheckBox, FormField):
|
||||
for validation in self.validation_types:
|
||||
if validation == "required":
|
||||
if not value:
|
||||
self.error = "This field is required"
|
||||
self.error = "This field is required"
|
||||
break
|
||||
|
||||
self.error_widget.set_text(("error", self.error or ""))
|
||||
|
||||
Reference in New Issue
Block a user