mirror of
https://github.com/markqvist/Sideband.git
synced 2024-12-11 08:54:31 -05:00
1140 lines
51 KiB
Python
1140 lines
51 KiB
Python
import time
|
|
import RNS
|
|
import LXMF
|
|
|
|
from kivy.metrics import dp,sp
|
|
from kivy.core.clipboard import Clipboard
|
|
from kivy.core.image import Image as CoreImage
|
|
from kivymd.uix.card import MDCard
|
|
from kivymd.uix.menu import MDDropdownMenu
|
|
# from kivymd.uix.behaviors import RoundedRectangularElevationBehavior, FakeRectangularElevationBehavior
|
|
from kivymd.uix.behaviors import CommonElevationBehavior
|
|
from kivy.properties import StringProperty, BooleanProperty
|
|
from kivy.uix.gridlayout import GridLayout
|
|
from kivy.uix.boxlayout import BoxLayout
|
|
from kivy.clock import Clock
|
|
from kivy.utils import escape_markup
|
|
|
|
from kivymd.uix.button import MDRectangleFlatButton, MDRectangleFlatIconButton
|
|
from kivymd.uix.dialog import MDDialog
|
|
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
from ui.helpers import multilingual_markup
|
|
else:
|
|
from .helpers import multilingual_markup
|
|
|
|
import io
|
|
import os
|
|
import subprocess
|
|
import shlex
|
|
|
|
from kivy.graphics.opengl import glGetIntegerv, GL_MAX_TEXTURE_SIZE
|
|
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
import plyer
|
|
from sideband.sense import Telemeter, Commands
|
|
from ui.helpers import ts_format, file_ts_format, mdc
|
|
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
|
|
else:
|
|
import sbapp.plyer as plyer
|
|
from sbapp.sideband.sense import Telemeter, Commands
|
|
from .helpers import ts_format, file_ts_format, mdc
|
|
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
|
|
|
|
if RNS.vendor.platformutils.is_darwin():
|
|
from PIL import Image as PilImage
|
|
|
|
from kivy.lang.builder import Builder
|
|
|
|
class ListLXMessageCard(MDCard):
|
|
# class ListLXMessageCard(MDCard, FakeRectangularElevationBehavior):
|
|
text = StringProperty()
|
|
heading = StringProperty()
|
|
|
|
class Messages():
|
|
def __init__(self, app, context_dest):
|
|
self.app = app
|
|
self.context_dest = context_dest
|
|
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)
|
|
|
|
self.screen = self.app.root.ids.screen_manager.get_screen("messages_screen")
|
|
self.ids = self.screen.ids
|
|
|
|
self.max_texture_size = glGetIntegerv(GL_MAX_TEXTURE_SIZE)[0]
|
|
self.new_messages = []
|
|
self.added_item_hashes = []
|
|
self.added_messages = 0
|
|
self.latest_message_timestamp = None
|
|
self.earliest_message_timestamp = time.time()
|
|
self.loading_earlier_messages = False
|
|
self.list = None
|
|
self.widgets = []
|
|
self.send_error_dialog = None
|
|
self.load_more_button = None
|
|
self.update()
|
|
|
|
def reload(self):
|
|
if self.list != None:
|
|
self.list.clear_widgets()
|
|
|
|
self.new_messages = []
|
|
self.added_item_hashes = []
|
|
self.added_messages = 0
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
if self.list == None:
|
|
layout = GridLayout(cols=1, spacing=dp(16), padding=dp(16), size_hint_y=None)
|
|
layout.bind(minimum_height=layout.setter('height'))
|
|
self.list = layout
|
|
|
|
if self.ptt_enabled:
|
|
self.hide_widget(self.ids.message_ptt, False)
|
|
else:
|
|
self.hide_widget(self.ids.message_ptt, True)
|
|
|
|
c_ts = time.time()
|
|
if len(self.new_messages) > 0:
|
|
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))
|
|
|
|
if self.app.sideband.config["dark_ui"]:
|
|
intensity_msgs = intensity_msgs_dark
|
|
intensity_play = intensity_play_dark
|
|
else:
|
|
intensity_msgs = intensity_msgs_light
|
|
intensity_play = intensity_play_light
|
|
|
|
for w in self.widgets:
|
|
m = w.m
|
|
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)
|
|
|
|
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 = ""
|
|
sphrase = "Sending"
|
|
prg = self.app.sideband.get_lxm_progress(msg["hash"])
|
|
if prg != None:
|
|
prgstr = ", "+str(round(prg*100, 1))+"% done"
|
|
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:
|
|
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"
|
|
w.heading = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] "+sphrase+prgstr+" "
|
|
if w.has_audio:
|
|
alstr = RNS.prettysize(w.audio_size)
|
|
w.heading += f"\n[b]Audio Message[/b] ({alstr})"
|
|
m["state"] = msg["state"]
|
|
|
|
|
|
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"
|
|
if w.has_audio:
|
|
alstr = RNS.prettysize(w.audio_size)
|
|
w.heading += f"\n[b]Audio Message[/b] ({alstr})"
|
|
m["state"] = msg["state"]
|
|
|
|
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"]
|
|
|
|
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"
|
|
if w.has_audio:
|
|
alstr = RNS.prettysize(w.audio_size)
|
|
w.heading += f"\n[b]Audio Message[/b] ({alstr})"
|
|
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"
|
|
m["state"] = msg["state"]
|
|
if w.has_audio:
|
|
alstr = RNS.prettysize(w.audio_size)
|
|
w.heading += f"\n[b]Audio Message[/b] ({alstr})"
|
|
w.dmenu.items.append(w.dmenu.retry_item)
|
|
|
|
|
|
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
|
|
|
|
def update_widget(self):
|
|
if self.app.sideband.config["dark_ui"]:
|
|
intensity_msgs = intensity_msgs_dark
|
|
intensity_play = intensity_play_dark
|
|
mt_color = [1.0, 1.0, 1.0, 0.8]
|
|
else:
|
|
intensity_msgs = intensity_msgs_light
|
|
intensity_play = intensity_play_light
|
|
mt_color = [1.0, 1.0, 1.0, 0.95]
|
|
|
|
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:
|
|
if not m["hash"] in self.added_item_hashes:
|
|
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"":
|
|
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")
|
|
|
|
message_markup = multilingual_markup(message_input)
|
|
|
|
txstr = time.strftime(ts_format, time.localtime(m["sent"]))
|
|
rxstr = time.strftime(ts_format, time.localtime(m["received"]))
|
|
titlestr = ""
|
|
extra_content = ""
|
|
extra_telemetry = {}
|
|
telemeter = None
|
|
image_field = None
|
|
audio_field = None
|
|
has_image = False
|
|
has_audio = False
|
|
attachments_field = None
|
|
has_attachment = False
|
|
force_markup = False
|
|
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
|
|
|
|
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"
|
|
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)
|
|
|
|
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
|
|
|
|
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]
|
|
has_image = True
|
|
except Exception as e:
|
|
pass
|
|
|
|
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
|
|
|
|
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:
|
|
rcvd_d_str = "\n[b]Distance[/b] "+RNS.prettydistance(edst)
|
|
elif "geodesic" in d:
|
|
gdst = d["geodesic"]
|
|
if gdst != None:
|
|
rcvd_d_str = "\n[b]Distance[/b] "+RNS.prettydistance(gdst) + " (geodesic)"
|
|
|
|
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
|
|
|
|
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"
|
|
|
|
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"
|
|
|
|
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"
|
|
|
|
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"
|
|
|
|
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 "
|
|
|
|
else:
|
|
msg_color = mdc(color_unknown, intensity_msgs)
|
|
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Unknown"
|
|
|
|
else:
|
|
msg_color = mdc(color_received, intensity_msgs)
|
|
heading_str = titlestr
|
|
if phy_stats_str != "" and self.app.sideband.config["advanced_stats"]:
|
|
heading_str += phy_stats_str+"\n"
|
|
|
|
heading_str += "[b]Sent[/b] "+txstr
|
|
heading_str += "\n[b]Received[/b] "+rxstr
|
|
|
|
if rcvd_d_str != "":
|
|
heading_str += rcvd_d_str
|
|
|
|
pre_content = ""
|
|
if not signature_valid:
|
|
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
|
|
|
|
if has_attachment:
|
|
heading_str += "\n[b]Attachments[/b] "
|
|
for attachment in attachments_field:
|
|
heading_str += str(attachment[0])+", "
|
|
heading_str = heading_str[:-2]
|
|
|
|
if has_audio:
|
|
alstr = RNS.prettysize(len(audio_field[1]))
|
|
heading_str += f"\n[b]Audio Message[/b] ({alstr})"
|
|
|
|
item = ListLXMessageCard(
|
|
text=pre_content+message_markup.decode("utf-8")+extra_content,
|
|
heading=heading_str,
|
|
md_bg_color=msg_color,
|
|
)
|
|
item.lsource = m["source"]
|
|
item.has_audio = False
|
|
|
|
if has_attachment:
|
|
item.attachments_field = attachments_field
|
|
|
|
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
|
|
item.audio_size = len(audio_field[1])
|
|
item.audio_field = audio_field
|
|
item.bind(on_release=play_audio)
|
|
|
|
if image_field != None:
|
|
item.has_image = True
|
|
item.image_field = image_field
|
|
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"
|
|
|
|
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):
|
|
try:
|
|
if w.texture_size[0] > 360 and w.texture_size[1] >= self.max_texture_size:
|
|
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]"
|
|
|
|
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)
|
|
|
|
if not RNS.vendor.platformutils.is_android():
|
|
item.radius = dp(5)
|
|
|
|
item.sb_uid = m["hash"]
|
|
item.m = m
|
|
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
|
|
|
|
def gen_del(mhash, item):
|
|
def x():
|
|
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))
|
|
dialog = MDDialog(
|
|
title="Delete message?",
|
|
buttons=[ yes_button, no_button ],
|
|
# elevation=0,
|
|
)
|
|
def dl_yes(s):
|
|
dialog.dismiss()
|
|
self.app.sideband.delete_message(mhash)
|
|
|
|
def cb(dt):
|
|
self.reload()
|
|
Clock.schedule_once(cb, 0.2)
|
|
|
|
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
|
|
|
|
def gen_retry(mhash, mcontent, item):
|
|
def x():
|
|
self.app.messages_view.ids.message_text.text = mcontent.decode("utf-8")
|
|
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
|
|
|
|
def gen_copy(msg, item):
|
|
def x():
|
|
Clipboard.copy(msg)
|
|
item.dmenu.dismiss()
|
|
|
|
return x
|
|
|
|
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):
|
|
def x():
|
|
try:
|
|
telemeter
|
|
if extra_telemetry and len(extra_telemetry) != 0:
|
|
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()
|
|
Clipboard.copy(str(tlm))
|
|
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
|
|
|
|
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():
|
|
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)
|
|
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
|
|
|
|
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
|
|
|
|
retry_item = {
|
|
"viewclass": "OneLineListItem",
|
|
"text": "Retry",
|
|
"height": dp(40),
|
|
"on_release": gen_retry(m["hash"], m["content"], item)
|
|
}
|
|
if m["method"] == LXMF.LXMessage.PAPER:
|
|
if RNS.vendor.platformutils.is_android():
|
|
qr_save_text = "Share QR Code"
|
|
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)
|
|
}
|
|
]
|
|
|
|
else:
|
|
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)
|
|
}
|
|
]
|
|
|
|
else:
|
|
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:
|
|
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)
|
|
},
|
|
{
|
|
"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)
|
|
}
|
|
]
|
|
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)
|
|
|
|
item.dmenu = MDDropdownMenu(
|
|
caller=item.ids.msg_submenu,
|
|
items=dm_items,
|
|
position="auto",
|
|
width=dp(256),
|
|
elevation=0,
|
|
radius=dp(3),
|
|
)
|
|
item.dmenu.md_bg_color = self.app.color_hover
|
|
item.dmenu.retry_item = retry_item
|
|
|
|
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
|
|
|
|
self.added_item_hashes.append(m["hash"])
|
|
self.widgets.append(item)
|
|
self.list.add_widget(item, insert_pos)
|
|
|
|
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 = []
|
|
|
|
def get_widget(self):
|
|
return self.list
|
|
|
|
def close_send_error_dialog(self, sender=None):
|
|
if self.send_error_dialog:
|
|
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:
|
|
[
|
|
['attachment-plus', lambda x: root.app.message_attachment_action(self)],
|
|
['map-marker-path', lambda x: root.app.peer_show_telemetry_action(self)],
|
|
['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"
|
|
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)
|
|
on_release: root.app.message_send_action(self)
|
|
"""
|
|
|
|
Builder.load_string("""
|
|
<ListLXMessageCard>:
|
|
style: "outlined"
|
|
padding: dp(8)
|
|
radius: dp(4)
|
|
size_hint: 1.0, None
|
|
height: content_text.height + heading_text.height + message_image.size[1] + dp(32)
|
|
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))
|
|
|
|
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)
|
|
|
|
MDLabel:
|
|
id: content_text
|
|
text: root.text
|
|
markup: True
|
|
size_hint_y: None
|
|
text_size: self.width, None
|
|
height: self.texture_size[1]
|
|
|
|
<CustomOneLineIconListItem>
|
|
IconLeftWidget:
|
|
icon: root.icon
|
|
""")
|