feat(phase3): threads backend + thread panel UI
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { create } from 'zustand';
|
||||
import { api } from '../lib/api.ts';
|
||||
import type { Message } from './message.ts';
|
||||
|
||||
export interface Thread {
|
||||
id: string;
|
||||
server_id: string;
|
||||
parent_channel_id: string;
|
||||
name: string;
|
||||
type: 'thread';
|
||||
category: string | null;
|
||||
position: number;
|
||||
archived_at: string | null;
|
||||
auto_archive_duration: number;
|
||||
message_count: number;
|
||||
last_message_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreateThreadData {
|
||||
name: string;
|
||||
message_id?: string;
|
||||
}
|
||||
|
||||
interface ThreadState {
|
||||
threadsByParent: Record<string, Thread[]>;
|
||||
activeThreadId: string | null;
|
||||
messagesByThread: Record<string, Message[]>;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
fetchThreads: (parentChannelId: string) => Promise<void>;
|
||||
createThread: (parentChannelId: string, data: CreateThreadData) => Promise<Thread>;
|
||||
archiveThread: (threadId: string) => Promise<void>;
|
||||
setActiveThread: (id: string | null) => void;
|
||||
fetchThreadMessages: (threadId: string, before?: string) => Promise<void>;
|
||||
sendThreadMessage: (threadId: string, content: string) => Promise<Message>;
|
||||
addThreadMessage: (message: Message) => void;
|
||||
}
|
||||
|
||||
export const useThreadStore = create<ThreadState>((set) => ({
|
||||
threadsByParent: {},
|
||||
activeThreadId: null,
|
||||
messagesByThread: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
fetchThreads: async (parentChannelId) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const threads = await api.get<Thread[]>(`/channels/${parentChannelId}/threads`);
|
||||
set((state) => ({
|
||||
threadsByParent: { ...state.threadsByParent, [parentChannelId]: Array.isArray(threads) ? threads : [] },
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
set({ isLoading: false, error: error instanceof Error ? error.message : 'Failed to fetch threads' });
|
||||
}
|
||||
},
|
||||
|
||||
createThread: async (parentChannelId, data) => {
|
||||
const thread = await api.post<Thread>(`/channels/${parentChannelId}/threads`, data);
|
||||
set((state) => {
|
||||
const list = state.threadsByParent[parentChannelId] || [];
|
||||
return {
|
||||
threadsByParent: { ...state.threadsByParent, [parentChannelId]: [...list, thread] },
|
||||
activeThreadId: thread.id,
|
||||
};
|
||||
});
|
||||
return thread;
|
||||
},
|
||||
|
||||
archiveThread: async (threadId) => {
|
||||
await api.patch(`/threads/${threadId}`, { archived: true });
|
||||
set((state) => {
|
||||
const next: Record<string, Thread[]> = {};
|
||||
for (const parentId of Object.keys(state.threadsByParent)) {
|
||||
next[parentId] = state.threadsByParent[parentId].map((t) =>
|
||||
t.id === threadId ? { ...t, archived_at: new Date().toISOString() } : t,
|
||||
);
|
||||
}
|
||||
return { threadsByParent: next };
|
||||
});
|
||||
},
|
||||
|
||||
setActiveThread: (id) => set({ activeThreadId: id }),
|
||||
|
||||
fetchThreadMessages: async (threadId, before) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const params = before ? `?before=${encodeURIComponent(before)}` : '';
|
||||
const messages = await api.get<Message[]>(`/channels/${threadId}/messages${params}`);
|
||||
set((state) => ({
|
||||
messagesByThread: { ...state.messagesByThread, [threadId]: Array.isArray(messages) ? messages : [] },
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
set({ isLoading: false, error: error instanceof Error ? error.message : 'Failed to fetch thread messages' });
|
||||
}
|
||||
},
|
||||
|
||||
sendThreadMessage: async (threadId, content) => {
|
||||
const message = await api.post<Message>(`/channels/${threadId}/messages`, { content });
|
||||
set((state) => {
|
||||
const list = state.messagesByThread[threadId] || [];
|
||||
return { messagesByThread: { ...state.messagesByThread, [threadId]: [...list, message] } };
|
||||
});
|
||||
return message;
|
||||
},
|
||||
|
||||
addThreadMessage: (message) =>
|
||||
set((state) => {
|
||||
const list = state.messagesByThread[message.channel_id] || [];
|
||||
if (list.some((m) => m.id === message.id)) return state;
|
||||
return { messagesByThread: { ...state.messagesByThread, [message.channel_id]: [...list, message] } };
|
||||
}),
|
||||
}));
|
||||
Reference in New Issue
Block a user