sync: phase 1 backend + frontend from server

This commit is contained in:
2026-06-30 09:26:03 -04:00
parent d4cdd89544
commit 30e159cbdb
31 changed files with 4758 additions and 114 deletions
+120
View File
@@ -0,0 +1,120 @@
import { create } from "zustand";
import { api } from "../lib/api.ts";
export interface ConversationMember {
id: string;
username: string;
display_name: string;
avatar: string;
}
export interface Conversation {
id: string;
type: "dm" | "group_dm";
name: string;
members: ConversationMember[];
created_at: string;
}
export interface ConversationMessage {
id: string;
conversation_id: string;
author_id: string;
author_username: string;
author_display_name: string | null;
content: string;
created_at: string;
edited_at: string | null;
}
interface ConversationState {
conversations: Conversation[];
activeConversationId: string | null;
messagesByConversation: Record<string, ConversationMessage[]>;
isLoading: boolean;
error: string | null;
fetchConversations: () => Promise<void>;
createConversation: (userIds: string[]) => Promise<Conversation>;
setActiveConversation: (id: string | null) => void;
fetchMessages: (conversationId: string, before?: string) => Promise<void>;
sendMessage: (conversationId: string, content: string) => Promise<ConversationMessage>;
addMessage: (message: ConversationMessage) => void;
}
export const useConversationStore = create<ConversationState>((set, _get) => ({
conversations: [],
activeConversationId: null,
messagesByConversation: {},
isLoading: false,
error: null,
fetchConversations: async () => {
try {
const conversations = await api.get<Conversation[]>("/conversations");
set({ conversations: Array.isArray(conversations) ? conversations : [] });
} catch (error) {
set({
error: error instanceof Error ? error.message : "Failed to fetch conversations",
});
}
},
createConversation: async (userIds) => {
const conversation = await api.post<Conversation>("/conversations", { user_ids: userIds });
set((state) => ({
conversations: [conversation, ...state.conversations],
activeConversationId: conversation.id,
}));
return conversation;
},
setActiveConversation: (id) => set({ activeConversationId: id }),
fetchMessages: async (conversationId, before) => {
set({ isLoading: true });
try {
const params = before ? "?before=" + encodeURIComponent(before) : "";
const messages = await api.get<ConversationMessage[]>(
`/conversations/${conversationId}/messages${params}`,
);
set((state) => ({
messagesByConversation: {
...state.messagesByConversation,
[conversationId]: Array.isArray(messages) ? messages : [],
},
isLoading: false,
}));
} catch (error) {
set({ isLoading: false, error: error instanceof Error ? error.message : "Failed" });
}
},
sendMessage: async (conversationId, content) => {
const message = await api.post<ConversationMessage>(
`/conversations/${conversationId}/messages`,
{ content },
);
set((state) => ({
messagesByConversation: {
...state.messagesByConversation,
[conversationId]: [
...(state.messagesByConversation[conversationId] || []),
message,
],
},
}));
return message;
},
addMessage: (message) => {
set((state) => ({
messagesByConversation: {
...state.messagesByConversation,
[message.conversation_id]: [
...(state.messagesByConversation[message.conversation_id] || []),
message,
],
},
}));
},
}));
+11
View File
@@ -0,0 +1,11 @@
import { create } from 'zustand';
interface LayoutState {
isDM: boolean;
setDM: (value: boolean) => void;
}
export const useLayoutStore = create<LayoutState>((set) => ({
isDM: false,
setDM: (value) => set({ isDM: value }),
}));
+36 -3
View File
@@ -1,6 +1,15 @@
import { create } from "zustand";
import { api } from "../lib/api.ts";
export interface MessageEmbed {
id?: string;
url: string;
title?: string;
description?: string;
image_url?: string;
site_name?: string;
}
export interface Message {
id: string;
channel_id: string;
@@ -8,16 +17,24 @@ export interface Message {
author_username: string;
author_display_name: string | null;
content: string;
reply_to?: string | null;
embeds?: MessageEmbed[];
created_at: string;
edited_at: string | null;
}
export interface SearchResultMessage extends Message {
channel_name?: string;
}
interface MessageState {
messagesByChannel: Record<string, Message[]>;
searchResultsByChannel: Record<string, SearchResultMessage[]>;
isLoading: boolean;
error: string | null;
fetchMessages: (channelId: string, before?: string) => Promise<void>;
sendMessage: (channelId: string, content: string) => Promise<Message>;
sendMessage: (channelId: string, content: string, replyTo?: string) => Promise<Message>;
searchMessages: (channelId: string, query: string) => Promise<SearchResultMessage[]>;
addMessage: (message: Message) => void;
updateMessage: (message: Message) => void;
removeMessage: (channelId: string, messageId: string) => void;
@@ -25,6 +42,7 @@ interface MessageState {
export const useMessageStore = create<MessageState>((set) => ({
messagesByChannel: {},
searchResultsByChannel: {},
isLoading: false,
error: null,
@@ -53,10 +71,25 @@ export const useMessageStore = create<MessageState>((set) => ({
}
},
sendMessage: async (channelId, content) => {
searchMessages: async (channelId, query) => {
const results = await api.get<SearchResultMessage[]>(
`/channels/${channelId}/messages/search?q=${encodeURIComponent(query)}`,
);
set((state) => ({
searchResultsByChannel: {
...state.searchResultsByChannel,
[channelId]: Array.isArray(results) ? results : [],
},
}));
return Array.isArray(results) ? results : [];
},
sendMessage: async (channelId, content, replyTo) => {
const body: { content: string; reply_to?: string } = { content };
if (replyTo) body.reply_to = replyTo;
const message = await api.post<Message>(
`/channels/${channelId}/messages`,
{ content },
body,
);
set((state) => {
const list = state.messagesByChannel[channelId] || [];
+67
View File
@@ -0,0 +1,67 @@
import { create } from 'zustand';
import { api } from '../lib/api.ts';
export interface ModerationState {
isLoading: boolean;
error: string | null;
kickMember: (serverId: string, userId: string, reason?: string) => Promise<void>;
banMember: (serverId: string, userId: string, reason?: string) => Promise<void>;
unbanMember: (serverId: string, userId: string) => Promise<void>;
muteMember: (serverId: string, userId: string, durationSeconds: number) => Promise<void>;
clearError: () => void;
}
export const useModerationStore = create<ModerationState>((set) => ({
isLoading: false,
error: null,
kickMember: async (serverId, userId, reason) => {
set({ isLoading: true, error: null });
try {
await api.post(`/servers/${serverId}/members/${userId}/kick`, { reason });
set({ isLoading: false });
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to kick member';
set({ isLoading: false, error: message });
throw error;
}
},
banMember: async (serverId, userId, reason) => {
set({ isLoading: true, error: null });
try {
await api.post(`/servers/${serverId}/members/${userId}/ban`, { reason });
set({ isLoading: false });
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to ban member';
set({ isLoading: false, error: message });
throw error;
}
},
unbanMember: async (serverId, userId) => {
set({ isLoading: true, error: null });
try {
await api.post(`/servers/${serverId}/members/${userId}/unban`, {});
set({ isLoading: false });
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to unban member';
set({ isLoading: false, error: message });
throw error;
}
},
muteMember: async (serverId, userId, durationSeconds) => {
set({ isLoading: true, error: null });
try {
await api.post(`/servers/${serverId}/members/${userId}/mute`, { duration_seconds: durationSeconds });
set({ isLoading: false });
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to mute member';
set({ isLoading: false, error: message });
throw error;
}
},
clearError: () => set({ error: null }),
}));
+22
View File
@@ -12,6 +12,14 @@ export interface Server {
interface ServerState {
servers: Server[];
activeServerId: string | null;
userPermissions: {
kick_members: boolean;
ban_members: boolean;
mute_members: boolean;
manage_channels: boolean;
manage_server: boolean;
administrator: boolean;
};
isLoading: boolean;
error: string | null;
fetchServers: () => Promise<void>;
@@ -19,11 +27,20 @@ interface ServerState {
addServer: (server: Server) => void;
updateServer: (server: Server) => void;
removeServer: (id: string) => void;
setUserPermissions: (perms: Partial<ServerState['userPermissions']>) => void;
}
export const useServerStore = create<ServerState>((set) => ({
servers: [],
activeServerId: null,
userPermissions: {
kick_members: false,
ban_members: false,
mute_members: false,
manage_channels: false,
manage_server: false,
administrator: false,
},
isLoading: false,
error: null,
@@ -57,4 +74,9 @@ export const useServerStore = create<ServerState>((set) => ({
servers: state.servers.filter((s) => s.id !== id),
activeServerId: state.activeServerId === id ? null : state.activeServerId,
})),
setUserPermissions: (perms) =>
set((state) => ({
userPermissions: { ...state.userPermissions, ...perms },
})),
}));