feat(phase7): per-channel notification settings and read receipts
Phase 7.1 — Per-channel notification settings
- New notification_settings table (user_id, channel_id, level)
- GET/PUT/DELETE handlers under /channels/{channelID}/notifications
- Push dispatch and @mention loops filtered by notification level
- Frontend: bell icon per channel cycling all/mentions/none
Phase 7.4 — Read receipts
- New read_states table (user_id, channel_id, last_read_message_id)
- PUT /channels/{channelID}/read and GET /users/me/read-states
- Auto-mark-read when messages load in ChatArea
- Unread dot for channels never opened
Also: favicon/icon refresh in index.html
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { create } from 'zustand';
|
||||
import { api } from '../lib/api';
|
||||
|
||||
type NotificationLevel = 'all' | 'mentions' | 'none';
|
||||
|
||||
interface NotificationSettingsState {
|
||||
settings: Record<string, NotificationLevel>;
|
||||
initialized: boolean;
|
||||
loaded: boolean;
|
||||
fetchSettings: () => Promise<void>;
|
||||
setLevel: (channelId: string, level: NotificationLevel) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useNotificationSettingsStore = create<NotificationSettingsState>()((set) => ({
|
||||
settings: {},
|
||||
initialized: false,
|
||||
loaded: false,
|
||||
|
||||
fetchSettings: async () => {
|
||||
try {
|
||||
const settings = await api.get<Record<string, NotificationLevel>>('/channels/me/notifications');
|
||||
set({ settings, loaded: true, initialized: true });
|
||||
} catch {
|
||||
set({ loaded: true, initialized: true });
|
||||
}
|
||||
},
|
||||
|
||||
setLevel: async (channelId: string, level: NotificationLevel) => {
|
||||
await api.put(`/channels/${channelId}/notifications`, { level });
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, [channelId]: level },
|
||||
}));
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,41 @@
|
||||
import { create } from 'zustand';
|
||||
import { api } from '../lib/api';
|
||||
|
||||
interface ReadStatesState {
|
||||
states: Record<string, string>; // channel_id -> last_read_message_id
|
||||
loaded: boolean;
|
||||
fetchStates: () => Promise<void>;
|
||||
markRead: (channelId: string, messageId: string) => Promise<void>;
|
||||
hasUnread: (channelId: string) => boolean; // true if no read state (never viewed)
|
||||
}
|
||||
|
||||
export const useReadStatesStore = create<ReadStatesState>()((set, get) => ({
|
||||
states: {},
|
||||
loaded: false,
|
||||
|
||||
fetchStates: async () => {
|
||||
try {
|
||||
const states = await api.get<Record<string, string>>('/users/me/read-states');
|
||||
set({ states, loaded: true });
|
||||
} catch {
|
||||
set({ loaded: true });
|
||||
}
|
||||
},
|
||||
|
||||
markRead: async (channelId: string, messageId: string) => {
|
||||
set((state) => ({
|
||||
states: { ...state.states, [channelId]: messageId },
|
||||
}));
|
||||
try {
|
||||
await api.put(`/channels/${channelId}/read`, { last_read_message_id: messageId });
|
||||
} catch {
|
||||
// optimistic update, ignore failure
|
||||
}
|
||||
},
|
||||
|
||||
hasUnread: (channelId: string): boolean => {
|
||||
const state = get().states;
|
||||
// No read state means the user has never viewed this channel
|
||||
return !(channelId in state);
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user