mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-11 07:26:53 +00:00
feat(meshchat): update conversation previews with user display names and improve telemetry handling
This commit is contained in:
+10
-2
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user