Private
Public Access
Add the ability to hide a DM conversation (#52 follow-up)
Neither participant could get rid of a DM at all -- Leave/Delete were both deliberately hidden for DMs during the initial build to sidestep an edge case (removing a membership would break find_or_create_dm's exactly-two-members assumption), but that left no way out whatsoever. RoomMembership.hidden_at is a per-viewer display flag, not a membership deletion: hiding a DM only sets it on your own membership row, so it disappears from just your sidebar without touching the other participant's copy or any messages. It's automatically cleared (reappearing) in two cases: a new message arrives in the room (broadcast_new_message), or find_or_create_dm resolves back to the same room because either person re-opens it from the People list -- both count as the conversation being active again. Also fixes two now-flaky tests (test_message_edit, test_reactions): broadcast_new_message doing more work before returning shifted timing enough to expose a pre-existing race where a per-user-channel frame (desktop_notification/unread_update) could legitimately arrive before a connection's own "joined" ack. Broadened their existing _recv() noise-filtering helper (already used for member_updated) to cover those types too, and used it at the two call sites that were reading raw receive_json() instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models import Message, MessageFile, MessageMention, Room, RoomMembership, User
|
||||
@@ -140,6 +140,16 @@ async def broadcast_new_message(
|
||||
trigger identical fan-out/push/event behavior."""
|
||||
payload = await _message_payload(db, message, sender.username)
|
||||
await broadcaster.publish(room_id, payload)
|
||||
# A no-op for a regular room (hidden_at is only ever set on a DM's
|
||||
# membership row -- see RoomMembership.hidden_at) -- new activity
|
||||
# un-hiding a DM someone closed matches find_or_create_dm's own
|
||||
# un-hide-on-reopen behavior.
|
||||
await db.execute(
|
||||
update(RoomMembership)
|
||||
.where(RoomMembership.room_id == room_id, RoomMembership.hidden_at.is_not(None))
|
||||
.values(hidden_at=None)
|
||||
)
|
||||
await db.commit()
|
||||
await _notify_offline_members(db, broadcaster, presence, room_id, sender, message)
|
||||
await dispatch_event(db, "message.created", room_id, payload)
|
||||
_maybe_fetch_link_preview(broadcaster, room_id, message)
|
||||
|
||||
@@ -68,6 +68,10 @@ class CannotModifyDmError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotADmError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def dm_room_name(user_a_id: uuid.UUID, user_b_id: uuid.UUID) -> str:
|
||||
"""Deterministic, internal-only name for the DM room between these two
|
||||
users -- same canonical string regardless of argument order, so
|
||||
@@ -79,6 +83,27 @@ def dm_room_name(user_a_id: uuid.UUID, user_b_id: uuid.UUID) -> str:
|
||||
return f"dm:{ids[0]}:{ids[1]}"
|
||||
|
||||
|
||||
async def _unhide(db: AsyncSession, room_id: uuid.UUID, user_id: uuid.UUID) -> None:
|
||||
membership = (
|
||||
await db.execute(
|
||||
select(RoomMembership).where(
|
||||
RoomMembership.room_id == room_id, RoomMembership.user_id == user_id
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if membership is not None and membership.hidden_at is not None:
|
||||
membership.hidden_at = None
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def hide_dm(db: AsyncSession, room: Room, user_id: uuid.UUID) -> None:
|
||||
if not room.is_dm:
|
||||
raise NotADmError()
|
||||
membership = await _get_membership(db, room.id, user_id)
|
||||
membership.hidden_at = func.now()
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def create_room(db: AsyncSession, owner_id: uuid.UUID, data: RoomCreate) -> Room:
|
||||
room = Room(
|
||||
name=data.name,
|
||||
@@ -110,6 +135,7 @@ async def find_or_create_dm(db: AsyncSession, user_id: uuid.UUID, other_user_id:
|
||||
result = await db.execute(select(Room).where(Room.name == name))
|
||||
room = result.scalar_one_or_none()
|
||||
if room is not None:
|
||||
await _unhide(db, room.id, user_id)
|
||||
return room
|
||||
|
||||
# is_private=True is belt-and-suspenders here -- list_open_rooms also
|
||||
@@ -130,7 +156,9 @@ async def find_or_create_dm(db: AsyncSession, user_id: uuid.UUID, other_user_id:
|
||||
# race is the room we actually want.
|
||||
await db.rollback()
|
||||
result = await db.execute(select(Room).where(Room.name == name))
|
||||
return result.scalar_one()
|
||||
room = result.scalar_one()
|
||||
await _unhide(db, room.id, user_id)
|
||||
return room
|
||||
|
||||
db.add(RoomMembership(room_id=room.id, user_id=user_id, role=RoomRole.member))
|
||||
db.add(RoomMembership(room_id=room.id, user_id=other_user_id, role=RoomRole.member))
|
||||
@@ -180,7 +208,7 @@ async def list_member_rooms(
|
||||
Room, RoomMembership.role, RoomMembership.last_read_at, last_message_at, has_unread_mention
|
||||
)
|
||||
.join(RoomMembership, RoomMembership.room_id == Room.id)
|
||||
.where(RoomMembership.user_id == user_id)
|
||||
.where(RoomMembership.user_id == user_id, RoomMembership.hidden_at.is_(None))
|
||||
# A secondary key on the primary key -- without it, Postgres has no
|
||||
# obligation to return two same-instant rooms (a plausible tie:
|
||||
# bulk-created/migrated rooms, or just two created in quick
|
||||
|
||||
Reference in New Issue
Block a user