From 84b214cb909abdb507fd91338da5930b3f16c943 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 20 Jan 2025 14:25:58 +0100 Subject: [PATCH] Added markdown rendering and message composing --- sbapp/buildozer.spec | 2 +- sbapp/main.py | 54 +++++++++++++++++++++++++++++++++++++++++- sbapp/sideband/core.py | 15 +++++++++--- sbapp/ui/layouts.py | 16 +++++++++++++ sbapp/ui/messages.py | 25 +++++++++++++------ setup.py | 2 ++ 6 files changed, 102 insertions(+), 12 deletions(-) diff --git a/sbapp/buildozer.spec b/sbapp/buildozer.spec index 01fea20..24721b1 100644 --- a/sbapp/buildozer.spec +++ b/sbapp/buildozer.spec @@ -12,7 +12,7 @@ version.regex = __version__ = ['"](.*)['"] version.filename = %(source.dir)s/main.py android.numeric_version = 20250120 -requirements = kivy==2.3.0,libbz2,pillow==10.2.0,qrcode==7.3.1,usb4a,usbserial4a,able_recipe,libwebp,libogg,libopus,opusfile,numpy,cryptography,ffpyplayer,codec2,pycodec2,sh,pynacl,typing-extensions +requirements = kivy==2.3.0,libbz2,pillow==10.2.0,qrcode==7.3.1,usb4a,usbserial4a,able_recipe,libwebp,libogg,libopus,opusfile,numpy,cryptography,ffpyplayer,codec2,pycodec2,sh,pynacl,typing-extensions,mistune>=3.0.2,beautifulsoup4 android.gradle_dependencies = com.android.support:support-compat:28.0.0 #android.enable_androidx = True diff --git a/sbapp/main.py b/sbapp/main.py index 21ff373..6eb4294 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -19,6 +19,7 @@ import RNS import LXMF import time import os +import re import pathlib import base64 import threading @@ -1523,6 +1524,50 @@ class SidebandApp(MDApp): ### Messages (conversation) screen ###################################### + + def md_to_bbcode(self, text): + if not hasattr(self, "mdconv"): + from md2bbcode.main import process_readme as mdconv + self.mdconv = mdconv + converted = self.mdconv(text) + while converted.endswith("\n"): + converted = converted[:-1] + + return converted + + def process_bb_markup(self, text): + st = time.time() + ms = int(sp(14)) + h1s = int(sp(20)) + h2s = int(sp(18)) + h3s = int(sp(16)) + + if not hasattr(self, "pres"): + self.pres = [] + res = [ [r"\[(?:code|icode).*?\]", f"[font=mono][size={ms}]"], + [r"\[\/(?:code|icode).*?\]", "[/size][/font]"], + [r"\[(?:heading)\]", f"[b][size={h1s}]"], + [r"\[(?:heading=1)*?\]", f"[b][size={h1s}]"], + [r"\[(?:heading=2)*?\]", f"[b][size={h2s}]"], + [r"\[(?:heading=3)*?\]", f"[b][size={h3s}]"], + [r"\[(?:heading=).*?\]", f"[b][size={h3s}]"], # Match all remaining lower-level headings + [r"\[\/(?:heading).*?\]", "[/size][/b]"], + [r"\[(?:list).*?\]", ""], + [r"\[\/(?:list).*?\]", ""], + [r"\n\[(?:\*).*?\]", "\n - "], + [r"\[(?:url).*?\]", ""], # Strip URLs for now + [r"\[\/(?:url).*?\]", ""], + [r"\[(?:img).*?\].*\[\/(?:img).*?\]", ""] # Strip images for now + ] + + for r in res: + self.pres.append([re.compile(r[0], re.IGNORECASE | re.MULTILINE ), r[1]]) + + for pr in self.pres: + text = pr[0].sub(pr[1], text) + + return text + def conversation_from_announce_action(self, context_dest): if self.sideband.has_conversation(context_dest): pass @@ -2758,7 +2803,7 @@ class SidebandApp(MDApp): str_comps = " - [b]Reticulum[/b] (MIT License)\n - [b]LXMF[/b] (MIT License)\n - [b]KivyMD[/b] (MIT License)" str_comps += "\n - [b]Kivy[/b] (MIT License)\n - [b]Codec2[/b] (LGPL License)\n - [b]PyCodec2[/b] (BSD-3 License)" - str_comps += "\n - [b]PyDub[/b] (MIT License)\n - [b]PyOgg[/b] (Public Domain)" + str_comps += "\n - [b]PyDub[/b] (MIT License)\n - [b]PyOgg[/b] (Public Domain)\n - [b]MD2bbcode[/b] (GPL3 License)" str_comps += "\n - [b]GeoidHeight[/b] (LGPL License)\n - [b]Python[/b] (PSF License)" str_comps += "\n\nGo to [u][ref=link]https://unsigned.io/donate[/ref][/u] to support the project.\n\nThe Sideband app is Copyright © 2025 Mark Qvist / unsigned.io\n\nPermission is granted to freely share and distribute binary copies of "+self.root.ids.app_version_info.text+", so long as no payment or compensation is charged for said distribution or sharing.\n\nIf you were charged or paid anything for this copy of Sideband, please report it to [b]license@unsigned.io[/b].\n\nTHIS IS EXPERIMENTAL SOFTWARE - SIDEBAND COMES WITH ABSOLUTELY NO WARRANTY - USE AT YOUR OWN RISK AND RESPONSIBILITY" info = "This is "+self.root.ids.app_version_info.text+", on RNS v"+RNS.__version__+" and LXMF v"+LXMF.__version__+".\n\nHumbly build using the following open components:\n\n"+str_comps @@ -3041,6 +3086,10 @@ class SidebandApp(MDApp): self.sideband.config["trusted_markup_only"] = self.settings_screen.ids.settings_trusted_markup_only.active self.sideband.save_configuration() + def save_compose_in_markdown(sender=None, event=None): + self.sideband.config["compose_in_markdown"] = self.settings_screen.ids.settings_compose_in_markdown.active + self.sideband.save_configuration() + def save_advanced_stats(sender=None, event=None): self.sideband.config["advanced_stats"] = self.settings_screen.ids.settings_advanced_statistics.active self.sideband.save_configuration() @@ -3219,6 +3268,9 @@ class SidebandApp(MDApp): self.settings_screen.ids.settings_trusted_markup_only.active = self.sideband.config["trusted_markup_only"] self.settings_screen.ids.settings_trusted_markup_only.bind(active=save_trusted_markup_only) + self.settings_screen.ids.settings_compose_in_markdown.active = self.sideband.config["compose_in_markdown"] + self.settings_screen.ids.settings_compose_in_markdown.bind(active=save_compose_in_markdown) + self.settings_screen.ids.settings_ignore_invalid_stamps.active = self.sideband.config["lxmf_ignore_invalid_stamps"] self.settings_screen.ids.settings_ignore_invalid_stamps.bind(active=save_lxmf_ignore_invalid_stamps) diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index b68186b..c4c3669 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -457,6 +457,7 @@ class SidebandCore(): self.config["eink_mode"] = True self.config["lxm_limit_1mb"] = True self.config["trusted_markup_only"] = False + self.config["compose_in_markdown"] = False # Connectivity self.config["connect_transport"] = False @@ -601,6 +602,8 @@ class SidebandCore(): self.config["hq_ptt"] = False if not "trusted_markup_only" in self.config: self.config["trusted_markup_only"] = False + if not "compose_in_markdown" in self.config: + self.config["compose_in_markdown"] = False if not "input_language" in self.config: self.config["input_language"] = None @@ -4396,7 +4399,13 @@ class SidebandCore(): fields[LXMF.FIELD_IMAGE] = image if audio != None: fields[LXMF.FIELD_AUDIO] = audio - if self.has_bb_markup(content): + md_sig = "#!md\n" + if content.startswith(md_sig): + content = content[len(md_sig):] + fields[LXMF.FIELD_RENDERER] = LXMF.RENDERER_MARKDOWN + elif self.config["compose_in_markdown"]: + fields[LXMF.FIELD_RENDERER] = LXMF.RENDERER_MARKDOWN + elif self.has_bb_markup(content): fields[LXMF.FIELD_RENDERER] = LXMF.RENDERER_BBCODE lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = fields, include_ticket=self.is_trusted(destination_hash)) @@ -4538,12 +4547,12 @@ class SidebandCore(): def strip_bb_markup(self, text): if not hasattr(self, "smr") or self.smr == None: - self.smr = re.compile(r'\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]',re.IGNORECASE | re.MULTILINE ) + self.smr = re.compile(r"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]",re.IGNORECASE | re.MULTILINE ) return self.smr.sub("", text) def has_bb_markup(self, text): if not hasattr(self, "smr") or self.smr == None: - self.smr = re.compile(r'\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]',re.IGNORECASE | re.MULTILINE ) + self.smr = re.compile(r"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]",re.IGNORECASE | re.MULTILINE ) if self.smr.match(text): return True else: diff --git a/sbapp/ui/layouts.py b/sbapp/ui/layouts.py index 4e6ab43..6155953 100644 --- a/sbapp/ui/layouts.py +++ b/sbapp/ui/layouts.py @@ -1655,6 +1655,22 @@ MDScreen: disabled: False active: False + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + padding: [0,0,dp(24),dp(0)] + height: dp(48) + + MDLabel: + text: "Compose messages in markdown" + font_style: "H6" + + MDSwitch: + id: settings_compose_in_markdown + pos_hint: {"center_y": 0.3} + disabled: False + active: False + MDBoxLayout: orientation: "horizontal" size_hint_y: None diff --git a/sbapp/ui/messages.py b/sbapp/ui/messages.py index b884b76..87557b0 100644 --- a/sbapp/ui/messages.py +++ b/sbapp/ui/messages.py @@ -110,8 +110,6 @@ class Messages(): msg = self.app.sideband.message(lxm_hash) if msg: close_button = MDRectangleFlatButton(text="Close", font_size=dp(18)) - # d_items = [ ] - # d_items.append(DialogItem(IconLeftWidget(icon="postage-stamp"), text="[size="+str(ss)+"]Stamp[/size]")) d_text = "" @@ -492,11 +490,24 @@ class Messages(): for m in self.new_messages: if not m["hash"] in self.added_item_hashes: + renderer = None + message_source = m["content"] + if "lxm" in m and m["lxm"] and m["lxm"].fields != None and LXMF.FIELD_RENDERER in m["lxm"].fields: + renderer = m["lxm"].fields[LXMF.FIELD_RENDERER] + try: if self.app.sideband.config["trusted_markup_only"] and not self.is_trusted: message_input = str( escape_markup(m["content"].decode("utf-8")) ).encode("utf-8") else: message_input = m["content"] + if renderer == LXMF.RENDERER_MARKDOWN: + message_input = self.app.md_to_bbcode(message_input.decode("utf-8")).encode("utf-8") + message_input = self.app.process_bb_markup(message_input.decode("utf-8")).encode("utf-8") + elif renderer == LXMF.RENDERER_BBCODE: + message_input = self.app.process_bb_markup(message_input.decode("utf-8")).encode("utf-8") + else: + message_input = str(escape_markup(m["content"].decode("utf-8"))).encode("utf-8") + except Exception as e: RNS.log(f"Message content could not be decoded: {e}", RNS.LOG_DEBUG) message_input = b"" @@ -1144,7 +1155,7 @@ class Messages(): "viewclass": "OneLineListItem", "text": "Copy message text", "height": dp(40), - "on_release": gen_copy(message_input.decode("utf-8"), item) + "on_release": gen_copy(message_source.decode("utf-8"), item) }, { "text": "Delete", @@ -1178,7 +1189,7 @@ class Messages(): "viewclass": "OneLineListItem", "text": "Copy message text", "height": dp(40), - "on_release": gen_copy(message_input.decode("utf-8"), item) + "on_release": gen_copy(message_source.decode("utf-8"), item) }, { "text": "Delete", @@ -1196,7 +1207,7 @@ class Messages(): "viewclass": "OneLineListItem", "text": "Copy", "height": dp(40), - "on_release": gen_copy(message_input.decode("utf-8"), item) + "on_release": gen_copy(message_source.decode("utf-8"), item) }, { "text": "Delete", @@ -1213,7 +1224,7 @@ class Messages(): "viewclass": "OneLineListItem", "text": "Copy", "height": dp(40), - "on_release": gen_copy(message_input.decode("utf-8"), item) + "on_release": gen_copy(message_source.decode("utf-8"), item) }, { "viewclass": "OneLineListItem", @@ -1236,7 +1247,7 @@ class Messages(): "viewclass": "OneLineListItem", "text": "Copy", "height": dp(40), - "on_release": gen_copy(message_input.decode("utf-8"), item) + "on_release": gen_copy(message_source.decode("utf-8"), item) }, { "text": "Delete", diff --git a/setup.py b/setup.py index e6b2cf7..6a5f879 100644 --- a/setup.py +++ b/setup.py @@ -123,6 +123,8 @@ setuptools.setup( "ffpyplayer", "sh", "numpy<=1.26.4", + "mistune>=3.0.2", + "beautifulsoup4", "pycodec2;sys.platform!='Windows' and sys.platform!='win32' and sys.platform!='darwin'", "pyaudio;sys.platform=='linux'", "pyobjus;sys.platform=='darwin'",