Add per-room opt-in email notifications on mention (#67)

Lets a member subscribe to email when they're @mentioned in a room
while offline, alongside #66's always-on DM email. Debounced the same
way #66 is (one email per unread burst, not one per mention), and
deliberately scoped to regular rooms only -- DMs already have #66's
automatic offline email with no separate toggle needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 19:45:04 -06:00
co-authored by Claude Sonnet 5
parent 3dabf0022c
commit 250bb862f6
10 changed files with 441 additions and 6 deletions
+25 -3
View File
@@ -182,7 +182,7 @@ async def list_open_rooms(db: AsyncSession, user_id: uuid.UUID) -> list[tuple[Ro
async def list_member_rooms(
db: AsyncSession, user_id: uuid.UUID
) -> list[tuple[Room, RoomRole, bool, bool, User | None]]:
) -> list[tuple[Room, RoomRole, bool, bool, bool, User | None]]:
last_message_at = (
select(func.max(Message.created_at))
.where(Message.room_id == Room.id)
@@ -205,7 +205,12 @@ async def list_member_rooms(
)
result = await db.execute(
select(
Room, RoomMembership.role, RoomMembership.last_read_at, last_message_at, has_unread_mention
Room,
RoomMembership.role,
RoomMembership.last_read_at,
last_message_at,
has_unread_mention,
RoomMembership.email_notifications,
)
.join(RoomMembership, RoomMembership.room_id == Room.id)
.where(RoomMembership.user_id == user_id, RoomMembership.hidden_at.is_(None))
@@ -238,9 +243,10 @@ async def list_member_rooms(
role,
last_message_at is not None and last_message_at > last_read_at,
has_mention,
email_notifications,
partners_by_room.get(room.id),
)
for room, role, last_read_at, last_message_at, has_mention in rows
for room, role, last_read_at, last_message_at, has_mention, email_notifications in rows
]
@@ -486,6 +492,22 @@ async def mark_room_read(db: AsyncSession, room_id: uuid.UUID, user_id: uuid.UUI
await db.commit()
async def set_room_email_notifications(
db: AsyncSession, room_id: uuid.UUID, user_id: uuid.UUID, enabled: bool
) -> None:
"""#67: DMs are deliberately excluded -- they already get #66's
automatic offline email with no opt-in needed, and this setting only
makes sense for a regular room's mention-based notifications."""
room = await db.get(Room, room_id)
if room is None:
raise RoomNotFoundError()
if room.is_dm:
raise CannotModifyDmError()
membership = await _get_membership(db, room_id, user_id)
membership.email_notifications = enabled
await db.commit()
async def leave_room(db: AsyncSession, room_id: uuid.UUID, user_id: uuid.UUID) -> None:
membership = await _get_membership(db, room_id, user_id)
if membership.role == RoomRole.owner: