From 538c4d72cfdeebf10e40333522463f615562d783 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 3 May 2026 13:18:57 -0500 Subject: [PATCH] feat(meshchat): update conversation previews with user display names and improve telemetry handling --- meshchatx/meshchat.py | 12 ++- meshchatx/src/backend/lxmf_utils.py | 83 +++++++++++++---- tests/backend/test_lxmf_utils_extended.py | 64 ++++++++++++++ .../test_notification_user_facing_filter.py | 88 ++++++++++++++++++- 4 files changed, 228 insertions(+), 19 deletions(-) diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py index 4b875a2..734cb19 100644 --- a/meshchatx/meshchat.py +++ b/meshchatx/meshchat.py @@ -10971,6 +10971,10 @@ class ReticulumMeshChat: icon = self.database.misc.get_user_icon(other_user_hash) + peer_preview_name = ( + custom_display_name or display_name or "Anonymous Peer" + ) + conversations.append( { "type": "lxmf_message", @@ -10979,8 +10983,12 @@ class ReticulumMeshChat: "custom_display_name": custom_display_name, "lxmf_user_icon": dict(icon) if icon else None, "latest_message_preview": ( - latest_message_data["content"] or "" - )[:100], + lxmf_sidebar_preview_for_conversation_latest_row( + dict(latest_for_preview), + local_hash=local_hash, + peer_display_name=peer_preview_name, + )[:100] + ), "updated_at": datetime.fromtimestamp( latest_message_data["timestamp"] or 0, UTC, diff --git a/meshchatx/src/backend/lxmf_utils.py b/meshchatx/src/backend/lxmf_utils.py index 6c2dac9..34eeb48 100644 --- a/meshchatx/src/backend/lxmf_utils.py +++ b/meshchatx/src/backend/lxmf_utils.py @@ -28,10 +28,15 @@ def is_user_facing_lxmf_payload(fields, content, title) -> bool: Messages that should NOT raise the notification bell: - reactions (Columba app_extensions.reaction_to with no other payload) - - telemetry-only messages (FIELD_TELEMETRY / fields["telemetry"] with no body) + - bare telemetry updates with no coordinates, no stream, and no + Sideband location-request command (FIELD_TELEMETRY body-only noise) - icon-only / appearance-only updates (no body, no attachment) - empty pings (no content, no title, no attachment) + Location shares (telemetry including ``location``), telemetry streams, + and Sideband ``commands`` entries with key ``0x01`` (location request) ARE + treated as user-facing so the bell and previews stay informative. + The helper is intentionally tolerant: ``fields`` may be the rich dict produced by :func:`convert_lxmf_message_to_dict` (string keys), the raw LXMF integer-keyed dict, or a JSON-string from the database. @@ -92,6 +97,22 @@ def is_user_facing_lxmf_payload(fields, content, title) -> bool: if isinstance(raw_files, list) and len(raw_files) > 0: return True + telemetry = fields.get("telemetry") + if isinstance(telemetry, dict): + loc = telemetry.get("location") + if isinstance(loc, dict) and loc: + return True + + ts = fields.get("telemetry_stream") + if isinstance(ts, list) and len(ts) > 0: + return True + + commands = fields.get("commands") + if isinstance(commands, list): + for cmd in commands: + if isinstance(cmd, dict) and "0x01" in cmd: + return True + return False @@ -109,6 +130,22 @@ def _reaction_emoji_from_parsed_lxmf_fields(fields: dict) -> str | None: return None +def _lxmf_sidebar_actor_label( + row: dict, + *, + local_hash: str, + peer_display_name: str, +) -> str: + is_incoming = bool(row.get("is_incoming")) + if is_incoming: + return peer_display_name or "Anonymous Peer" + src = (row.get("source_hash") or "").lower() + loc = (local_hash or "").lower() + return ( + "You" if src and loc and src == loc else (peer_display_name or "Anonymous Peer") + ) + + def lxmf_sidebar_preview_for_conversation_latest_row( row: dict, *, @@ -131,23 +168,39 @@ def lxmf_sidebar_preview_for_conversation_latest_row( except (json.JSONDecodeError, TypeError): fields = {} + actor = _lxmf_sidebar_actor_label( + row, + local_hash=local_hash, + peer_display_name=peer_display_name, + ) + incoming = bool(row.get("is_incoming")) + emoji = _reaction_emoji_from_parsed_lxmf_fields(fields) - if not emoji: - return str(content or "") + if emoji: + return f"{actor} reacted {emoji}" - is_incoming = bool(row.get("is_incoming")) - if is_incoming: - actor = peer_display_name or "Anonymous Peer" - else: - src = (row.get("source_hash") or "").lower() - loc = (local_hash or "").lower() - actor = ( - "You" - if src and loc and src == loc - else (peer_display_name or "Anonymous Peer") - ) + telemetry = fields.get("telemetry") + if isinstance(telemetry, dict): + loc = telemetry.get("location") + if isinstance(loc, dict) and loc: + return f"{actor} shared their location" - return f"{actor} reacted {emoji}" + ts = fields.get("telemetry_stream") + if isinstance(ts, list) and len(ts) > 0: + return f"{actor} sent a telemetry stream" + + if isinstance(telemetry, dict) and len(telemetry) > 0: + return f"{actor} sent telemetry" + + commands = fields.get("commands") + if isinstance(commands, list): + for cmd in commands: + if isinstance(cmd, dict) and "0x01" in cmd: + if incoming: + return f"{actor} requested your location" + return f"{actor} sent a location request" + + return str(content or "") def convert_lxmf_message_to_dict( diff --git a/tests/backend/test_lxmf_utils_extended.py b/tests/backend/test_lxmf_utils_extended.py index 43aca00..850289d 100644 --- a/tests/backend/test_lxmf_utils_extended.py +++ b/tests/backend/test_lxmf_utils_extended.py @@ -334,3 +334,67 @@ def test_sidebar_preview_prefers_non_empty_content(): peer_display_name="Eve", ) assert out == " hi " + + +def test_sidebar_preview_telemetry_location_incoming(): + local = "a" * 32 + row = { + "content": "", + "fields": json.dumps( + {"telemetry": {"location": {"latitude": 1.0, "longitude": 2.0}}}, + ), + "is_incoming": 1, + "source_hash": "b" * 32, + } + out = lxmf_sidebar_preview_for_conversation_latest_row( + row, + local_hash=local, + peer_display_name="Riley", + ) + assert out == "Riley shared their location" + + +def test_sidebar_preview_location_request_outbound_you(): + me = "c" * 32 + row = { + "content": "", + "fields": json.dumps({"commands": [{"0x01": 1_700_000_000}]}), + "is_incoming": 0, + "source_hash": me, + } + out = lxmf_sidebar_preview_for_conversation_latest_row( + row, + local_hash=me, + peer_display_name="Sam", + ) + assert out == "You sent a location request" + + +def test_sidebar_preview_telemetry_battery_only(): + row = { + "content": "", + "fields": json.dumps({"telemetry": {"battery": {"charge_percent": 50}}}), + "is_incoming": 1, + "source_hash": "b" * 32, + } + out = lxmf_sidebar_preview_for_conversation_latest_row( + row, + local_hash="a" * 32, + peer_display_name="Taylor", + ) + assert out == "Taylor sent telemetry" + + +def test_sidebar_preview_telemetry_stream(): + row = { + "content": "", + "fields": json.dumps({"telemetry_stream": [{"t": 1}]}), + "is_incoming": 1, + "source_hash": "b" * 32, + } + out = lxmf_sidebar_preview_for_conversation_latest_row( + row, + local_hash="a" * 32, + peer_display_name="Jordan", + ) + assert out == "Jordan sent a telemetry stream" diff --git a/tests/backend/test_notification_user_facing_filter.py b/tests/backend/test_notification_user_facing_filter.py index c8addd6..714a6a1 100644 --- a/tests/backend/test_notification_user_facing_filter.py +++ b/tests/backend/test_notification_user_facing_filter.py @@ -10,8 +10,10 @@ Covers: - the DAO method :func:`MessageDAO.get_latest_user_facing_incoming_message` - end-to-end ``GET /api/v1/notifications`` integration: reactions, - telemetry-only, icon-only, empty pings and delivery-status updates - must not produce false unread badges or empty dropdown entries + generic telemetry-only payloads, icon-only, empty pings and + delivery-status updates must not produce false unread badges or empty + dropdown entries; location shares, telemetry streams, and Sideband + location requests must surface with a readable preview. """ from __future__ import annotations @@ -75,6 +77,18 @@ class TestIsUserFacingLxmfPayload: fields = {"telemetry": {"some": "data"}} assert not is_user_facing_lxmf_payload(fields, "", "") + def test_telemetry_with_location_is_user_facing(self): + fields = {"telemetry": {"location": {"latitude": 1.0, "longitude": 2.0}}} + assert is_user_facing_lxmf_payload(fields, "", "") + + def test_telemetry_stream_is_user_facing(self): + fields = {"telemetry_stream": [{"x": 1}]} + assert is_user_facing_lxmf_payload(fields, "", "") + + def test_sideband_location_request_command_is_user_facing(self): + fields = {"commands": [{"0x01": 1_700_000_000}]} + assert is_user_facing_lxmf_payload(fields, "", "") + def test_icon_only_is_not_user_facing(self): # Icon appearance updates are processed separately and never appear in # the converted ``fields`` dict; an icon-only message therefore looks @@ -167,6 +181,19 @@ class TestRequireUserFacingFlag: is False ) + def test_telemetry_location_unread_when_require_user_facing(self): + row = _row( + incoming=1, + fields={"telemetry": {"location": {"latitude": 0.0, "longitude": 0.0}}}, + ) + assert ( + compute_lxmf_conversation_unread_from_latest_row( + row, + require_user_facing=True, + ) + is True + ) + def test_user_facing_message_still_unread(self): row = _row(incoming=1, content="hello") assert ( @@ -305,6 +332,24 @@ class TestGetLatestUserFacingIncomingMessage: result = db.messages.get_latest_user_facing_incoming_message(PEER_HASH) assert result is None + def test_returns_incoming_location_telemetry(self, db): + db.messages.upsert_lxmf_message( + _mk_message( + msg_hash="loc1", + peer_hash=PEER_HASH, + content="", + fields={ + "telemetry": { + "location": {"latitude": 1.0, "longitude": 2.0}, + }, + }, + timestamp=200, + ), + ) + result = db.messages.get_latest_user_facing_incoming_message(PEER_HASH) + assert result is not None + assert result["hash"] == "loc1" + def test_skips_outgoing_messages(self, db): db.messages.upsert_lxmf_message( _mk_message( @@ -527,6 +572,45 @@ class TestNotificationsGetUserFacingFilter: assert body["unread_count"] == 0 assert body["notifications"] == [] + async def test_telemetry_location_raises_bell_with_preview(self, bell_app): + bell_app.database.messages.upsert_lxmf_message( + _mk_message( + msg_hash="loc1", + peer_hash=PEER_HASH, + content="", + fields={ + "telemetry": { + "location": {"latitude": 1.0, "longitude": 2.0}, + }, + }, + timestamp=1_700_000_000, + ), + ) + body = await self._get(bell_app, unread="true", limit=10) + assert body["unread_count"] == 1 + assert len(body["notifications"]) == 1 + peer_label = f"peer-{PEER_HASH[:6]}" + assert body["notifications"][0]["latest_message_preview"] == ( + f"{peer_label} shared their location" + ) + + async def test_sideband_location_request_raises_bell_with_preview(self, bell_app): + bell_app.database.messages.upsert_lxmf_message( + _mk_message( + msg_hash="req1", + peer_hash=PEER_HASH, + content="", + fields={"commands": [{"0x01": 1_700_000_000}]}, + timestamp=1_700_000_000, + ), + ) + body = await self._get(bell_app, unread="true", limit=10) + assert body["unread_count"] == 1 + peer_label = f"peer-{PEER_HASH[:6]}" + assert body["notifications"][0]["latest_message_preview"] == ( + f"{peer_label} requested your location" + ) + async def test_empty_payload_does_not_raise_bell(self, bell_app): bell_app.database.messages.upsert_lxmf_message( _mk_message(