Private
Public Access
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>
48 lines
2.0 KiB
Python
48 lines
2.0 KiB
Python
import enum
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, Enum, ForeignKey, PrimaryKeyConstraint, func
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.models.base import Base
|
|
|
|
|
|
class RoomRole(str, enum.Enum):
|
|
owner = "owner"
|
|
admin = "admin"
|
|
member = "member"
|
|
|
|
|
|
class RoomMembership(Base):
|
|
__tablename__ = "room_memberships"
|
|
__table_args__ = (PrimaryKeyConstraint("room_id", "user_id"),)
|
|
|
|
room_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("rooms.id"))
|
|
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"))
|
|
role: Mapped[RoomRole] = mapped_column(
|
|
Enum(RoomRole, name="room_role"), default=RoomRole.member, nullable=False
|
|
)
|
|
joined_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
# A server_default (not an app-code default) so every membership-creation
|
|
# call site (create_room, join_room, add_member) gets a sane starting
|
|
# point automatically: joining counts as being caught up as of then, not
|
|
# retroactively unread for the room's entire prior history.
|
|
last_read_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
# #52 follow-up: lets a DM be hidden from one participant's own sidebar
|
|
# without touching the other participant's copy or deleting anything --
|
|
# a DM has no sensible "leave" (it would corrupt find_or_create_dm's
|
|
# exactly-two-members assumption), so this is deliberately a per-viewer
|
|
# display flag on their own membership row, not a membership deletion.
|
|
# Cleared automatically (see message_events.py) whenever a new message
|
|
# arrives in the room, or when find_or_create_dm resolves back to it --
|
|
# both count as the conversation being active again.
|
|
hidden_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
room = relationship("Room", back_populates="memberships")
|
|
user = relationship("User")
|