Added markdown rendering and message composing

This commit is contained in:
Mark Qvist 2025-01-20 14:25:58 +01:00
parent a90a451865
commit 84b214cb90
6 changed files with 102 additions and 12 deletions

View File

@ -12,7 +12,7 @@ version.regex = __version__ = ['"](.*)['"]
version.filename = %(source.dir)s/main.py version.filename = %(source.dir)s/main.py
android.numeric_version = 20250120 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.gradle_dependencies = com.android.support:support-compat:28.0.0
#android.enable_androidx = True #android.enable_androidx = True

View File

@ -19,6 +19,7 @@ import RNS
import LXMF import LXMF
import time import time
import os import os
import re
import pathlib import pathlib
import base64 import base64
import threading import threading
@ -1523,6 +1524,50 @@ class SidebandApp(MDApp):
### Messages (conversation) screen ### 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): def conversation_from_announce_action(self, context_dest):
if self.sideband.has_conversation(context_dest): if self.sideband.has_conversation(context_dest):
pass 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 = " - [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]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 - [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" 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 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.config["trusted_markup_only"] = self.settings_screen.ids.settings_trusted_markup_only.active
self.sideband.save_configuration() 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): def save_advanced_stats(sender=None, event=None):
self.sideband.config["advanced_stats"] = self.settings_screen.ids.settings_advanced_statistics.active self.sideband.config["advanced_stats"] = self.settings_screen.ids.settings_advanced_statistics.active
self.sideband.save_configuration() 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.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_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.active = self.sideband.config["lxmf_ignore_invalid_stamps"]
self.settings_screen.ids.settings_ignore_invalid_stamps.bind(active=save_lxmf_ignore_invalid_stamps) self.settings_screen.ids.settings_ignore_invalid_stamps.bind(active=save_lxmf_ignore_invalid_stamps)

View File

@ -457,6 +457,7 @@ class SidebandCore():
self.config["eink_mode"] = True self.config["eink_mode"] = True
self.config["lxm_limit_1mb"] = True self.config["lxm_limit_1mb"] = True
self.config["trusted_markup_only"] = False self.config["trusted_markup_only"] = False
self.config["compose_in_markdown"] = False
# Connectivity # Connectivity
self.config["connect_transport"] = False self.config["connect_transport"] = False
@ -601,6 +602,8 @@ class SidebandCore():
self.config["hq_ptt"] = False self.config["hq_ptt"] = False
if not "trusted_markup_only" in self.config: if not "trusted_markup_only" in self.config:
self.config["trusted_markup_only"] = False 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: if not "input_language" in self.config:
self.config["input_language"] = None self.config["input_language"] = None
@ -4396,7 +4399,13 @@ class SidebandCore():
fields[LXMF.FIELD_IMAGE] = image fields[LXMF.FIELD_IMAGE] = image
if audio != None: if audio != None:
fields[LXMF.FIELD_AUDIO] = audio 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 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)) 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): def strip_bb_markup(self, text):
if not hasattr(self, "smr") or self.smr == None: 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) return self.smr.sub("", text)
def has_bb_markup(self, text): def has_bb_markup(self, text):
if not hasattr(self, "smr") or self.smr == None: 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): if self.smr.match(text):
return True return True
else: else:

View File

@ -1655,6 +1655,22 @@ MDScreen:
disabled: False disabled: False
active: 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: MDBoxLayout:
orientation: "horizontal" orientation: "horizontal"
size_hint_y: None size_hint_y: None

View File

@ -110,8 +110,6 @@ class Messages():
msg = self.app.sideband.message(lxm_hash) msg = self.app.sideband.message(lxm_hash)
if msg: if msg:
close_button = MDRectangleFlatButton(text="Close", font_size=dp(18)) 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 = "" d_text = ""
@ -492,11 +490,24 @@ class Messages():
for m in self.new_messages: for m in self.new_messages:
if not m["hash"] in self.added_item_hashes: 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: try:
if self.app.sideband.config["trusted_markup_only"] and not self.is_trusted: 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") message_input = str( escape_markup(m["content"].decode("utf-8")) ).encode("utf-8")
else: else:
message_input = m["content"] 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: except Exception as e:
RNS.log(f"Message content could not be decoded: {e}", RNS.LOG_DEBUG) RNS.log(f"Message content could not be decoded: {e}", RNS.LOG_DEBUG)
message_input = b"" message_input = b""
@ -1144,7 +1155,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy message text", "text": "Copy message text",
"height": dp(40), "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", "text": "Delete",
@ -1178,7 +1189,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy message text", "text": "Copy message text",
"height": dp(40), "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", "text": "Delete",
@ -1196,7 +1207,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy", "text": "Copy",
"height": dp(40), "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", "text": "Delete",
@ -1213,7 +1224,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy", "text": "Copy",
"height": dp(40), "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", "viewclass": "OneLineListItem",
@ -1236,7 +1247,7 @@ class Messages():
"viewclass": "OneLineListItem", "viewclass": "OneLineListItem",
"text": "Copy", "text": "Copy",
"height": dp(40), "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", "text": "Delete",

View File

@ -123,6 +123,8 @@ setuptools.setup(
"ffpyplayer", "ffpyplayer",
"sh", "sh",
"numpy<=1.26.4", "numpy<=1.26.4",
"mistune>=3.0.2",
"beautifulsoup4",
"pycodec2;sys.platform!='Windows' and sys.platform!='win32' and sys.platform!='darwin'", "pycodec2;sys.platform!='Windows' and sys.platform!='win32' and sys.platform!='darwin'",
"pyaudio;sys.platform=='linux'", "pyaudio;sys.platform=='linux'",
"pyobjus;sys.platform=='darwin'", "pyobjus;sys.platform=='darwin'",