Fix DM presence indicator never updating live (#63)

The sidebar shows every DM's online/offline dot at once, but the only
existing signal for a presence change (member_updated) is broadcast to
a room's own channel, which Presence only delivers to a connection that
currently has that specific room joined -- never true for a DM sitting
unopened in the sidebar. Add a dedicated per-user broadcast
(dm_presence_update) sent to each of a user's DM partners on their own
per-user channel whenever their global online/offline state changes, so
the sidebar dot updates without needing that DM to be the open room.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 20:41:57 -06:00
co-authored by Claude Sonnet 5
parent d42bf114dd
commit 4cc3823adf
7 changed files with 142 additions and 5 deletions
+24
View File
@@ -8,6 +8,7 @@ from app.models import Message, MessageFile, MessageMention, Room, RoomMembershi
from app.schemas.message import ReactionSummary
from app.services.link_preview_service import fetch_and_broadcast_link_preview
from app.services.push_service import send_push_to_user
from app.services.room_service import list_dm_partner_ids
from app.services.webhook_service import dispatch_event
from app.ws.broadcaster import Broadcaster
from app.ws.focus_presence import FocusPresence
@@ -237,6 +238,29 @@ async def broadcast_member_updated(db: AsyncSession, broadcaster: Broadcaster, u
)
async def broadcast_dm_presence_update(
db: AsyncSession, broadcaster: Broadcaster, user_id: uuid.UUID, online: bool
) -> None:
"""Tells every one of user_id's DM partners that their online/offline
status just changed (#63) -- on each partner's own per-user channel,
not the DM room's channel. The room channel alone doesn't reach the
sidebar: Presence gates room-channel delivery on actually having that
specific room's channel joined right now, which is only ever the one
room currently open in the UI -- so a DM sitting unopened in the
sidebar (which is the normal case; the sidebar shows every DM's status
at once) never saw its partner's status change until something else
forced a full room-list refetch."""
for partner_id in await list_dm_partner_ids(db, user_id):
await broadcaster.publish_to_user(
partner_id,
{
"type": "dm_presence_update",
"user_id": str(user_id),
"status": "online" if online else "offline",
},
)
async def broadcast_room_added(broadcaster: Broadcaster, user_id: uuid.UUID, room: Room) -> None:
"""The only signal a user's open client gets that they were just added
to a room -- without it, GET /rooms/mine is only ever fetched once at
+21
View File
@@ -244,6 +244,27 @@ async def list_member_rooms(
]
async def list_dm_partner_ids(db: AsyncSession, user_id: uuid.UUID) -> list[uuid.UUID]:
"""Every user this user_id shares a DM with (#63) -- used to know who
needs telling about a global online/offline transition, since Presence
gates room-channel delivery on actually having that specific room
joined right now (only ever the one room currently open in the UI), so
a DM sitting unopened in the sidebar would otherwise never hear about
its partner's status changing at all."""
result = await db.execute(
select(RoomMembership.user_id)
.join(Room, Room.id == RoomMembership.room_id)
.where(
Room.is_dm.is_(True),
RoomMembership.user_id != user_id,
RoomMembership.room_id.in_(
select(RoomMembership.room_id).where(RoomMembership.user_id == user_id)
),
)
)
return [row[0] for row in result.all()]
async def get_room(db: AsyncSession, room_id: uuid.UUID) -> Room:
room = await db.get(Room, room_id)
if room is None: