feat(meshchat): update conversation previews with user display names and improve telemetry handling

This commit is contained in:
Ivan
2026-05-03 13:18:57 -05:00
parent 5eb02b2a25
commit 538c4d72cf
4 changed files with 228 additions and 19 deletions
+10 -2
View File
@@ -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,
+68 -15
View File
@@ -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(
+64
View File
@@ -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"
@@ -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(