Sideband/sbapp/ui/messages.py

1140 lines
51 KiB
Python
Raw Normal View History

2022-04-07 15:03:53 -04:00
import time
import RNS
import LXMF
from kivy.metrics import dp,sp
2022-04-07 15:03:53 -04:00
from kivy.core.clipboard import Clipboard
2024-03-10 13:14:04 -04:00
from kivy.core.image import Image as CoreImage
2022-04-07 15:03:53 -04:00
from kivymd.uix.card import MDCard
from kivymd.uix.menu import MDDropdownMenu
2022-10-08 12:01:33 -04:00
# from kivymd.uix.behaviors import RoundedRectangularElevationBehavior, FakeRectangularElevationBehavior
from kivymd.uix.behaviors import CommonElevationBehavior
2022-04-07 15:03:53 -04:00
from kivy.properties import StringProperty, BooleanProperty
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
2024-01-04 19:49:25 -05:00
from kivy.utils import escape_markup
2022-04-07 15:03:53 -04:00
2022-11-23 07:28:26 -05:00
from kivymd.uix.button import MDRectangleFlatButton, MDRectangleFlatIconButton
2022-04-07 15:03:53 -04:00
from kivymd.uix.dialog import MDDialog
2024-01-04 19:49:25 -05:00
if RNS.vendor.platformutils.get_platform() == "android":
from ui.helpers import multilingual_markup
else:
from .helpers import multilingual_markup
2024-03-10 13:14:04 -04:00
import io
2022-11-22 08:25:56 -05:00
import os
import subprocess
import shlex
2024-03-10 11:36:41 -04:00
from kivy.graphics.opengl import glGetIntegerv, GL_MAX_TEXTURE_SIZE
2022-07-07 16:16:10 -04:00
if RNS.vendor.platformutils.get_platform() == "android":
2024-06-02 12:33:46 -04:00
import plyer
2023-10-29 21:28:35 -04:00
from sideband.sense import Telemeter, Commands
2022-11-22 08:25:56 -05:00
from ui.helpers import ts_format, file_ts_format, mdc
2024-06-02 19:53:54 -04:00
from ui.helpers import color_playing, color_received, color_delivered, color_propagated, color_paper, color_failed, color_unknown, intensity_msgs_dark, intensity_msgs_light, intensity_play_dark, intensity_play_light
2022-07-07 16:16:10 -04:00
else:
2024-06-02 12:33:46 -04:00
import sbapp.plyer as plyer
2023-10-29 21:28:35 -04:00
from sbapp.sideband.sense import Telemeter, Commands
2022-11-22 08:25:56 -05:00
from .helpers import ts_format, file_ts_format, mdc
2024-06-02 19:53:54 -04:00
from .helpers import color_playing, color_received, color_delivered, color_propagated, color_paper, color_failed, color_unknown, intensity_msgs_dark, intensity_msgs_light, intensity_play_dark, intensity_play_light
2022-04-07 15:03:53 -04:00
if RNS.vendor.platformutils.is_darwin():
from PIL import Image as PilImage
2023-10-22 08:01:31 -04:00
from kivy.lang.builder import Builder
2022-10-08 12:01:33 -04:00
class ListLXMessageCard(MDCard):
# class ListLXMessageCard(MDCard, FakeRectangularElevationBehavior):
2022-04-07 15:03:53 -04:00
text = StringProperty()
heading = StringProperty()
class Messages():
def __init__(self, app, context_dest):
self.app = app
self.context_dest = context_dest
2023-10-23 19:14:59 -04:00
self.source_dest = context_dest
self.is_trusted = self.app.sideband.is_trusted(self.context_dest)
self.ptt_enabled = self.app.sideband.ptt_enabled(self.context_dest)
2023-10-22 12:08:28 -04:00
self.screen = self.app.root.ids.screen_manager.get_screen("messages_screen")
self.ids = self.screen.ids
2024-03-10 11:36:41 -04:00
self.max_texture_size = glGetIntegerv(GL_MAX_TEXTURE_SIZE)[0]
self.new_messages = []
2022-04-07 15:03:53 -04:00
self.added_item_hashes = []
self.added_messages = 0
2022-04-07 15:03:53 -04:00
self.latest_message_timestamp = None
self.earliest_message_timestamp = time.time()
self.loading_earlier_messages = False
2022-04-07 15:03:53 -04:00
self.list = None
self.widgets = []
self.send_error_dialog = None
2022-11-23 07:28:26 -05:00
self.load_more_button = None
2022-04-07 15:03:53 -04:00
self.update()
def reload(self):
if self.list != None:
self.list.clear_widgets()
self.new_messages = []
2022-04-07 15:03:53 -04:00
self.added_item_hashes = []
self.added_messages = 0
2022-04-07 15:03:53 -04:00
self.latest_message_timestamp = None
self.widgets = []
self.update()
def load_more(self, dt):
for new_message in self.app.sideband.list_messages(self.context_dest, before=self.earliest_message_timestamp,limit=5):
self.new_messages.append(new_message)
if len(self.new_messages) > 0:
self.loading_earlier_messages = True
self.list.remove_widget(self.load_more_button)
2022-11-23 07:28:26 -05:00
def update(self, limit=8):
for new_message in self.app.sideband.list_messages(self.context_dest, after=self.latest_message_timestamp,limit=limit):
self.new_messages.append(new_message)
2022-11-23 07:28:26 -05:00
self.db_message_count = self.app.sideband.count_messages(self.context_dest)
if self.load_more_button == None:
self.load_more_button = MDRectangleFlatIconButton(
icon="message-text-clock-outline",
text="Load earlier messages",
font_size=dp(18),
theme_text_color="Custom",
size_hint=[1.0, None],
)
def lmcb(sender):
Clock.schedule_once(self.load_more, 0.15)
self.load_more_button.bind(on_release=lmcb)
2022-11-23 07:28:26 -05:00
2022-04-07 15:03:53 -04:00
if self.list == None:
2022-10-02 11:17:55 -04:00
layout = GridLayout(cols=1, spacing=dp(16), padding=dp(16), size_hint_y=None)
2022-04-07 15:03:53 -04:00
layout.bind(minimum_height=layout.setter('height'))
self.list = layout
2022-11-23 07:28:26 -05:00
2024-09-07 03:44:07 -04:00
if self.ptt_enabled:
self.hide_widget(self.ids.message_ptt, False)
else:
self.hide_widget(self.ids.message_ptt, True)
2022-11-23 07:28:26 -05:00
c_ts = time.time()
if len(self.new_messages) > 0:
2022-04-07 15:03:53 -04:00
self.update_widget()
if (len(self.added_item_hashes) < self.db_message_count) and not self.load_more_button in self.list.children:
self.list.add_widget(self.load_more_button, len(self.list.children))
2022-04-07 15:03:53 -04:00
2022-10-02 08:51:01 -04:00
if self.app.sideband.config["dark_ui"]:
intensity_msgs = intensity_msgs_dark
2024-06-02 19:53:54 -04:00
intensity_play = intensity_play_dark
2022-10-02 08:51:01 -04:00
else:
intensity_msgs = intensity_msgs_light
2024-06-02 19:53:54 -04:00
intensity_play = intensity_play_light
2022-10-02 08:51:01 -04:00
2022-04-07 15:03:53 -04:00
for w in self.widgets:
m = w.m
2022-10-08 12:01:33 -04:00
if self.app.sideband.config["dark_ui"]:
w.line_color = (1.0, 1.0, 1.0, 0.25)
else:
w.line_color = (1.0, 1.0, 1.0, 0.5)
2022-04-07 15:03:53 -04:00
if m["state"] == LXMF.LXMessage.SENDING or m["state"] == LXMF.LXMessage.OUTBOUND:
msg = self.app.sideband.message(m["hash"])
if msg["state"] == LXMF.LXMessage.OUTBOUND or msg["state"] == LXMF.LXMessage.SENDING:
w.md_bg_color = msg_color = mdc(color_unknown, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
prgstr = ""
2024-03-19 08:45:26 -04:00
sphrase = "Sending"
prg = self.app.sideband.get_lxm_progress(msg["hash"])
if prg != None:
prgstr = ", "+str(round(prg*100, 1))+"% done"
2024-03-19 08:45:26 -04:00
if prg <= 0.00:
stamp_cost = self.app.sideband.get_lxm_stamp_cost(msg["hash"])
if stamp_cost:
sphrase = f"Generating stamp with cost {stamp_cost}"
prgstr = ""
else:
sphrase = "Waiting for path"
elif prg <= 0.01:
2024-03-19 08:45:26 -04:00
sphrase = "Waiting for path"
elif prg <= 0.03:
sphrase = "Establishing link"
elif prg <= 0.05:
sphrase = "Link established"
elif prg >= 0.05:
sphrase = "Sending"
if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
2024-03-19 08:45:26 -04:00
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] "+sphrase+prgstr+" "
2024-06-02 19:53:54 -04:00
if w.has_audio:
2024-06-03 20:57:05 -04:00
alstr = RNS.prettysize(w.audio_size)
w.heading += f"\n[b]Audio Message[/b] ({alstr})"
m["state"] = msg["state"]
2024-06-02 19:53:54 -04:00
2022-04-07 15:03:53 -04:00
if msg["state"] == LXMF.LXMessage.DELIVERED:
w.md_bg_color = msg_color = mdc(color_delivered, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Delivered"
2024-06-02 19:53:54 -04:00
if w.has_audio:
2024-06-03 20:57:05 -04:00
alstr = RNS.prettysize(w.audio_size)
w.heading += f"\n[b]Audio Message[/b] ({alstr})"
2022-04-07 15:03:53 -04:00
m["state"] = msg["state"]
2022-11-22 08:25:56 -05:00
if msg["method"] == LXMF.LXMessage.PAPER:
w.md_bg_color = msg_color = mdc(color_paper, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Paper Message"
m["state"] = msg["state"]
2022-04-07 15:03:53 -04:00
if msg["method"] == LXMF.LXMessage.PROPAGATED and msg["state"] == LXMF.LXMessage.SENT:
w.md_bg_color = msg_color = mdc(color_propagated, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] On Propagation Net"
2024-06-02 19:53:54 -04:00
if w.has_audio:
2024-06-03 20:57:05 -04:00
alstr = RNS.prettysize(w.audio_size)
w.heading += f"\n[b]Audio Message[/b] ({alstr})"
2022-04-07 15:03:53 -04:00
m["state"] = msg["state"]
if msg["state"] == LXMF.LXMessage.FAILED:
w.md_bg_color = msg_color = mdc(color_failed, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
if msg["title"]:
titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n"
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Failed"
2022-04-07 15:03:53 -04:00
m["state"] = msg["state"]
2024-06-02 19:53:54 -04:00
if w.has_audio:
2024-06-03 20:57:05 -04:00
alstr = RNS.prettysize(w.audio_size)
w.heading += f"\n[b]Audio Message[/b] ({alstr})"
2023-10-16 16:21:38 -04:00
w.dmenu.items.append(w.dmenu.retry_item)
2022-04-07 15:03:53 -04:00
def hide_widget(self, wid, dohide=True):
if hasattr(wid, 'saved_attrs'):
if not dohide:
wid.height, wid.size_hint_y, wid.opacity, wid.disabled = wid.saved_attrs
del wid.saved_attrs
elif dohide:
wid.saved_attrs = wid.height, wid.size_hint_y, wid.opacity, wid.disabled
wid.height, wid.size_hint_y, wid.opacity, wid.disabled = 0, None, 0, True
2022-04-07 15:03:53 -04:00
def update_widget(self):
2022-10-02 08:51:01 -04:00
if self.app.sideband.config["dark_ui"]:
intensity_msgs = intensity_msgs_dark
2024-06-02 19:53:54 -04:00
intensity_play = intensity_play_dark
2022-10-02 19:36:21 -04:00
mt_color = [1.0, 1.0, 1.0, 0.8]
2022-10-02 08:51:01 -04:00
else:
intensity_msgs = intensity_msgs_light
2024-06-02 19:53:54 -04:00
intensity_play = intensity_play_light
2022-10-02 19:36:21 -04:00
mt_color = [1.0, 1.0, 1.0, 0.95]
2022-10-02 08:51:01 -04:00
2024-01-05 12:05:34 -05:00
self.ids.message_text.font_name = self.app.input_font
if self.loading_earlier_messages:
self.new_messages.reverse()
for m in self.new_messages:
2022-04-07 15:03:53 -04:00
if not m["hash"] in self.added_item_hashes:
2024-01-04 19:49:25 -05:00
if not self.is_trusted:
message_input = str( escape_markup(m["content"].decode("utf-8")) ).encode("utf-8")
else:
message_input = m["content"]
if message_input.strip() == b"":
2024-06-04 18:13:00 -04:00
if not ("lxm" in m and m["lxm"] != None and m["lxm"].fields != None and LXMF.FIELD_COMMANDS in m["lxm"].fields):
message_input = "[i]This message contains no text content[/i]".encode("utf-8")
2024-01-04 19:49:25 -05:00
message_markup = multilingual_markup(message_input)
2022-04-07 15:03:53 -04:00
txstr = time.strftime(ts_format, time.localtime(m["sent"]))
rxstr = time.strftime(ts_format, time.localtime(m["received"]))
titlestr = ""
2023-10-29 21:28:35 -04:00
extra_content = ""
2023-10-21 19:12:13 -04:00
extra_telemetry = {}
telemeter = None
2024-03-10 13:14:04 -04:00
image_field = None
2024-06-02 19:53:54 -04:00
audio_field = None
2024-03-16 19:23:55 -04:00
has_image = False
2024-06-02 19:53:54 -04:00
has_audio = False
2024-03-16 19:23:55 -04:00
attachments_field = None
has_attachment = False
force_markup = False
2023-10-29 21:28:35 -04:00
signature_valid = False
if "lxm" in m and m["lxm"] != None and m["lxm"].signature_validated:
signature_valid = True
if "extras" in m and m["extras"] != None and "packed_telemetry" in m["extras"]:
try:
telemeter = Telemeter.from_packed(m["extras"]["packed_telemetry"])
except Exception as e:
pass
2023-10-29 21:28:35 -04:00
if "lxm" in m and m["lxm"] != None and m["lxm"].fields != None and LXMF.FIELD_COMMANDS in m["lxm"].fields:
try:
commands = m["lxm"].fields[LXMF.FIELD_COMMANDS]
for command in commands:
if Commands.ECHO in command:
extra_content = "[font=RobotoMono-Regular]> echo "+command[Commands.ECHO].decode("utf-8")+"[/font]\n"
if Commands.PING in command:
extra_content = "[font=RobotoMono-Regular]> ping[/font]\n"
if Commands.SIGNAL_REPORT in command:
extra_content = "[font=RobotoMono-Regular]> sig[/font]\n"
2024-03-24 19:58:58 -04:00
if Commands.PLUGIN_COMMAND in command:
cmd_content = command[Commands.PLUGIN_COMMAND]
extra_content = "[font=RobotoMono-Regular]> "+str(cmd_content)+"[/font]\n"
extra_content = extra_content[:-1]
force_markup = True
except Exception as e:
RNS.log("Error while generating command display: "+str(e), RNS.LOG_ERROR)
2023-10-29 21:28:35 -04:00
if telemeter == None and "lxm" in m and m["lxm"] and m["lxm"].fields != None and LXMF.FIELD_TELEMETRY in m["lxm"].fields:
try:
packed_telemetry = m["lxm"].fields[LXMF.FIELD_TELEMETRY]
telemeter = Telemeter.from_packed(packed_telemetry)
except Exception as e:
pass
2024-03-10 13:14:04 -04:00
if "lxm" in m and m["lxm"] and m["lxm"].fields != None and LXMF.FIELD_IMAGE in m["lxm"].fields:
try:
image_field = m["lxm"].fields[LXMF.FIELD_IMAGE]
2024-03-16 19:23:55 -04:00
has_image = True
2024-03-10 13:14:04 -04:00
except Exception as e:
pass
2024-06-02 19:53:54 -04:00
if "lxm" in m and m["lxm"] and m["lxm"].fields != None and LXMF.FIELD_AUDIO in m["lxm"].fields:
try:
audio_field = m["lxm"].fields[LXMF.FIELD_AUDIO]
has_audio = True
except Exception as e:
pass
2024-03-16 19:23:55 -04:00
if "lxm" in m and m["lxm"] and m["lxm"].fields != None and LXMF.FIELD_FILE_ATTACHMENTS in m["lxm"].fields:
if len(m["lxm"].fields[LXMF.FIELD_FILE_ATTACHMENTS]) > 0:
try:
attachments_field = m["lxm"].fields[LXMF.FIELD_FILE_ATTACHMENTS]
has_attachment = True
except Exception as e:
pass
rcvd_d_str = ""
trcvd = telemeter.read("received") if telemeter else None
if trcvd and "distance" in trcvd:
d = trcvd["distance"]
if "euclidian" in d:
edst = d["euclidian"]
if edst != None:
2023-10-28 14:13:39 -04:00
rcvd_d_str = "\n[b]Distance[/b] "+RNS.prettydistance(edst)
elif "geodesic" in d:
gdst = d["geodesic"]
if gdst != None:
2023-10-28 14:13:39 -04:00
rcvd_d_str = "\n[b]Distance[/b] "+RNS.prettydistance(gdst) + " (geodesic)"
2023-10-21 19:12:13 -04:00
phy_stats_str = ""
if "extras" in m and m["extras"] != None:
phy_stats = m["extras"]
if "q" in phy_stats:
try:
lq = round(float(phy_stats["q"]), 1)
phy_stats_str += "[b]Link Quality[/b] "+str(lq)+"% "
extra_telemetry["quality"] = lq
except:
pass
if "rssi" in phy_stats:
try:
lr = round(float(phy_stats["rssi"]), 1)
phy_stats_str += "[b]RSSI[/b] "+str(lr)+"dBm "
extra_telemetry["rssi"] = lr
except:
pass
if "snr" in phy_stats:
try:
ls = round(float(phy_stats["snr"]), 1)
phy_stats_str += "[b]SNR[/b] "+str(ls)+"dB "
extra_telemetry["snr"] = ls
except:
pass
2022-04-07 15:03:53 -04:00
if m["title"]:
titlestr = "[b]Title[/b] "+m["title"].decode("utf-8")+"\n"
if m["source"] == self.app.sideband.lxmf_destination.hash:
if m["state"] == LXMF.LXMessage.DELIVERED:
msg_color = mdc(color_delivered, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Delivered"
2022-04-07 15:03:53 -04:00
elif m["method"] == LXMF.LXMessage.PROPAGATED and m["state"] == LXMF.LXMessage.SENT:
msg_color = mdc(color_propagated, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] On Propagation Net"
2022-04-07 15:03:53 -04:00
2022-11-22 08:25:56 -05:00
elif m["method"] == LXMF.LXMessage.PAPER:
msg_color = mdc(color_paper, intensity_msgs)
heading_str = titlestr+"[b]Created[/b] "+txstr+"\n[b]State[/b] Paper Message"
2022-04-07 15:03:53 -04:00
elif m["state"] == LXMF.LXMessage.FAILED:
msg_color = mdc(color_failed, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Failed"
2022-04-07 15:03:53 -04:00
elif m["state"] == LXMF.LXMessage.OUTBOUND or m["state"] == LXMF.LXMessage.SENDING:
msg_color = mdc(color_unknown, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Sending "
2022-04-07 15:03:53 -04:00
else:
msg_color = mdc(color_unknown, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Unknown"
2022-04-07 15:03:53 -04:00
else:
2022-10-02 16:28:09 -04:00
msg_color = mdc(color_received, intensity_msgs)
2023-10-21 19:12:13 -04:00
heading_str = titlestr
if phy_stats_str != "" and self.app.sideband.config["advanced_stats"]:
heading_str += phy_stats_str+"\n"
2023-10-28 14:13:39 -04:00
heading_str += "[b]Sent[/b] "+txstr
heading_str += "\n[b]Received[/b] "+rxstr
2023-10-28 14:13:39 -04:00
if rcvd_d_str != "":
heading_str += rcvd_d_str
2023-10-29 21:28:35 -04:00
pre_content = ""
if not signature_valid:
2023-10-31 17:29:07 -04:00
identity_known = False
if RNS.Identity.recall(m["hash"]) != None:
identity_known = True
if identity_known == True:
pre_content += "[b]Warning![/b] The signature for this message could not be validated. [b]This message is likely to be fake[/b].\n\n"
force_markup = True
2023-10-29 21:28:35 -04:00
2024-03-16 19:23:55 -04:00
if has_attachment:
heading_str += "\n[b]Attachments[/b] "
for attachment in attachments_field:
heading_str += str(attachment[0])+", "
heading_str = heading_str[:-2]
2024-06-02 19:53:54 -04:00
if has_audio:
2024-06-03 20:57:05 -04:00
alstr = RNS.prettysize(len(audio_field[1]))
heading_str += f"\n[b]Audio Message[/b] ({alstr})"
2024-06-02 19:53:54 -04:00
2022-04-07 15:03:53 -04:00
item = ListLXMessageCard(
2024-01-04 19:49:25 -05:00
text=pre_content+message_markup.decode("utf-8")+extra_content,
2022-04-07 15:03:53 -04:00
heading=heading_str,
md_bg_color=msg_color,
)
2024-06-02 19:53:54 -04:00
item.lsource = m["source"]
2024-06-03 06:55:35 -04:00
item.has_audio = False
2024-03-10 11:36:41 -04:00
2024-03-16 19:23:55 -04:00
if has_attachment:
item.attachments_field = attachments_field
2024-06-02 19:53:54 -04:00
if has_audio:
def play_audio(sender):
self.app.play_audio_field(sender.audio_field)
stored_color = sender.md_bg_color
if sender.lsource == self.app.sideband.lxmf_destination.hash:
sender.md_bg_color = mdc(color_delivered, intensity_play)
else:
sender.md_bg_color = mdc(color_received, intensity_play)
def cb(dt):
sender.md_bg_color = stored_color
Clock.schedule_once(cb, 0.25)
item.has_audio = True
2024-06-03 20:57:05 -04:00
item.audio_size = len(audio_field[1])
2024-06-02 19:53:54 -04:00
item.audio_field = audio_field
item.bind(on_release=play_audio)
2024-03-10 13:14:04 -04:00
if image_field != None:
item.has_image = True
2024-03-16 19:23:55 -04:00
item.image_field = image_field
2024-03-10 13:14:04 -04:00
img = item.ids.message_image
img.source = ""
# Convert to PNG format on OSX, since support
# for webp seems rather flaky.
if RNS.vendor.platformutils.is_darwin():
im = PilImage.open(io.BytesIO(image_field[1]))
buf = io.BytesIO()
im.save(buf, format="png")
image_field[1] = buf.getvalue()
image_field[0] = "png"
2024-03-10 13:14:04 -04:00
img.texture = CoreImage(io.BytesIO(image_field[1]), ext=image_field[0]).texture
img.reload()
else:
item.has_image = False
def check_textures(w, val):
2024-03-10 11:36:41 -04:00
try:
2024-03-25 14:32:03 -04:00
if w.texture_size[0] > 360 and w.texture_size[1] >= self.max_texture_size:
2024-03-10 11:36:41 -04:00
w.text = "[i]The content of this message is too large to display in the message stream. You can copy the message content into another program by using the context menu of this message, and selecting [b]Copy[/b].[/i]"
2024-03-10 13:14:04 -04:00
if w.owner.has_image:
img = w.owner.ids.message_image
img.size_hint_x = 1
img.size_hint_y = None
img_w = w.owner.size[0]
img_ratio = img.texture_size[0] / img.texture_size[1]
img.size = (img_w,img_w/img_ratio)
img.fit_mode = "contain"
except Exception as e:
RNS.log("An error occurred while scaling message display textures:", RNS.LOG_ERROR)
RNS.trace_exception(e)
item.ids.content_text.owner = item
item.ids.content_text.bind(texture_size=check_textures)
2024-03-10 11:36:41 -04:00
2023-10-26 09:31:17 -04:00
if not RNS.vendor.platformutils.is_android():
item.radius = dp(5)
2022-04-07 15:03:53 -04:00
item.sb_uid = m["hash"]
item.m = m
2022-10-02 19:36:21 -04:00
item.ids.heading_text.theme_text_color = "Custom"
item.ids.heading_text.text_color = mt_color
item.ids.content_text.theme_text_color = "Custom"
item.ids.content_text.text_color = mt_color
item.ids.msg_submenu.theme_text_color = "Custom"
item.ids.msg_submenu.text_color = mt_color
2022-04-07 15:03:53 -04:00
def gen_del(mhash, item):
def x():
2022-10-13 16:12:39 -04:00
yes_button = MDRectangleFlatButton(text="Yes",font_size=dp(18), theme_text_color="Custom", line_color=self.app.color_reject, text_color=self.app.color_reject)
no_button = MDRectangleFlatButton(text="No",font_size=dp(18))
2022-04-07 15:03:53 -04:00
dialog = MDDialog(
title="Delete message?",
2022-04-07 15:03:53 -04:00
buttons=[ yes_button, no_button ],
2022-10-02 18:56:39 -04:00
# elevation=0,
2022-04-07 15:03:53 -04:00
)
def dl_yes(s):
dialog.dismiss()
self.app.sideband.delete_message(mhash)
def cb(dt):
self.reload()
Clock.schedule_once(cb, 0.2)
2022-04-07 15:03:53 -04:00
def dl_no(s):
dialog.dismiss()
yes_button.bind(on_release=dl_yes)
no_button.bind(on_release=dl_no)
item.dmenu.dismiss()
dialog.open()
return x
2023-10-16 16:21:38 -04:00
def gen_retry(mhash, mcontent, item):
def x():
2023-10-22 12:08:28 -04:00
self.app.messages_view.ids.message_text.text = mcontent.decode("utf-8")
2023-10-16 16:21:38 -04:00
self.app.sideband.delete_message(mhash)
self.app.message_send_action()
item.dmenu.dismiss()
def cb(dt):
self.reload()
Clock.schedule_once(cb, 0.2)
return x
2022-04-07 15:03:53 -04:00
def gen_copy(msg, item):
def x():
Clipboard.copy(msg)
item.dmenu.dismiss()
return x
2024-03-16 19:23:55 -04:00
def gen_save_image(item):
if RNS.vendor.platformutils.is_android():
def x():
image_field = item.image_field
extension = str(image_field[0]).replace(".", "")
filename = time.strftime("LXM_%Y_%m_%d_%H_%M_%S", time.localtime(time.time()))+"."+str(extension)
self.app.share_image(image_field[1], filename)
item.dmenu.dismiss()
return x
else:
def x():
image_field = item.image_field
try:
extension = str(image_field[0]).replace(".", "")
filename = time.strftime("LXM_%Y_%m_%d_%H_%M_%S", time.localtime(time.time()))+"."+str(extension)
if RNS.vendor.platformutils.is_darwin():
save_path = str(plyer.storagepath.get_downloads_dir()+filename).replace("file://", "")
else:
save_path = plyer.storagepath.get_downloads_dir()+"/"+filename
with open(save_path, "wb") as save_file:
save_file.write(image_field[1])
item.dmenu.dismiss()
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
dialog = MDDialog(
title="Image Saved",
text="The image has been saved to: "+save_path+"",
buttons=[ ok_button ],
# elevation=0,
)
def dl_ok(s):
dialog.dismiss()
ok_button.bind(on_release=dl_ok)
dialog.open()
except Exception as e:
item.dmenu.dismiss()
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
dialog = MDDialog(
title="Error",
text="Could not save the image:\n\n"+save_path+"\n\n"+str(e),
buttons=[ ok_button ],
# elevation=0,
)
def dl_ok(s):
dialog.dismiss()
ok_button.bind(on_release=dl_ok)
dialog.open()
item.dmenu.dismiss()
return x
def gen_save_attachment(item):
def x():
attachments_field = item.attachments_field
if isinstance(attachments_field, list):
try:
if RNS.vendor.platformutils.is_darwin():
output_path = str(plyer.storagepath.get_downloads_dir()).replace("file://", "")
else:
output_path = plyer.storagepath.get_downloads_dir()+"/"
if len(attachments_field) == 1:
saved_text = "The attached file has been saved to: "+output_path
saved_title = "Attachment Saved"
else:
saved_text = "The attached files have been saved to: "+output_path
saved_title = "Attachment Saved"
for attachment in attachments_field:
filename = str(attachment[0]).replace("../", "").replace("..\\", "")
if RNS.vendor.platformutils.is_darwin():
save_path = str(plyer.storagepath.get_downloads_dir()+filename).replace("file://", "")
else:
save_path = plyer.storagepath.get_downloads_dir()+"/"+filename
name_counter = 1
pre_count = save_path
while os.path.exists(save_path):
save_path = str(pre_count)+"."+str(name_counter)
name_counter += 1
saved_text = "The attached file has been saved to: "+save_path
with open(save_path, "wb") as save_file:
save_file.write(attachment[1])
item.dmenu.dismiss()
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
dialog = MDDialog(
title=saved_title,
text=saved_text,
buttons=[ ok_button ],
# elevation=0,
)
def dl_ok(s):
dialog.dismiss()
ok_button.bind(on_release=dl_ok)
dialog.open()
except Exception as e:
item.dmenu.dismiss()
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
dialog = MDDialog(
title="Error",
text="Could not save the attachment:\n\n"+save_path+"\n\n"+str(e),
buttons=[ ok_button ],
# elevation=0,
)
def dl_ok(s):
dialog.dismiss()
ok_button.bind(on_release=dl_ok)
dialog.open()
item.dmenu.dismiss()
return x
def gen_copy_telemetry(telemeter, extra_telemetry, item):
2023-10-20 17:38:28 -04:00
def x():
try:
telemeter
2023-10-21 19:12:13 -04:00
if extra_telemetry and len(extra_telemetry) != 0:
2023-10-23 19:14:59 -04:00
physical_link = extra_telemetry
telemeter.synthesize("physical_link")
if "rssi" in physical_link: telemeter.sensors["physical_link"].rssi = physical_link["rssi"]
if "snr" in physical_link: telemeter.sensors["physical_link"].snr = physical_link["snr"]
if "quality" in physical_link: telemeter.sensors["physical_link"].q = physical_link["quality"]
telemeter.sensors["physical_link"].update_data()
tlm = telemeter.read_all()
2023-10-21 19:12:13 -04:00
Clipboard.copy(str(tlm))
2023-10-20 17:38:28 -04:00
item.dmenu.dismiss()
except Exception as e:
RNS.log("An error occurred while decoding telemetry. The contained exception was: "+str(e), RNS.LOG_ERROR)
Clipboard.copy("Could not decode telemetry")
return x
2022-11-22 08:25:56 -05:00
def gen_copy_lxm_uri(lxm, item):
def x():
Clipboard.copy(lxm.as_uri())
item.dmenu.dismiss()
return x
def gen_save_qr(lxm, item):
if RNS.vendor.platformutils.is_android():
def x():
2022-11-22 13:47:13 -05:00
qr_image = lxm.as_qr()
hash_str = RNS.hexrep(lxm.hash[-2:], delimit=False)
filename = "Paper_Message_"+time.strftime(file_ts_format, time.localtime(m["sent"]))+"_"+hash_str+".png"
# filename = "Paper_Message.png"
self.app.share_image(qr_image, filename)
2022-11-22 08:25:56 -05:00
item.dmenu.dismiss()
return x
else:
def x():
try:
qr_image = lxm.as_qr()
hash_str = RNS.hexrep(lxm.hash[-2:], delimit=False)
filename = "Paper_Message_"+time.strftime(file_ts_format, time.localtime(m["sent"]))+"_"+hash_str+".png"
if RNS.vendor.platformutils.is_darwin():
save_path = str(plyer.storagepath.get_downloads_dir()+filename).replace("file://", "")
else:
save_path = plyer.storagepath.get_downloads_dir()+"/"+filename
2022-11-22 08:25:56 -05:00
qr_image.save(save_path)
item.dmenu.dismiss()
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
dialog = MDDialog(
title="QR Code Saved",
text="The paper message has been saved to: "+save_path+"",
buttons=[ ok_button ],
# elevation=0,
)
def dl_ok(s):
dialog.dismiss()
ok_button.bind(on_release=dl_ok)
dialog.open()
except Exception as e:
item.dmenu.dismiss()
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
dialog = MDDialog(
title="Error",
text="Could not save the paper message QR-code to:\n\n"+save_path+"\n\n"+str(e),
buttons=[ ok_button ],
# elevation=0,
)
def dl_ok(s):
dialog.dismiss()
ok_button.bind(on_release=dl_ok)
dialog.open()
return x
def gen_print_qr(lxm, item):
if RNS.vendor.platformutils.is_android():
def x():
item.dmenu.dismiss()
return x
else:
def x():
try:
qr_image = lxm.as_qr()
qr_tmp_path = self.app.sideband.tmp_dir+"/"+str(RNS.hexrep(lxm.hash, delimit=False))
qr_image.save(qr_tmp_path)
print_command = self.app.sideband.config["print_command"]+" "+qr_tmp_path
return_code = subprocess.call(shlex.split(print_command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
os.unlink(qr_tmp_path)
item.dmenu.dismiss()
except Exception as e:
item.dmenu.dismiss()
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
dialog = MDDialog(
title="Error",
text="Could not print the paper message QR-code.\n\n"+str(e),
buttons=[ ok_button ],
# elevation=0,
)
def dl_ok(s):
dialog.dismiss()
ok_button.bind(on_release=dl_ok)
dialog.open()
return x
2023-10-16 16:21:38 -04:00
retry_item = {
"viewclass": "OneLineListItem",
"text": "Retry",
"height": dp(40),
"on_release": gen_retry(m["hash"], m["content"], item)
}
2022-11-22 08:25:56 -05:00
if m["method"] == LXMF.LXMessage.PAPER:
if RNS.vendor.platformutils.is_android():
qr_save_text = "Share QR Code"
2022-11-22 13:47:13 -05:00
dm_items = [
{
"viewclass": "OneLineListItem",
"text": "Share QR Code",
"height": dp(40),
"on_release": gen_save_qr(m["lxm"], item)
},
{
"viewclass": "OneLineListItem",
"text": "Copy LXM URI",
"height": dp(40),
"on_release": gen_copy_lxm_uri(m["lxm"], item)
},
{
"viewclass": "OneLineListItem",
"text": "Copy message text",
"height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item)
},
{
"text": "Delete",
"viewclass": "OneLineListItem",
"height": dp(40),
"on_release": gen_del(m["hash"], item)
}
]
2022-11-22 08:25:56 -05:00
else:
2022-11-22 13:47:13 -05:00
dm_items = [
{
"viewclass": "OneLineListItem",
"text": "Print QR Code",
"height": dp(40),
"on_release": gen_print_qr(m["lxm"], item)
},
{
"viewclass": "OneLineListItem",
"text": "Save QR Code",
"height": dp(40),
"on_release": gen_save_qr(m["lxm"], item)
},
{
"viewclass": "OneLineListItem",
"text": "Copy LXM URI",
"height": dp(40),
"on_release": gen_copy_lxm_uri(m["lxm"], item)
},
{
"viewclass": "OneLineListItem",
"text": "Copy message text",
"height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item)
},
{
"text": "Delete",
"viewclass": "OneLineListItem",
"height": dp(40),
"on_release": gen_del(m["hash"], item)
}
]
2022-11-22 08:25:56 -05:00
else:
2023-10-16 16:21:38 -04:00
if m["state"] == LXMF.LXMessage.FAILED:
dm_items = [
retry_item,
{
"viewclass": "OneLineListItem",
"text": "Copy",
"height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item)
},
{
"text": "Delete",
"viewclass": "OneLineListItem",
"height": dp(40),
"on_release": gen_del(m["hash"], item)
}
]
else:
if telemeter != None:
2023-10-20 17:38:28 -04:00
dm_items = [
{
"viewclass": "OneLineListItem",
"text": "Copy",
"height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item)
},
{
"viewclass": "OneLineListItem",
"text": "Copy telemetry",
"height": dp(40),
"on_release": gen_copy_telemetry(telemeter, extra_telemetry, item)
2023-10-20 17:38:28 -04:00
},
{
"text": "Delete",
"viewclass": "OneLineListItem",
"height": dp(40),
"on_release": gen_del(m["hash"], item)
}
]
else:
dm_items = [
{
"viewclass": "OneLineListItem",
"text": "Copy",
"height": dp(40),
"on_release": gen_copy(m["content"].decode("utf-8"), item)
},
{
"text": "Delete",
"viewclass": "OneLineListItem",
"height": dp(40),
"on_release": gen_del(m["hash"], item)
}
]
2024-03-16 19:23:55 -04:00
if has_image:
extra_item = {
"viewclass": "OneLineListItem",
"text": "Save image",
"height": dp(40),
"on_release": gen_save_image(item)
}
dm_items.append(extra_item)
if has_attachment:
extra_item = {
"viewclass": "OneLineListItem",
"text": "Save attachment",
"height": dp(40),
"on_release": gen_save_attachment(item)
}
dm_items.append(extra_item)
2022-04-07 15:03:53 -04:00
item.dmenu = MDDropdownMenu(
caller=item.ids.msg_submenu,
items=dm_items,
2023-07-10 13:20:24 -04:00
position="auto",
width=dp(256),
elevation=0,
radius=dp(3),
2022-04-07 15:03:53 -04:00
)
2024-03-10 20:21:54 -04:00
item.dmenu.md_bg_color = self.app.color_hover
2023-10-16 16:21:38 -04:00
item.dmenu.retry_item = retry_item
2022-04-07 15:03:53 -04:00
def callback_factory(ref):
def x(sender):
ref.dmenu.open()
return x
# Bind menu open
item.ids.msg_submenu.bind(on_release=callback_factory(item))
if self.loading_earlier_messages:
insert_pos = len(self.list.children)
else:
insert_pos = 0
2022-04-07 15:03:53 -04:00
self.added_item_hashes.append(m["hash"])
self.widgets.append(item)
self.list.add_widget(item, insert_pos)
2022-04-07 15:03:53 -04:00
if self.latest_message_timestamp == None or m["received"] > self.latest_message_timestamp:
self.latest_message_timestamp = m["received"]
if self.earliest_message_timestamp == None or m["received"] < self.earliest_message_timestamp:
self.earliest_message_timestamp = m["received"]
self.added_messages += len(self.new_messages)
self.new_messages = []
2022-11-23 07:28:26 -05:00
2022-04-07 15:03:53 -04:00
def get_widget(self):
return self.list
def close_send_error_dialog(self, sender=None):
if self.send_error_dialog:
2023-10-22 08:01:31 -04:00
self.send_error_dialog.dismiss()
messages_screen_kv = """
MDScreen:
name: "messages_screen"
BoxLayout:
orientation: "vertical"
MDTopAppBar:
id: messages_toolbar
anchor_title: "left"
title: "Messages"
elevation: 0
left_action_items:
[['menu', lambda x: root.app.nav_drawer.set_state("open")],]
right_action_items:
[
2024-03-10 20:21:54 -04:00
['attachment-plus', lambda x: root.app.message_attachment_action(self)],
2023-10-23 19:14:59 -04:00
['map-marker-path', lambda x: root.app.peer_show_telemetry_action(self)],
2023-10-22 12:08:28 -04:00
['map-search', lambda x: root.app.peer_show_location_action(self)],
['lan-connect', lambda x: root.app.message_propagation_action(self)],
['close', lambda x: root.app.close_settings_action(self)],
]
ScrollView:
id: messages_scrollview
do_scroll_x: False
do_scroll_y: True
BoxLayout:
id: no_keys_part
orientation: "vertical"
padding: [dp(16), dp(0), dp(16), dp(16)]
spacing: dp(24)
size_hint_y: None
height: self.minimum_height + dp(64)
MDLabel:
id: nokeys_text
text: ""
MDRectangleFlatIconButton:
icon: "key-wireless"
text: "Query Network For Keys"
2023-10-22 12:08:28 -04:00
on_release: root.app.key_query_action(self)
BoxLayout:
id: message_ptt
padding: [dp(16), dp(8), dp(16), dp(8)]
spacing: dp(24)
size_hint_y: None
height: self.minimum_height
MDRectangleFlatIconButton:
id: message_ptt_button
icon: "microphone"
text: "PTT"
size_hint_x: 1.0
padding: [dp(10), dp(13), dp(10), dp(14)]
icon_size: dp(24)
font_size: dp(16)
on_press: root.app.message_ptt_down_action(self)
on_release: root.app.message_ptt_up_action(self)
_no_ripple_effect: True
background_normal: ""
background_down: ""
BoxLayout:
id: message_input_part
padding: [dp(16), dp(0), dp(16), dp(16)]
spacing: dp(24)
size_hint_y: None
height: self.minimum_height
MDTextField:
id: message_text
keyboard_suggestions: True
multiline: True
hint_text: "Write message"
mode: "rectangle"
max_height: dp(100)
MDRectangleFlatIconButton:
id: message_send_button
icon: "transfer-up"
text: "Send"
padding: [dp(10), dp(13), dp(10), dp(14)]
icon_size: dp(24)
font_size: dp(16)
2023-10-22 12:08:28 -04:00
on_release: root.app.message_send_action(self)
"""
2023-10-22 08:01:31 -04:00
Builder.load_string("""
<ListLXMessageCard>:
style: "outlined"
padding: dp(8)
2023-10-26 09:23:25 -04:00
radius: dp(4)
2023-10-22 08:01:31 -04:00
size_hint: 1.0, None
2024-03-10 13:14:04 -04:00
height: content_text.height + heading_text.height + message_image.size[1] + dp(32)
2023-10-22 08:01:31 -04:00
pos_hint: {"center_x": .5, "center_y": .5}
MDRelativeLayout:
size_hint: 1.0, None
theme_text_color: "ContrastParentBackground"
MDIconButton:
id: msg_submenu
icon: "dots-vertical"
# theme_text_color: 'Custom'
# text_color: rgba(255,255,255,216)
pos:
root.width - (self.width + root.padding[0] + dp(4)), root.height - (self.height + root.padding[0] + dp(4))
MDLabel:
id: heading_text
markup: True
text: root.heading
adaptive_size: True
# theme_text_color: 'Custom'
# text_color: rgba(255,255,255,100)
pos: 0, root.height - (self.height + root.padding[0] + dp(8))
2024-03-10 13:14:04 -04:00
Image:
id: message_image
size_hint_x: 0
size_hint_y: 0
pos: 0, root.height - (self.height + root.padding[0] + dp(8)) - heading_text.height - dp(8)
2023-10-22 08:01:31 -04:00
MDLabel:
id: content_text
text: root.text
2024-01-04 19:49:25 -05:00
markup: True
2023-10-22 08:01:31 -04:00
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
<CustomOneLineIconListItem>
IconLeftWidget:
icon: root.icon
""")