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

+9 -3
View File
@@ -19,6 +19,7 @@ import { useAccount } from '@/store/account';
import { permissionsOf } from '@/lib/roles';
import { ThreadSidebar } from './ThreadSidebar';
import { ChatPane } from './ChatPane';
import { useThreadReads } from './useThreadReads';
import { FileExplorer } from './FileExplorer';
import { FilePaneResize } from './FilePaneResize';
import { InviteSheet } from './InviteSheet';
@@ -78,7 +79,10 @@ export function Workspace({ onCreateChannel }: { onCreateChannel: () => void })
const [memberRevision, setMemberRevision] = useState(0);
const [fileWidth, setFileWidth] = useState<number>();
const [upload, setUpload] = useState<{ files: File[]; threadId: string; folderId?: string }>();
const activeThread = threads.find((thread) => thread.id === threadId) ?? threads.find((thread) => thread.name === '常规') ?? threads[threads.length - 1];
const locked = useWorkspace((s) => s.lockedThreads);
// 没有密钥的频道不能打开:优先用选中的,其次「常规」,再其次最新的可用频道
const usable = threads.filter((thread) => !locked.includes(thread.id));
const activeThread = usable.find((thread) => thread.id === threadId) ?? usable.find((thread) => thread.name === '常规') ?? usable[usable.length - 1];
const activeThreadId = activeThread?.id;
const activeFolders = folders.filter((folder) => folder.threadId === activeThreadId);
const activeFolderId = activeFolders.some((folder) => folder.id === folderId) ? folderId : undefined;
@@ -98,6 +102,8 @@ export function Workspace({ onCreateChannel }: { onCreateChannel: () => void })
const item = s.byId[id]; return item?.kind === 'file' && item.metadataHidden;
})));
const { unread, marker } = useThreadReads(channel?.id, threads.map((thread) => thread.id), activeThreadId);
const requestFiles = useCallback((incoming: File[], destination?: { folderId?: string }) => {
const target = destination ? destination.folderId : activeFolderId;
if (incoming.length && activeThreadId && enabled) setUpload({ files: incoming, threadId: activeThreadId, ...(target ? { folderId: target } : {}) });
@@ -131,11 +137,11 @@ export function Workspace({ onCreateChannel }: { onCreateChannel: () => void })
const channelId = channel?.id;
useEffect(() => { setDrawerOpen(false); }, [channelId]);
const rail = <SpaceRail id={channel?.id} onCreate={onCreateChannel} onJoin={() => setScanOpen(true)} />;
const threadSidebar = <ThreadSidebar threads={threads} groups={groups} selectedThreadId={activeThreadId} enabled={Boolean(channel?.canManageStructure) || perms.has('structure.manage')} canCreate={owner} owner={owner} onHistory={setChannelHistory}
const threadSidebar = <ThreadSidebar threads={threads} groups={groups} selectedThreadId={activeThreadId} unread={unread} locked={locked} enabled={Boolean(channel?.canManageStructure) || perms.has('structure.manage')} canCreate={owner} owner={owner} onHistory={setChannelHistory}
onSelectThread={(id) => { selectThread(id); setDrawerOpen(false); setMobileView('chat'); }} onCreateThread={createThread} onRenameThread={renameThread}
onCreateGroup={createGroup} onRenameGroup={renameGroup} onDeleteGroup={deleteGroup} onGroupThread={groupThread} />;
const navigation = <><SpaceHeader />{threadSidebar}{desktop ? <Notepad /> : null}</>;
const chat = <ChatPane threadName={activeThread?.name ?? '选择频道'} ids={messages} enabled={enabled} onSend={(text) => sendText(text, activeThreadId)} onDelete={deleteItem} onOpenFile={setPreviewId} onFiles={requestFiles} />;
const chat = <ChatPane threadName={activeThread?.name ?? '选择频道'} lastSeen={marker} ids={messages} enabled={enabled} onSend={(text) => sendText(text, activeThreadId)} onDelete={deleteItem} onOpenFile={setPreviewId} onFiles={requestFiles} />;
const explorer = <>
<FilterBar filter={filter} searchOpen={searchOpen} onChange={setFilter} />
<FileExplorer key={activeThreadId} scope={`${channel?.id}:${activeThreadId}`} threadId={activeThreadId} threadName={activeThread?.name ?? '选择频道'}