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 <noreply@anthropic.com>
This commit is contained in:
Jesús Pavón Abián
2026-02-18 08:46:17 +01:00
parent 83781e521e
commit e115464cc8
5 changed files with 97 additions and 20 deletions

View File

@@ -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(

View File

@@ -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]:

View File

@@ -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)

View File

@@ -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", "")

View File

@@ -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()