From 179ebe7cecdb15b9bccfd5ddf1e892e6572ce44e Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 20 Oct 2023 23:38:28 +0200 Subject: [PATCH] Map marker drawing --- sbapp/main.py | 98 +++++++++++++++++++- sbapp/mapview/downloader.py | 21 ++--- sbapp/sideband/core.py | 172 +++++++++++++++++++++++++++++++++++- sbapp/ui/layouts.py | 16 ++++ sbapp/ui/messages.py | 67 ++++++++++---- 5 files changed, 341 insertions(+), 33 deletions(-) diff --git a/sbapp/main.py b/sbapp/main.py index 1530054..86fc713 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -1,4 +1,4 @@ -__debug_build__ = False +__debug_build__ = True __disable_shaders__ = False __version__ = "0.6.3" __variant__ = "beta" @@ -44,6 +44,8 @@ from kivy.uix.screenmanager import FadeTransition, NoTransition from kivymd.uix.list import OneLineIconListItem from kivy.properties import StringProperty from kivymd.uix.pickers import MDColorPicker +from sideband.sense import Telemeter +from mapview import MapMarker if RNS.vendor.platformutils.get_platform() == "android": from sideband.core import SidebandCore @@ -132,6 +134,8 @@ class SidebandApp(MDApp): self.notification_icon = self.sideband.asset_dir+"/notification_icon.png" self.connectivity_updater = None + self.last_map_update = 0 + self.last_telemetry_received = 0 ################################################# @@ -601,6 +605,16 @@ class SidebandApp(MDApp): if self.announces_view != None: self.announces_view.update() + elif self.root.ids.screen_manager.current == "map_screen": + if hasattr(self.root.ids.map_layout, "map") and self.root.ids.map_layout.map != None: + self.sideband.config["map_lat"] = self.root.ids.map_layout.map.lat + self.sideband.config["map_lon"] = self.root.ids.map_layout.map.lon + self.sideband.config["map_zoom"] = self.root.ids.map_layout.map.zoom + + self.last_telemetry_received = self.sideband.getstate("app.flags.last_telemetry", allow_cache=True) or 0 + if self.last_telemetry_received > self.last_map_update: + self.map_update_markers() + if self.sideband.getstate("app.flags.new_conversations", allow_cache=True): if self.conversations_view != None: self.conversations_view.update() @@ -695,6 +709,10 @@ class SidebandApp(MDApp): self.message_send_action() if len(modifiers) > 0 and modifiers[0] == 'ctrl' and (text == "l"): self.announces_action(self) + if len(modifiers) > 0 and modifiers[0] == 'ctrl' and (text == "m"): + self.map_action(self) + if len(modifiers) > 0 and modifiers[0] == 'ctrl' and (text == "t"): + self.telemetry_action(self) if len(modifiers) > 0 and modifiers[0] == 'ctrl' and (text == "r"): self.conversations_action(self) if len(modifiers) > 0 and modifiers[0] == 'ctrl' and (text == "g"): @@ -897,7 +915,6 @@ class SidebandApp(MDApp): on_release=self.messages_view.close_send_error_dialog ) ], - # elevation=0, ) self.messages_view.send_error_dialog.open() @@ -1198,7 +1215,7 @@ class SidebandApp(MDApp): self.root.ids.information_scrollview.effect_cls = ScrollEffect self.root.ids.information_logo.icon = self.sideband.asset_dir+"/rns_256.png" - info = "This is Sideband v"+__version__+" "+__variant__+", on RNS v"+RNS.__version__+" and LXMF v"+LXMF.__version__+".\n\nHumbly build using the following open components:\n\n - [b]Reticulum[/b] (MIT License)\n - [b]LXMF[/b] (MIT License)\n - [b]KivyMD[/b] (MIT License)\n - [b]Kivy[/b] (MIT License)\n - [b]Python[/b] (PSF License)"+"\n\nGo to [u][ref=link]https://unsigned.io/donate[/ref][/u] to support the project.\n\nThe Sideband app is Copyright (c) 2022 Mark Qvist / unsigned.io\n\nPermission is granted to freely share and distribute binary copies of Sideband v"+__version__+" "+__variant__+", 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 - USE AT YOUR OWN RISK AND RESPONSIBILITY" + info = "This is Sideband v"+__version__+" "+__variant__+", on RNS v"+RNS.__version__+" and LXMF v"+LXMF.__version__+".\n\nHumbly build using the following open components:\n\n - [b]Reticulum[/b] (MIT License)\n - [b]LXMF[/b] (MIT License)\n - [b]KivyMD[/b] (MIT License)\n - [b]Kivy[/b] (MIT License)\n - [b]Python[/b] (PSF License)"+"\n\nGo to [u][ref=link]https://unsigned.io/donate[/ref][/u] to support the project.\n\nThe Sideband app is Copyright (c) 2022 Mark Qvist / unsigned.io\n\nPermission is granted to freely share and distribute binary copies of Sideband v"+__version__+" "+__variant__+", 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" self.root.ids.information_info.text = info self.root.ids.information_info.bind(on_ref_press=link_exec) self.root.ids.screen_manager.transition.direction = "left" @@ -2832,6 +2849,9 @@ class SidebandApp(MDApp): self.root.ids.telemetry_send_to_trusted.active = self.sideband.config["telemetry_send_to_trusted"] self.root.ids.telemetry_send_to_trusted.bind(active=self.telemetry_save) + self.root.ids.telemetry_send_appearance.active = self.sideband.config["telemetry_send_appearance"] + self.root.ids.telemetry_send_appearance.bind(active=self.telemetry_save) + self.root.ids.telemetry_s_location.active = self.sideband.config["telemetry_s_location"] self.root.ids.telemetry_s_location.bind(active=self.telemetry_location_toggle) @@ -2909,6 +2929,7 @@ class SidebandApp(MDApp): self.sideband.config["telemetry_enabled"] = self.root.ids.telemetry_enabled.active self.sideband.config["telemetry_send_to_collector"] = self.root.ids.telemetry_send_to_collector.active self.sideband.config["telemetry_send_to_trusted"] = self.root.ids.telemetry_send_to_trusted.active + self.sideband.config["telemetry_send_appearance"] = self.root.ids.telemetry_send_appearance.active self.sideband.config["telemetry_s_location"] = self.root.ids.telemetry_s_location.active self.sideband.config["telemetry_s_orientation"] = self.root.ids.telemetry_s_orientation.active @@ -3034,7 +3055,7 @@ class SidebandApp(MDApp): def map_action(self, sender=None): if not hasattr(self.root.ids.map_layout, "map") or self.root.ids.map_layout.map == None: from mapview import MapView - mapview = MapView(zoom=11, lat=50.6394, lon=3.057) + mapview = MapView(zoom=self.sideband.config["map_zoom"], lat=self.sideband.config["map_lat"], lon=self.sideband.config["map_lon"]) mapview.snap_to_zoom = False mapview.double_tap_zoom = False self.root.ids.map_layout.map = mapview @@ -3045,6 +3066,73 @@ class SidebandApp(MDApp): self.root.ids.nav_drawer.set_state("closed") self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current) + if not hasattr(self, "map_markers") or self.map_markers == None: + self.map_markers = {} + + def am_job(dt): + self.map_update_markers() + Clock.schedule_once(am_job, 0.6) + + + def map_update_markers(self, sender=None): + # TODO: Remove + # time_s = time.time() + # RNS.log("Update map markers", RNS.LOG_WARNING) + earliest = time.time() - self.sideband.config["map_history_limit"] + telemetry_entries = self.sideband.list_telemetry(after=earliest) + changes = False + for telemetry_source in telemetry_entries: + skip = False + + if telemetry_source in self.map_markers: + marker = self.map_markers[telemetry_source] + newest_timestamp = telemetry_entries[telemetry_source][0][0] + if newest_timestamp <= marker.latest_timestamp: + skip = True + + latest_viewable = None + if not skip: + for telemetry_entry in telemetry_entries[telemetry_source]: + telemetry_timestamp = telemetry_entry[0] + telemetry_data = telemetry_entry[1] + t = Telemeter.from_packed(telemetry_data) + if t != None: + telemetry = t.read_all() + # TODO: Remove + # RNS.log(str(telemetry)+" "+str(t), RNS.LOG_WARNING) + if "location" in telemetry and telemetry["location"]["latitude"] != None and telemetry["location"]["longtitude"] != None: + latest_viewable = telemetry + break + + if latest_viewable != None: + l = latest_viewable["location"] + if not telemetry_source in self.map_markers: + marker = MapMarker(lat=l["latitude"], lon=l["longtitude"]) + marker.source_dest = telemetry_source + marker.latest_timestamp = latest_viewable["time"]["utc"] + self.map_markers[telemetry_source] = marker + self.root.ids.map_layout.map.add_widget(marker) + changes = True + else: + marker = self.map_markers[telemetry_source] + marker.latest_timestamp = latest_viewable["time"]["utc"] + marker.lat = l["latitude"] + marker.lon = l["longtitude"] + changes = True + + self.last_map_update = time.time() + if changes: + mv = self.root.ids.map_layout.map + mv.trigger_update(True) + + # TODO: Remove + # RNS.log("Updated map markers in "+RNS.prettytime(time.time()-time_s), RNS.LOG_WARNING) + + + def map_add_marker(self, marker): + marker = MapMarker(lat=0.0, lon=0.0) + self.root.ids.map_layout.map.add_widget(marker) + ### Guide screen ###################################### def close_guide_action(self, sender=None): @@ -3090,6 +3178,8 @@ If you use Reticulum and LXMF on hardware that does not carry any identifiers ti - Ctrl-D or Ctrl-S Send message - Ctrl-R Show Conversations - Ctrl-L Show Announce Stream + - Ctrl-M Show Situation Map + - Ctrl-T Show Telemetry Setup - Ctrl-N New conversation - Ctrl-G Show guide""" diff --git a/sbapp/mapview/downloader.py b/sbapp/mapview/downloader.py index 141598d..ed8ae8a 100644 --- a/sbapp/mapview/downloader.py +++ b/sbapp/mapview/downloader.py @@ -18,6 +18,7 @@ from mapview.constants import CACHE_DIR # if "MAPVIEW_DEBUG_DOWNLOADER" in environ: # Logger.setLevel(LOG_LEVELS['debug']) +Logger.setLevel(LOG_LEVELS['error']) # user agent is needed because since may 2019 OSM gives me a 429 or 403 server error # I tried it with a simpler one (just Mozilla/5.0) this also gets rejected @@ -56,21 +57,21 @@ class Downloader: self._futures.append(future) def download_tile(self, tile): - Logger.debug( - "Downloader: queue(tile) zoom={} x={} y={}".format( - tile.zoom, tile.tile_x, tile.tile_y - ) - ) + # Logger.debug( + # "Downloader: queue(tile) zoom={} x={} y={}".format( + # tile.zoom, tile.tile_x, tile.tile_y + # ) + # ) future = self.executor.submit(self._load_tile, tile) self._futures.append(future) def download(self, url, callback, **kwargs): - Logger.debug("Downloader: queue(url) {}".format(url)) + # Logger.debug("Downloader: queue(url) {}".format(url)) future = self.executor.submit(self._download_url, url, callback, kwargs) self._futures.append(future) def _download_url(self, url, callback, kwargs): - Logger.debug("Downloader: download(url) {}".format(url)) + # Logger.debug("Downloader: download(url) {}".format(url)) response = requests.get(url, **kwargs) response.raise_for_status() return callback, (url, response) @@ -80,20 +81,20 @@ class Downloader: return cache_fn = tile.cache_fn if exists(cache_fn): - Logger.debug("Downloader: use cache {}".format(cache_fn)) + # Logger.debug("Downloader: use cache {}".format(cache_fn)) return tile.set_source, (cache_fn,) tile_y = tile.map_source.get_row_count(tile.zoom) - tile.tile_y - 1 uri = tile.map_source.url.format( z=tile.zoom, x=tile.tile_x, y=tile_y, s=choice(tile.map_source.subdomains) ) - Logger.debug("Downloader: download(tile) {}".format(uri)) + # Logger.debug("Downloader: download(tile) {}".format(uri)) response = requests.get(uri, headers={'User-agent': USER_AGENT}, timeout=5) try: response.raise_for_status() data = response.content with open(cache_fn, "wb") as fd: fd.write(data) - Logger.debug("Downloaded {} bytes: {}".format(len(data), uri)) + # Logger.debug("Downloaded {} bytes: {}".format(len(data), uri)) return tile.set_source, (cache_fn,) except Exception as e: print("Downloader error: {!r}".format(e)) diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 0dcfbc2..49c5986 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -4,6 +4,7 @@ import threading import plyer import os.path import time +import struct import sqlite3 import random @@ -313,6 +314,7 @@ class SidebandCore(): else: self._db_initstate() self._db_initpersistent() + self._db_inittelemetry() self.__save_config() @@ -457,6 +459,8 @@ class SidebandCore(): self.config["telemetry_fg"] = [0,0,0,1] if not "telemetry_bg" in self.config: self.config["telemetry_bg"] = [1,1,1,1] + if not "telemetry_send_appearance" in self.config: + self.config["telemetry_send_appearance"] = False if not "telemetry_s_location" in self.config: self.config["telemetry_s_location"] = False @@ -483,12 +487,22 @@ class SidebandCore(): if not "telemetry_s_proximity" in self.config: self.config["telemetry_s_proximity"] = False + if not "map_history_limit" in self.config: + self.config["map_history_limit"] = 7*24*60*60 + if not "map_lat" in self.config: + self.config["map_lat"] = 0.0 + if not "map_lon" in self.config: + self.config["map_lon"] = 0.0 + if not "map_zoom" in self.config: + self.config["map_zoom"] = 3 + # Make sure we have a database if not os.path.isfile(self.db_path): self.__db_init() else: self._db_initstate() self._db_initpersistent() + self._db_inittelemetry() self.__db_indices() def __reload_config(self): @@ -709,6 +723,9 @@ class SidebandCore(): else: return None + def list_telemetry(self, context_dest = None, after = None, before = None, limit = None): + return self._db_telemetry(context_dest = context_dest, after = after, before = before, limit = limit) + def list_messages(self, context_dest, after = None, before = None, limit = None): result = self._db_messages(context_dest, after, before, limit) if result != None: @@ -909,6 +926,13 @@ class SidebandCore(): dbc.execute("CREATE TABLE IF NOT EXISTS persistent (property BLOB PRIMARY KEY, value BLOB)") db.commit() + def _db_inittelemetry(self): + db = self.__db_connect() + dbc = db.cursor() + + dbc.execute("CREATE TABLE IF NOT EXISTS telemetry (id INTEGER PRIMARY KEY, dest_context BLOB, ts INTEGER, data BLOB)") + db.commit() + def _db_getpersistent(self, prop): try: db = self.__db_connect() @@ -998,6 +1022,110 @@ class SidebandCore(): result = dbc.fetchall() db.commit() + def _db_telemetry(self, context_dest = None, after = None, before = None, limit = None): + db = self.__db_connect() + dbc = db.cursor() + + # TODO: Implement limit + + order_part = " order by ts DESC" + if context_dest == None: + if after != None and before == None: + query = "select * from telemetry where ts>:after_ts"+order_part + dbc.execute(query, {"after_ts": after}) + elif after == None and before != None: + query = "select * from telemetry where ts<:before_ts"+order_part + dbc.execute(query, {"before_ts": before}) + elif after != None and before != None: + query = "select * from telemetry where ts<:before_ts and ts>:after_ts"+order_part + dbc.execute(query, {"before_ts": before, "after_ts": after}) + else: + query = query = "select * from telemetry" + dbc.execute(query, {}) + + else: + if after != None and before == None: + query = "select * from telemetry where dest_context=:context_dest and ts>:after_ts"+order_part + dbc.execute(query, {"context_dest": context_dest, "after_ts": after}) + elif after == None and before != None: + query = "select * from telemetry where dest_context=:context_dest and ts<:before_ts"+order_part + dbc.execute(query, {"context_dest": context_dest, "before_ts": before}) + elif after != None and before != None: + query = "select * from telemetry where dest_context=:context_dest and ts<:before_ts and ts>:after_ts"+order_part + dbc.execute(query, {"context_dest": context_dest, "before_ts": before, "after_ts": after}) + else: + query = query = "select * from telemetry where dest_context=:context_dest"+order_part + dbc.execute(query, {"context_dest": context_dest}) + + result = dbc.fetchall() + + if len(result) < 1: + return None + else: + results = {} + for entry in result: + telemetry_source = entry[1] + telemetry_timestamp = entry[2] + telemetry_data = entry[3] + + if not telemetry_source in results: + results[telemetry_source] = [] + + results[telemetry_source].append([telemetry_timestamp, telemetry_data]) + + return results + + def _db_save_telemetry(self, context_dest, telemetry): + # TODO: Remove + # RNS.log("Saving telemetry for "+RNS.prettyhexrep(context_dest), RNS.LOG_WARNING) + + try: + remote_telemeter = Telemeter.from_packed(telemetry) + telemetry_timestamp = remote_telemeter.read_all()["time"]["utc"] + + db = self.__db_connect() + dbc = db.cursor() + + query = "select * from telemetry where dest_context=:ctx and ts=:tts" + dbc.execute(query, {"ctx": context_dest, "tts": telemetry_timestamp}) + result = dbc.fetchall() + + if len(result) != 0: + # TODO: Remove + # RNS.log("Telemetry entry already exists, ignoring", RNS.LOG_WARNING) + return + + query = "INSERT INTO telemetry (dest_context, ts, data) values (?, ?, ?)" + data = (context_dest, telemetry_timestamp, telemetry) + dbc.execute(query, data) + db.commit() + self.setstate("app.flags.last_telemetry", time.time()) + + except Exception as e: + RNS.log("An error occurred while saving telemetry to database: "+str(e), RNS.LOG_ERROR) + self.db = None + + def _db_update_appearance(self, context_dest, timestamp, appearance): + # TODO: Remove + # RNS.log("Updating appearance for "+RNS.prettyhexrep(context_dest), RNS.LOG_WARNING) + + conv = self._db_conversation(context_dest) + data_dict = conv["data"] + if data_dict == None: + data_dict = {} + + data_dict["appearance"] = appearance + packed_dict = msgpack.packb(data_dict) + + db = self.__db_connect() + dbc = db.cursor() + + query = "UPDATE conv set data = ? where dest_context = ?" + data = (packed_dict, context_dest) + dbc.execute(query, data) + result = dbc.fetchall() + db.commit() + def _db_conversation_set_telemetry(self, context_dest, send_telemetry=False): conv = self._db_conversation(context_dest) data_dict = conv["data"] @@ -1346,6 +1474,13 @@ class SidebandCore(): db.commit() + if lxm.fields != None: + if LXMF.FIELD_ICON_APPEARANCE in lxm.fields: + self._db_update_appearance(context_dest, lxm.timestamp, lxm.fields[LXMF.FIELD_ICON_APPEARANCE]) + + if LXMF.FIELD_TELEMETRY in lxm.fields: + self._db_save_telemetry(context_dest, lxm.fields[LXMF.FIELD_TELEMETRY]) + self.__event_conversation_changed(context_dest) def _db_save_announce(self, destination_hash, app_data, dest_type="lxmf.delivery"): @@ -1442,8 +1577,6 @@ class SidebandCore(): def get_packed_telemetry(self): self.update_telemeter_config() packed = self.telemeter.packed() - # TODO: Remove - RNS.log("Packed telemetry: "+str(packed), RNS.LOG_WARNING) return packed def is_known(self, dest_hash): @@ -2096,6 +2229,34 @@ class SidebandCore(): else: self.lxm_ingest(message, originator=True) + def get_message_fields(self, context_dest): + fields = None + send_telemetry = self.should_send_telemetry(context_dest) + send_appearance = self.config["telemetry_send_appearance"] or send_telemetry + if send_telemetry or send_appearance: + fields = {} + if send_appearance: + # TODO: REMOVE + # RNS.log("Sending appearance", RNS.LOG_WARNING) + def fth(c): + r = c[0]; g = c[1]; b = c[2] + r = min(max(0, r), 1); g = min(max(0, g), 1); b = min(max(0, b), 1) + d = 1.0/255.0 + return struct.pack("!BBB", int(r/d), int(g/d), int(b/d)) + + icon = self.config["telemetry_icon"] + fg = fth(self.config["telemetry_fg"][:-1]) + bg = fth(self.config["telemetry_bg"][:-1]) + + fields[LXMF.FIELD_ICON_APPEARANCE] = [icon, fg, bg] + + if send_telemetry: + # TODO: REMOVE + # RNS.log("Sending telemetry", RNS.LOG_WARNING) + fields[LXMF.FIELD_TELEMETRY] = self.latest_packed_telemetry + + return fields + def paper_message(self, content, destination_hash): try: if content == "": @@ -2106,7 +2267,7 @@ class SidebandCore(): source = self.lxmf_destination desired_method = LXMF.LXMessage.PAPER - lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method) + lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = self.get_message_fields(destination_hash)) self.lxm_ingest(lxm, originator=True) @@ -2130,7 +2291,7 @@ class SidebandCore(): else: desired_method = LXMF.LXMessage.DIRECT - lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method) + lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = self.get_message_fields(destination_hash)) lxm.register_delivery_callback(self.message_notification) lxm.register_failed_callback(self.message_notification) @@ -2195,6 +2356,9 @@ class SidebandCore(): self.setstate("lxm_uri_ingest.result", response) def lxm_ingest(self, message, originator = False): + # TODO: Remove + RNS.log("MESSAGE FIELDS: "+str(message.fields), RNS.LOG_WARNING) + should_notify = False is_trusted = False unread_reason_tx = False diff --git a/sbapp/ui/layouts.py b/sbapp/ui/layouts.py index 666382a..f5cbab5 100644 --- a/sbapp/ui/layouts.py +++ b/sbapp/ui/layouts.py @@ -964,6 +964,22 @@ MDNavigationLayout: pos_hint: {"center_y": 0.3} active: False + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + padding: [0,0,dp(24),dp(0)] + height: dp(48) + + MDLabel: + text: "Always send custom display style" + font_style: "H6" + + MDSwitch: + id: telemetry_send_appearance + pos_hint: {"center_y": 0.3} + active: False + + MDLabel: markup: True text: "\\n\\n" diff --git a/sbapp/ui/messages.py b/sbapp/ui/messages.py index 0a93f08..6138d45 100644 --- a/sbapp/ui/messages.py +++ b/sbapp/ui/messages.py @@ -22,9 +22,11 @@ import subprocess import shlex if RNS.vendor.platformutils.get_platform() == "android": + from sideband.sense import Telemeter from ui.helpers import ts_format, file_ts_format, mdc from ui.helpers import color_received, color_delivered, color_propagated, color_paper, color_failed, color_unknown, intensity_msgs_dark, intensity_msgs_light else: + from sbapp.sideband.sense import Telemeter from .helpers import ts_format, file_ts_format, mdc from .helpers import color_received, color_delivered, color_propagated, color_paper, color_failed, color_unknown, intensity_msgs_dark, intensity_msgs_light @@ -260,6 +262,18 @@ class Messages(): return x + def gen_copy_telemetry(packed_telemetry, item): + def x(): + try: + telemeter = Telemeter.from_packed(packed_telemetry) + Clipboard.copy(str(telemeter.read_all())) + 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()) @@ -447,21 +461,44 @@ class Messages(): } ] 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 "lxm" in m and m["lxm"] and m["lxm"].fields != None and LXMF.FIELD_TELEMETRY in m["lxm"].fields: + packed_telemetry = m["lxm"].fields[LXMF.FIELD_TELEMETRY] + 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(packed_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) + } + ] item.dmenu = MDDropdownMenu( caller=item.ids.msg_submenu,