Files
ds-chat/backend/app/models/membership.py
T
ksmithandClaude Sonnet 5 68e487e5ec Add unread message indicators for rooms (#38)
Shows a dot on rooms with unread messages in the sidebar, updated live
over WebSocket. Reuses the same offline-member audience computation
already used for push notifications: a member gets the real-time signal
whenever they aren't currently connected to that room's channel, which
correctly covers both "room not open" and "room open but tab
backgrounded" (the client leaves a room's channel while hidden).

Persisted server-side via a new room_memberships.last_read_at column so
state survives reload and stays consistent across devices, advanced by
an explicit mark-read call the frontend makes on room-open and on each
live message received while the room is genuinely visible -- gated on a
live visibility check, not a cached ref, so a backgrounded-but-open room
keeps accumulating unread instead of auto-marking-read the instant a
message arrives somewhere it can't be seen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 20:44:20 -06:00

39 lines
1.4 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
)
room = relationship("Room", back_populates="memberships")
user = relationship("User")