feat(web): role groups, last seen and unread markers in members, channels and spaces; lock channels without keys

This commit is contained in:
justme committed 2026-10-17 21:31:15 +08:00
1 parent 5043c798ff
commit d1ff8bab7d
23 files changed
+381 -34

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
import { create } from 'zustand';
/** 每个频道「读到哪里」,只存在这台设备的浏览器里(只是便利功能,取不到就当全读过) */
const STORAGE_KEY = 'wd.readState';
type Reads = Record<string, number>;
function load(): Reads {
try {
const parsed: unknown = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}');
return parsed && typeof parsed === 'object' ? parsed as Reads : {};
} catch { return {}; }
}
function save(reads: Reads) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(reads)); } catch { /* 隐私模式等:这次会话内仍然有效 */ }
}
/** 空间级别读到的日志序号:别的空间 last_seq 超过它,就说明有新动静 */
export const spaceSeqKey = (spaceId: string) => `seq:${spaceId}`;
export const readKey =(spaceId: string, threadId: string) => `${spaceId}:${threadId}`;
interface ReadState {
reads: Reads;
/** 把频道标记为读到 ts;只前进不后退 */
markRead: (key: string, ts: number) => void;
/** 第一次见到的频道:以此刻为基线,不把历史消息算成未读 */
baseline: (key: string, value?: number) => void;
}
export const useReadState = create<ReadState>((set, get) => ({
reads: load(),
markRead: (key, ts) => {
const current = get().reads[key];
if (current !== undefined && current >= ts) return;
const reads = { ...get().reads, [key]: ts };
save(reads); set({ reads });
},
baseline: (key, value = Date.now()) => {
if (get().reads[key] !== undefined) return;
const reads = { ...get().reads, [key]: value };
save(reads); set({ reads });
},
}));
+5
View File
@@ -16,6 +16,8 @@ export interface WorkspaceData {
connection: ConnectionState;
channel: ChannelInfo | null;
threads: ThreadInfo[];
/** 没有密钥、不能读写的频道 id */
lockedThreads: string[];
groups: ChannelGroupInfo[];
roles: RoleInfo[];
folders: FolderInfo[];
@@ -35,6 +37,7 @@ export const initialData: WorkspaceData = {
connection: 'offline',
channel: null,
threads: [],
lockedThreads: [],
groups: [],
roles: [],
folders: [],
@@ -81,6 +84,8 @@ export function reduce(s: WorkspaceData, e: WorkspaceEvent): WorkspaceData {
return { ...s, people: profiles, threads: e.state.channels.map(c => ({ ...c, groupId: c.groupId ?? undefined })), groups: e.state.groups, roles: e.state.roles ?? [],
folders: e.state.folders.map(f => ({ ...f, parentId: f.parentId ?? undefined })), locations, authors, byId };
}
case 'access':
return { ...s, lockedThreads: e.locked };
case 'reset':
return { ...initialData, connection: s.connection };
case 'error':