diff --git a/nomadnet/Conversation.py b/nomadnet/Conversation.py index c3012bb..20bcdde 100644 --- a/nomadnet/Conversation.py +++ b/nomadnet/Conversation.py @@ -17,9 +17,9 @@ class Conversation: def conversation_list(app): conversations = [] for entry in os.listdir(app.conversationpath): - if os.path.isdir(app.conversationpath + "/" + entry): + if len(entry) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2 and os.path.isdir(app.conversationpath + "/" + entry): try: - conversations.append(Conversation(entry, app)) + conversations.append(entry) except Exception as e: RNS.log("Error while loading conversation "+str(entry)+", skipping it. The contained exception was: "+str(e), RNS.LOG_ERROR) @@ -29,12 +29,71 @@ class Conversation: def __init__(self, source_hash, app): self.source_hash = source_hash - self.message_path = app.conversationpath + "/" + source_hash + self.messages_path = app.conversationpath + "/" + source_hash self.messages_load_time = None self.messages = [] self.source_known = False self.source_trusted = False self.source_blocked = False + for filename in os.listdir(self.messages_path): + if len(filename) == RNS.Identity.HASHLENGTH//8*2: + message_path = self.messages_path + "/" + filename + self.messages.append(ConversationMessage(message_path)) + def __str__(self): - return self.source_hash \ No newline at end of file + return self.source_hash + +class ConversationMessage: + def __init__(self, file_path): + self.file_path = file_path + self.loaded = False + self.timestamp = None + self.lxm = None + + def load(self): + try: + self.lxm = LXMF.LXMessage.unpack_from_file(open(self.file_path, "rb")) + self.loaded = True + self.timestamp = self.lxm.timestamp + except Exception as e: + RNS.log("Error while loading LXMF message "+str(self.file_path)+" from disk. The contained exception was: "+str(e), RNS.LOG_ERROR) + + def unload(self): + self.loaded = False + self.lxm = None + + def get_title(self): + if not self.loaded: + self.load() + + return self.lxm.title_as_string() + + def get_content(self): + if not self.loaded: + self.load() + + return self.lxm.content_as_string() + + def get_hash(self): + if not self.loaded: + self.load() + + return self.lxm.hash + + def signature_validated(self): + if not self.loaded: + self.load() + + return self.lxm.signature_validated + + def get_signature_description(self): + if self.signature_validated(): + return "Signature Verified" + else: + if self.lxm.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + return "Unknown Origin" + elif self.lxm.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + return "Invalid Signature" + else: + return "Unknown signature validation failure" \ No newline at end of file diff --git a/nomadnet/NomadNetworkApp.py b/nomadnet/NomadNetworkApp.py index 8d7f20d..49bdd84 100644 --- a/nomadnet/NomadNetworkApp.py +++ b/nomadnet/NomadNetworkApp.py @@ -10,6 +10,7 @@ from ._version import __version__ from .vendor.configobj import ConfigObj class NomadNetworkApp: + time_format = "%Y-%m-%d %H:%M:%S" _shared_instance = None configdir = os.path.expanduser("~")+"/.nomadnetwork" @@ -193,25 +194,25 @@ class NomadNetworkApp: self.config["textui"]["colormode"] = nomadnet.ui.COLORMODE_16 else: if self.config["textui"]["colormode"].lower() == "monochrome": - self.config["textui"]["colormode"] = nomadnet.ui.COLORMODE_MONO + self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_MONO elif self.config["textui"]["colormode"].lower() == "16": - self.config["textui"]["colormode"] = nomadnet.ui.COLORMODE_16 + self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_16 elif self.config["textui"]["colormode"].lower() == "88": - self.config["textui"]["colormode"] = nomadnet.ui.COLORMODE_88 + self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_88 elif self.config["textui"]["colormode"].lower() == "256": - self.config["textui"]["colormode"] = nomadnet.ui.COLORMODE_256 + self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_256 elif self.config["textui"]["colormode"].lower() == "24bit": - self.config["textui"]["colormode"] = nomadnet.ui.COLORMODE_TRUE + self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_TRUE else: raise ValueError("The selected Text UI color mode is invalid") if not "theme" in self.config["textui"]: - self.config["textui"]["theme"] = nomadnet.ui.THEME_DARK + self.config["textui"]["theme"] = nomadnet.ui.TextUI.THEME_DARK else: if self.config["textui"]["theme"].lower() == "dark": - self.config["textui"]["theme"] = nomadnet.ui.THEME_DARK + self.config["textui"]["theme"] = nomadnet.ui.TextUI.THEME_DARK elif self.config["textui"]["theme"].lower() == "light": - self.config["textui"]["theme"] = nomadnet.ui.THEME_LIGHT + self.config["textui"]["theme"] = nomadnet.ui.TextUI.THEME_LIGHT else: raise ValueError("The selected Text UI theme is invalid") else: diff --git a/nomadnet/ui/TextUI.py b/nomadnet/ui/TextUI.py index 2490493..a14b657 100644 --- a/nomadnet/ui/TextUI.py +++ b/nomadnet/ui/TextUI.py @@ -2,9 +2,32 @@ import RNS import importlib import time -from nomadnet import NomadNetworkApp -from nomadnet.ui import * +import nomadnet from nomadnet.ui.textui import * +from nomadnet import NomadNetworkApp + +COLORMODE_MONO = 1 +COLORMODE_16 = 16 +COLORMODE_88 = 88 +COLORMODE_256 = 256 +COLORMODE_TRUE = 2**24 +THEME_DARK = 0x01 +THEME_LIGHT = 0x02 + +THEMES = { + THEME_DARK: [ + # Style name # 16-color style # Monochrome style # 88, 256 and true-color style + ('heading', 'light gray,underline', 'default', 'underline', 'g93,underline', 'default'), + ('menubar', 'black', 'light gray', 'standout', '#111', '#bbb'), + ('shortcutbar', 'black', 'light gray', 'standout', '#111', '#bbb'), + ('body_text', 'white', 'default', 'default', '#0a0', 'default'), + ('buttons', 'light green,bold', 'default', 'default', '#00a533', 'default'), + ('msg_editor', 'black', 'light cyan', 'standout', '#111', '#0bb'), + ("msg_header_ok", 'black', 'light green', 'standout', 'black', '#6b2',), + ("msg_header_caution", 'black', 'yellow', 'standout', 'black', '#fd3',), + ("list_focus", "black", "light cyan", "standout", "#111", "#0bb"), + ] +} class TextUI: @@ -25,16 +48,15 @@ class TextUI: colormode = self.app.config["textui"]["colormode"] theme = self.app.config["textui"]["theme"] - palette = nomadnet.ui.THEMES[theme] + palette = THEMES[theme] self.screen = urwid.raw_display.Screen() self.screen.register_palette(palette) - #self.main_display = nomadnet.ui.textui.Extras.DemoDisplay(self, self.app) - self.main_display = nomadnet.ui.textui.Main.MainDisplay(self, self.app) + self.main_display = Main.MainDisplay(self, self.app) if intro_timeout > 0: - self.intro_display = nomadnet.ui.textui.Extras.IntroDisplay(self.app) + self.intro_display = Extras.IntroDisplay(self.app) initial_widget = self.intro_display.widget else: initial_widget = self.main_display.widget diff --git a/nomadnet/ui/__init__.py b/nomadnet/ui/__init__.py index bfd13b1..8fd7944 100644 --- a/nomadnet/ui/__init__.py +++ b/nomadnet/ui/__init__.py @@ -3,11 +3,6 @@ import glob import RNS import nomadnet -from .MenuUI import MenuUI -from .TextUI import TextUI -from .GraphicalUI import GraphicalUI -from .WebUI import WebUI - modules = glob.glob(os.path.dirname(__file__)+"/*.py") __all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')] @@ -19,37 +14,20 @@ UI_GRAPHICAL = 0x03 UI_WEB = 0x04 UI_MODES = [UI_MENU, UI_TEXT, UI_GRAPHICAL, UI_WEB] -COLORMODE_MONO = 1 -COLORMODE_16 = 16 -COLORMODE_88 = 88 -COLORMODE_256 = 256 -COLORMODE_TRUE = 2**24 -THEME_DARK = 0x01 -THEME_LIGHT = 0x02 - -THEMES = { - THEME_DARK: [ - # Style name # 16-color style # Monochrome style # 88, 256 and true-color style - ('heading', 'light gray,underline', 'default', 'underline', 'g93,underline', 'default'), - ('menubar', 'black', 'light gray', 'standout', '#111', '#bbb'), - ('shortcutbar', 'black', 'light gray', 'standout', '#111', '#bbb'), - ('body_text', 'white', 'default', 'default', '#0a0', 'default'), - ('buttons', 'light green,bold', 'default', 'default', '#00a533', 'default'), - ('msg_editor', 'black', 'light cyan', 'standout', '#111', '#0bb'), - ("list_focus", "black", "light cyan", "standout", "#111", "#0bb"), - ] -} - def spawn(uimode): if uimode in UI_MODES: RNS.log("Starting user interface...", RNS.LOG_INFO) if uimode == UI_MENU: + from .MenuUI import MenuUI return MenuUI() elif uimode == UI_TEXT: + from .TextUI import TextUI return TextUI() elif uimode == UI_GRAPHICAL: + from .GraphicalUI import GraphicalUI return GraphicalUI() elif uimode == UI_WEB: + from .WebUI import WebUI return WebUI() else: return None diff --git a/nomadnet/ui/textui/Conversations.py b/nomadnet/ui/textui/Conversations.py index 59fd0ac..921d840 100644 --- a/nomadnet/ui/textui/Conversations.py +++ b/nomadnet/ui/textui/Conversations.py @@ -1,3 +1,7 @@ +import RNS +import time +import nomadnet + class ConversationsDisplayShortcuts(): def __init__(self, app): import urwid @@ -6,6 +10,9 @@ class ConversationsDisplayShortcuts(): self.widget = urwid.AttrMap(urwid.Text("Conversations Display Shortcuts"), "shortcutbar") class ConversationsDisplay(): + list_width = 0.33 + cached_conversation_widgets = {} + def __init__(self, app): import urwid from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox @@ -14,26 +21,104 @@ class ConversationsDisplay(): conversation_list_widgets = [] for conversation in app.conversations(): - widget = urwid.SelectableIcon(str(conversation), cursor_position=-1) - widget.conversation = conversation - conversation_list_widgets.append(urwid.AttrMap(widget, None, "list_focus")) + conversation_list_widgets.append(self.conversation_list_widget(conversation)) walker = urwid.SimpleFocusListWalker(conversation_list_widgets) listbox = urwid.LineBox(urwid.Filler(IndicativeListBox(conversation_list_widgets), height=("relative", 100))) - placeholder = urwid.Text("Conversation Display Area", "left") - - conversation_area = urwid.LineBox( - urwid.Frame( - urwid.Filler(placeholder,"top"), - footer=urwid.AttrMap(urwid.Edit(caption="\u270E", edit_text="Message input"), "msg_editor") - ) - ) - - columns_widget = urwid.Columns([("weight", 0.33, listbox), ("weight", 0.67, conversation_area)], dividechars=0, focus_column=0, box_columns=[0]) + columns_widget = urwid.Columns([("weight", ConversationsDisplay.list_width, listbox), ("weight", 1-ConversationsDisplay.list_width, self.make_conversation_widget(None))], dividechars=0, focus_column=0, box_columns=[0]) self.shortcuts_display = ConversationsDisplayShortcuts(self.app) self.widget = columns_widget + def display_conversation(self, sender=None, source_hash=None): + options = self.widget.options("weight", 1-ConversationsDisplay.list_width) + self.widget.contents[1] = (self.make_conversation_widget(source_hash), options) + + + def make_conversation_widget(self, source_hash): + time_format = self.app.time_format + import urwid + class LXMessageWidget(urwid.WidgetWrap): + def __init__(self, message): + title_string = time.strftime(time_format) + if message.get_title() != "": + title_string += " | " + message.get_title() + if message.signature_validated(): + header_style = "msg_header_ok" + else: + header_style = "msg_header_caution" + title_string = "\u26A0 "+message.get_signature_description() + "\n" + title_string + + title = urwid.AttrMap(urwid.Text(title_string), header_style) + + display_widget = urwid.Pile([ + title, + urwid.Text(message.get_content()), + urwid.Text("") + ]) + + urwid.WidgetWrap.__init__(self, display_widget) + + if source_hash == None: + return urwid.LineBox(urwid.Filler(urwid.Text("No conversation selected"), "top")) + else: + if source_hash in ConversationsDisplay.cached_conversation_widgets: + return ConversationsDisplay.cached_conversation_widgets[source_hash] + else: + conversation = nomadnet.Conversation(source_hash, self.app) + message_widgets = [] + + for message in conversation.messages: + message_widget = LXMessageWidget(message) + message_widgets.append(message_widget) + + + from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox + messagelist = IndicativeListBox(message_widgets) + widget = urwid.LineBox( + urwid.Frame( + messagelist, + footer=urwid.AttrMap(urwid.Edit(caption="\u270E", edit_text=""), "msg_editor") + ) + ) + + ConversationsDisplay.cached_conversation_widgets[source_hash] = widget + return widget + + + def conversation_list_widget(self, conversation): + import urwid + + 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 + + #widget = urwid.SelectableIcon(str(conversation), cursor_position=-1) + widget = ListEntry(str(conversation)) + urwid.connect_signal(widget, "click", self.display_conversation, conversation) + return urwid.AttrMap(widget, None, "list_focus") + + def shortcuts(self): return self.shortcuts_display \ No newline at end of file diff --git a/nomadnet/ui/textui/Extras.py b/nomadnet/ui/textui/Extras.py index d07e6b3..ce62430 100644 --- a/nomadnet/ui/textui/Extras.py +++ b/nomadnet/ui/textui/Extras.py @@ -1,5 +1,3 @@ -from nomadnet.ui import * - class IntroDisplay(): def __init__(self, app): import urwid