Private
Public Access
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:
@@ -68,6 +68,16 @@ export function hideDm(roomId: string): Promise<void> {
|
||||
return apiFetch<void>(`/api/rooms/${roomId}/hide`, { method: 'POST' })
|
||||
}
|
||||
|
||||
// #67: per-viewer opt-in for email-on-mention in this room. 400s for a DM
|
||||
// (backend rejects it -- see set_room_email_notifications), so callers
|
||||
// should only expose the toggle for a non-DM room.
|
||||
export function updateRoomNotifications(roomId: string, emailNotifications: boolean): Promise<void> {
|
||||
return apiFetch<void>(`/api/rooms/${roomId}/notifications`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ email_notifications: emailNotifications }),
|
||||
})
|
||||
}
|
||||
|
||||
export function listRoomMembers(roomId: string): Promise<RoomMember[]> {
|
||||
return apiFetch<RoomMember[]>(`/api/rooms/${roomId}/members`)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
removeMember,
|
||||
transferOwnership,
|
||||
updateRoom,
|
||||
updateRoomNotifications,
|
||||
} from '../api/rooms'
|
||||
import {
|
||||
createEventSubscription,
|
||||
@@ -106,6 +107,8 @@ export function RoomInfoPanel({
|
||||
const [descDraft, setDescDraft] = useState(room.description ?? '')
|
||||
const [isPrivateDraft, setIsPrivateDraft] = useState(room.is_private)
|
||||
const [roomError, setRoomError] = useState<string | null>(null)
|
||||
const [emailNotifications, setEmailNotifications] = useState(room.email_notifications)
|
||||
const [notificationsError, setNotificationsError] = useState<string | null>(null)
|
||||
|
||||
const [integrationsOpen, setIntegrationsOpen] = useState(false)
|
||||
const [incomingWebhooks, setIncomingWebhooks] = useState<WebhookIncoming[]>([])
|
||||
@@ -129,6 +132,7 @@ export function RoomInfoPanel({
|
||||
setNameDraft(room.name)
|
||||
setDescDraft(room.description ?? '')
|
||||
setIsPrivateDraft(room.is_private)
|
||||
setEmailNotifications(room.email_notifications)
|
||||
if (canManage) {
|
||||
listIncomingWebhooks(room.id).then(setIncomingWebhooks).catch(() => setIncomingWebhooks([]))
|
||||
listEventSubscriptions(room.id).then(setEventSubscriptions).catch(() => setEventSubscriptions([]))
|
||||
@@ -138,7 +142,7 @@ export function RoomInfoPanel({
|
||||
setEventSubscriptions([])
|
||||
setDirectoryUsers([])
|
||||
}
|
||||
}, [room.id, room.name, room.description, room.is_private, canManage])
|
||||
}, [room.id, room.name, room.description, room.is_private, room.email_notifications, canManage])
|
||||
|
||||
useEffect(() => {
|
||||
// Fetched lazily (only once expanded), not alongside the section above
|
||||
@@ -152,6 +156,19 @@ export function RoomInfoPanel({
|
||||
.catch((err) => setAttachmentsError(err instanceof ApiError ? err.message : String(err)))
|
||||
}, [filesOpen, room.id])
|
||||
|
||||
async function handleToggleEmailNotifications(enabled: boolean) {
|
||||
const previous = emailNotifications
|
||||
setEmailNotifications(enabled)
|
||||
setNotificationsError(null)
|
||||
try {
|
||||
await updateRoomNotifications(room.id, enabled)
|
||||
onRoomUpdated()
|
||||
} catch (err) {
|
||||
setEmailNotifications(previous)
|
||||
setNotificationsError(err instanceof ApiError ? err.message : String(err))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddMember(target: UserDirectoryEntry) {
|
||||
setInviteError(null)
|
||||
try {
|
||||
@@ -377,6 +394,26 @@ export function RoomInfoPanel({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{!room.is_dm && (
|
||||
<div className="room-info-section">
|
||||
<div className="toggle-row">
|
||||
<div className="toggle-label">
|
||||
<span className="t">Email me on mentions</span>
|
||||
<span className="d">Sent only while you're offline</span>
|
||||
</div>
|
||||
<label className="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={emailNotifications}
|
||||
onChange={(e) => handleToggleEmailNotifications(e.target.checked)}
|
||||
/>
|
||||
<span className="track" />
|
||||
</label>
|
||||
</div>
|
||||
{notificationsError && <p className="room-info-error">{notificationsError}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="room-info-section">
|
||||
<button type="button" className="room-info-settings-toggle" onClick={() => setFilesOpen((v) => !v)}>
|
||||
<DisclosureChevron open={filesOpen} /> Files
|
||||
|
||||
@@ -87,6 +87,9 @@ export interface MyRoomItem extends Room {
|
||||
// #52: the other participant, only for is_dm rooms -- see backend
|
||||
// schemas/room.py's MyRoomItem for why this is precomputed server-side.
|
||||
dm_partner: DmPartnerInfo | null
|
||||
// #67: this viewer's own opt-in for "email me when mentioned here while
|
||||
// offline" -- always false for a DM (see backend's message_events.py).
|
||||
email_notifications: boolean
|
||||
}
|
||||
|
||||
export interface RoomMember {
|
||||
|
||||
Reference in New Issue
Block a user