From e115464cc815350c7f4401bbe24c89991499d56d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Pav=C3=B3n=20Abi=C3=A1n?= Date: Wed, 18 Feb 2026 08:46:17 +0100 Subject: [PATCH] Fix unknown sender names in Bluesky chats, reduce false notifications, and reorder Chats buffer Resolve sender DIDs to display names by building member maps from conversation data. Fix compose functions to prefer snake_case attributes (ATProto SDK convention). Ensure stable key comparison in dedup logic by converting ATProto objects to strings. Move Chats buffer to appear after Mentions and before Notifications. Co-Authored-By: Claude Opus 4.6 --- src/controller/blueski/handler.py | 20 +++++------ src/controller/buffers/blueski/base.py | 6 ++-- src/controller/buffers/blueski/chat.py | 47 ++++++++++++++++++++++++-- src/sessions/blueski/compose.py | 32 +++++++++++++++--- src/sessions/blueski/session.py | 12 +++++++ 5 files changed, 97 insertions(+), 20 deletions(-) diff --git a/src/controller/blueski/handler.py b/src/controller/blueski/handler.py index bdd79f27..c99b9cec 100644 --- a/src/controller/blueski/handler.py +++ b/src/controller/blueski/handler.py @@ -95,6 +95,16 @@ class Handler: start=False, kwargs=dict(parent=controller.view.nb, name="mentions", session=session, sound="mention_received.ogg") ) + # Chats + pub.sendMessage( + "createBuffer", + buffer_type="ConversationListBuffer", + session_type="blueski", + buffer_title=_("Chats"), + parent_tab=root_position, + start=False, + kwargs=dict(parent=controller.view.nb, name="direct_messages", session=session, sound="dm_received.ogg") + ) # Notifications pub.sendMessage( "createBuffer", @@ -155,16 +165,6 @@ class Handler: start=False, kwargs=dict(parent=controller.view.nb, name="blocked", session=session) ) - # Chats - pub.sendMessage( - "createBuffer", - buffer_type="ConversationListBuffer", - session_type="blueski", - buffer_title=_("Chats"), - parent_tab=root_position, - start=False, - kwargs=dict(parent=controller.view.nb, name="direct_messages", session=session, sound="dm_received.ogg") - ) # Timelines container pub.sendMessage( diff --git a/src/controller/buffers/blueski/base.py b/src/controller/buffers/blueski/base.py index 2efd6abf..24173bae 100644 --- a/src/controller/buffers/blueski/base.py +++ b/src/controller/buffers/blueski/base.py @@ -1046,13 +1046,13 @@ class BaseBuffer(base.Buffer): author = it.get("author") if isinstance(author, dict): return author.get("did") or author.get("handle") - # Chat message fallback + # Chat message fallback — use str() to ensure stable hash/comparison sent_at = it.get("sentAt") or it.get("sent_at") or it.get("createdAt") or it.get("created_at") sender = it.get("sender") or (nested.get("sender") if isinstance(nested, dict) else {}) or {} sender_did = sender.get("did") if isinstance(sender, dict) else None text = it.get("text") or (nested.get("text") if isinstance(nested, dict) else None) if sent_at or sender_did or text: - return (sent_at, sender_did, text) + return (str(sent_at) if sent_at else None, str(sender_did) if sender_did else None, str(text) if text else None) return None post = getattr(it, "post", None) if post is not None: @@ -1075,7 +1075,7 @@ class BaseBuffer(base.Buffer): sender_did = getattr(sender, "did", None) if sender is not None else None text = getattr(it, "text", None) or (getattr(nested, "text", None) if nested is not None else None) if sent_at or sender_did or text: - return (sent_at, sender_did, text) + return (str(sent_at) if sent_at else None, str(sender_did) if sender_did else None, str(text) if text else None) return None for item in self.session.db[self.name]: diff --git a/src/controller/buffers/blueski/chat.py b/src/controller/buffers/blueski/chat.py index a238e67a..b51ac948 100644 --- a/src/controller/buffers/blueski/chat.py +++ b/src/controller/buffers/blueski/chat.py @@ -95,21 +95,21 @@ class ConversationListBuffer(BaseBuffer): for attr in ("id", "messageId", "message_id", "msgId", "msg_id", "cid", "rev"): val = g(last_msg, attr) if val: - return val + return str(val) nested = g(last_msg, "message") or g(last_msg, "record") if nested: for attr in ("id", "messageId", "message_id", "msgId", "msg_id", "cid", "rev"): val = g(nested, attr) if val: - return val + return str(val) sent_at = g(last_msg, "sentAt") or g(last_msg, "sent_at") or g(last_msg, "createdAt") or g(last_msg, "created_at") sender = g(last_msg, "sender") or (g(nested, "sender") if nested else {}) or {} sender_did = g(sender, "did") text = g(last_msg, "text") or (g(nested, "text") if nested else None) if sent_at or sender_did or text: - return (sent_at, sender_did, text) + return (str(sent_at) if sent_at else None, str(sender_did) if sender_did else None, str(text) if text else None) return None def get_formatted_message(self): @@ -181,12 +181,33 @@ class ConversationListBuffer(BaseBuffer): try: res = self.session.list_convos(limit=count) items = res.get("items", []) + self._build_member_maps(items) return self._merge_conversations(items, play_sound, avoid_autoreading=avoid_autoreading) except Exception: log.exception("Error fetching conversations") output.speak(_("Error loading conversations."), True) return 0 + def _build_member_maps(self, convos): + """Build DID→name maps from conversation members and store in db for chat buffers.""" + def g(obj, key, default=None): + if isinstance(obj, dict): + return obj.get(key, default) + return getattr(obj, key, default) + for convo in convos: + convo_id = self.get_convo_id(convo) + if not convo_id: + continue + members = g(convo, "members", []) or [] + member_map = {} + for m in members: + did = g(m, "did", None) + if did: + name = g(m, "display_name") or g(m, "displayName") or g(m, "handle", "unknown") + member_map[did] = name + if member_map: + self.session.db["convo_" + str(convo_id) + "_members"] = member_map + def _merge_conversations(self, items, play_sound=True, avoid_autoreading=False): """Merge conversation list, updating items without duplicating or re-alerting.""" if self.session.db.get(self.name) is None: @@ -395,14 +416,34 @@ class ChatBuffer(BaseBuffer): self.type = "chat_messages" self.convo_id = kwargs.get("convo_id") self.sound = "dm_received.ogg" + self._member_map_loaded = False def create_buffer(self, parent, name): self.buffer = BlueskiPanels.ChatMessagePanel(parent, name) self.buffer.session = self.session + def _update_member_map(self): + """Fetch conversation members to build a DID-to-name map for sender resolution.""" + try: + convo = self.session.get_convo(self.convo_id) + if not convo: + return + member_map = {} + for m in getattr(convo, "members", []) or []: + did = getattr(m, "did", None) + if did: + name = getattr(m, "display_name", None) or getattr(m, "handle", None) or "unknown" + member_map[did] = name + self.session.db[self.name + "_members"] = member_map + except Exception: + log.exception("Error fetching conversation members for DID resolution") + def start_stream(self, mandatory=False, play_sound=True): if not self.convo_id: return 0 + if not self._member_map_loaded: + self._update_member_map() + self._member_map_loaded = True count = self.get_max_items() try: res = self.session.get_convo_messages(self.convo_id, limit=count) diff --git a/src/sessions/blueski/compose.py b/src/sessions/blueski/compose.py index 18f030fa..a87bf630 100644 --- a/src/sessions/blueski/compose.py +++ b/src/sessions/blueski/compose.py @@ -366,17 +366,25 @@ def compose_convo(convo, db, settings, relative_times, show_screen_names=False, members = g(convo, "members", []) self_did = db.get("user_id") if isinstance(db, dict) else None + # Build a local DID→name map from conversation members for sender resolution + member_names = {} + for m in members: + did = g(m, "did", None) + if did: + name = g(m, "display_name") or g(m, "displayName") or g(m, "handle", "unknown") + member_names[did] = name + # Get other participants (exclude self) others = [] for m in members: did = g(m, "did", None) if self_did and did == self_did: continue - label = g(m, "displayName") or g(m, "display_name") or g(m, "handle", "unknown") + label = member_names.get(did, "unknown") if did else g(m, "display_name") or g(m, "displayName") or g(m, "handle", "unknown") others.append(label) if not others: - others = [g(m, "displayName") or g(m, "display_name") or g(m, "handle", "unknown") for m in members] + others = [member_names.get(g(m, "did"), "unknown") if g(m, "did") else "unknown" for m in members] participants = ", ".join(others) @@ -389,7 +397,14 @@ def compose_convo(convo, db, settings, relative_times, show_screen_names=False, last_text = g(last_msg_obj, "text", "") sender = g(last_msg_obj, "sender", None) if sender: - last_sender = g(sender, "displayName") or g(sender, "display_name") or g(sender, "handle", "") + last_sender = g(sender, "display_name") or g(sender, "displayName") or g(sender, "handle") + if not last_sender: + # Resolve DID via local member map + sdid = g(sender, "did") + if sdid: + last_sender = member_names.get(sdid, "") + if not last_sender: + last_sender = sdid or "" # Date date_str = "" @@ -434,7 +449,16 @@ def compose_chat_message(msg, db, settings, relative_times, show_screen_names=Fa return getattr(obj, key, default) sender = g(msg, "sender", {}) - handle = g(sender, "displayName") or g(sender, "display_name") or g(sender, "handle", "unknown") + sender_did = g(sender, "did") + handle = g(sender, "display_name") or g(sender, "displayName") or g(sender, "handle") + if not handle and sender_did and isinstance(db, dict): + # Look up DID in member maps stored by ChatBuffer + for key, val in db.items(): + if key.endswith("_members") and isinstance(val, dict) and sender_did in val: + handle = val[sender_did] + break + if not handle: + handle = sender_did or "unknown" text = g(msg, "text", "") diff --git a/src/sessions/blueski/session.py b/src/sessions/blueski/session.py index a23e82e6..8e5810fe 100644 --- a/src/sessions/blueski/session.py +++ b/src/sessions/blueski/session.py @@ -751,6 +751,18 @@ class Session(base.baseSession): log.exception("Error listing conversations") return {"items": [], "cursor": None} + def get_convo(self, convo_id: str): + """Fetch a single conversation by ID, returning the convo object or None.""" + api = self._ensure_client() + dm_client = api.with_bsky_chat_proxy() + dm = dm_client.chat.bsky.convo + try: + res = dm.get_convo({"convoId": convo_id}) + return res.convo + except Exception: + log.exception("Error fetching conversation %s", convo_id) + return None + def get_convo_messages(self, convo_id: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]: api = self._ensure_client() dm_client = api.with_bsky_chat_proxy()