chore: Initial commit of Opencord base state
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { create } from 'zustand';
|
||||
import { api } from '../api/client';
|
||||
export const useAuthStore = create((set, get) => ({
|
||||
token: localStorage.getItem('opencord_token'),
|
||||
user: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
login: async (username, password) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const response = await api.auth.login({ username, password });
|
||||
localStorage.setItem('opencord_token', response.token);
|
||||
set({ token: response.token, user: response.user, isLoading: false });
|
||||
}
|
||||
catch (err) {
|
||||
set({ isLoading: false, error: err instanceof Error ? err.message : 'Login failed' });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
register: async (username, password, displayName) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const response = await api.auth.register({ username, password, displayName });
|
||||
localStorage.setItem('opencord_token', response.token);
|
||||
set({ token: response.token, user: response.user, isLoading: false });
|
||||
}
|
||||
catch (err) {
|
||||
set({ isLoading: false, error: err instanceof Error ? err.message : 'Registration failed' });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
logout: () => {
|
||||
localStorage.removeItem('opencord_token');
|
||||
set({ token: null, user: null });
|
||||
},
|
||||
loadUser: async () => {
|
||||
const token = get().token;
|
||||
if (!token)
|
||||
return;
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const user = await api.users.me();
|
||||
set({ user, isLoading: false });
|
||||
}
|
||||
catch {
|
||||
localStorage.removeItem('opencord_token');
|
||||
set({ token: null, user: null, isLoading: false });
|
||||
}
|
||||
},
|
||||
updateProfile: async (data) => {
|
||||
try {
|
||||
const user = await api.users.update(data);
|
||||
set({ user });
|
||||
}
|
||||
catch (err) {
|
||||
set({ error: err instanceof Error ? err.message : 'Update failed' });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
setUser: (user) => set({ user }),
|
||||
clearError: () => set({ error: null }),
|
||||
}));
|
||||
@@ -0,0 +1,81 @@
|
||||
import { create } from 'zustand';
|
||||
import type { User } from '@opencord/shared';
|
||||
import { api } from '../api/client';
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
user: User | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
register: (username: string, password: string, displayName?: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
loadUser: () => Promise<void>;
|
||||
updateProfile: (data: { displayName?: string; avatar?: string; customStatus?: string }) => Promise<void>;
|
||||
setUser: (user: User) => void;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
token: localStorage.getItem('opencord_token'),
|
||||
user: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
login: async (username: string, password: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const response = await api.auth.login({ username, password });
|
||||
localStorage.setItem('opencord_token', response.token);
|
||||
set({ token: response.token, user: response.user, isLoading: false });
|
||||
} catch (err) {
|
||||
set({ isLoading: false, error: err instanceof Error ? err.message : 'Login failed' });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
register: async (username: string, password: string, displayName?: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const response = await api.auth.register({ username, password, displayName });
|
||||
localStorage.setItem('opencord_token', response.token);
|
||||
set({ token: response.token, user: response.user, isLoading: false });
|
||||
} catch (err) {
|
||||
set({ isLoading: false, error: err instanceof Error ? err.message : 'Registration failed' });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem('opencord_token');
|
||||
set({ token: null, user: null });
|
||||
},
|
||||
|
||||
loadUser: async () => {
|
||||
const token = get().token;
|
||||
if (!token) return;
|
||||
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const user = await api.users.me();
|
||||
set({ user, isLoading: false });
|
||||
} catch {
|
||||
localStorage.removeItem('opencord_token');
|
||||
set({ token: null, user: null, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateProfile: async (data) => {
|
||||
try {
|
||||
const user = await api.users.update(data);
|
||||
set({ user });
|
||||
} catch (err) {
|
||||
set({ error: err instanceof Error ? err.message : 'Update failed' });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
setUser: (user: User) => set({ user }),
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
}));
|
||||
@@ -0,0 +1,128 @@
|
||||
import { create } from 'zustand';
|
||||
import { api } from '../api/client';
|
||||
export const useChatStore = create((set, get) => ({
|
||||
messages: new Map(),
|
||||
currentChannelId: null,
|
||||
typingUsers: new Map(),
|
||||
hasMore: new Map(),
|
||||
isLoading: false,
|
||||
setCurrentChannel: (channelId) => set({ currentChannelId: channelId }),
|
||||
loadMessages: async (channelId) => {
|
||||
if (get().messages.has(channelId))
|
||||
return;
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const messages = await api.channels.messages(channelId);
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
newMessages.set(channelId, messages);
|
||||
const newHasMore = new Map(state.hasMore);
|
||||
newHasMore.set(channelId, messages.length >= 50);
|
||||
return { messages: newMessages, hasMore: newHasMore, isLoading: false };
|
||||
});
|
||||
}
|
||||
catch {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
loadMoreMessages: async (channelId) => {
|
||||
const existing = get().messages.get(channelId);
|
||||
if (!existing || existing.length === 0)
|
||||
return false;
|
||||
if (!get().hasMore.get(channelId))
|
||||
return false;
|
||||
const oldestMessage = existing[0];
|
||||
if (!oldestMessage)
|
||||
return false;
|
||||
try {
|
||||
const olderMessages = await api.channels.messages(channelId, oldestMessage.id);
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(channelId) ?? [];
|
||||
newMessages.set(channelId, [...olderMessages, ...current]);
|
||||
const newHasMore = new Map(state.hasMore);
|
||||
newHasMore.set(channelId, olderMessages.length >= 50);
|
||||
return { messages: newMessages, hasMore: newHasMore };
|
||||
});
|
||||
return olderMessages.length > 0;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
sendMessage: async (channelId, content, attachmentIds) => {
|
||||
await api.channels.sendMessage(channelId, { content, attachments: attachmentIds });
|
||||
// Message will arrive via WebSocket
|
||||
},
|
||||
editMessage: async (messageId, content) => {
|
||||
await api.messages.update(messageId, { content });
|
||||
// Update will arrive via WebSocket
|
||||
},
|
||||
deleteMessage: async (messageId) => {
|
||||
await api.messages.delete(messageId);
|
||||
// Deletion will arrive via WebSocket
|
||||
},
|
||||
addMessage: (channelId, message) => {
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(channelId) ?? [];
|
||||
// Avoid duplicates
|
||||
if (current.find(m => m.id === message.id))
|
||||
return state;
|
||||
newMessages.set(channelId, [...current, message]);
|
||||
return { messages: newMessages };
|
||||
});
|
||||
},
|
||||
updateMessage: (message) => {
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(message.channelId);
|
||||
if (!current)
|
||||
return state;
|
||||
newMessages.set(message.channelId, current.map(m => m.id === message.id ? message : m));
|
||||
return { messages: newMessages };
|
||||
});
|
||||
},
|
||||
removeMessage: (messageId, channelId) => {
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(channelId);
|
||||
if (!current)
|
||||
return state;
|
||||
newMessages.set(channelId, current.filter(m => m.id !== messageId));
|
||||
return { messages: newMessages };
|
||||
});
|
||||
},
|
||||
setTyping: (channelId, userId, username) => {
|
||||
set((state) => {
|
||||
const newTyping = new Map(state.typingUsers);
|
||||
const current = newTyping.get(channelId) ?? [];
|
||||
const filtered = current.filter(t => t.userId !== userId);
|
||||
filtered.push({ userId, username, timestamp: Date.now() });
|
||||
newTyping.set(channelId, filtered);
|
||||
return { typingUsers: newTyping };
|
||||
});
|
||||
// Auto-clear after 5 seconds
|
||||
setTimeout(() => {
|
||||
get().clearTyping(channelId, userId);
|
||||
}, 5000);
|
||||
},
|
||||
clearTyping: (channelId, userId) => {
|
||||
set((state) => {
|
||||
const newTyping = new Map(state.typingUsers);
|
||||
const current = newTyping.get(channelId);
|
||||
if (!current)
|
||||
return state;
|
||||
newTyping.set(channelId, current.filter(t => t.userId !== userId));
|
||||
return { typingUsers: newTyping };
|
||||
});
|
||||
},
|
||||
getMessages: (channelId) => {
|
||||
return get().messages.get(channelId) ?? [];
|
||||
},
|
||||
getTypingUsers: (channelId) => {
|
||||
const users = get().typingUsers.get(channelId) ?? [];
|
||||
const now = Date.now();
|
||||
return users.filter(t => now - t.timestamp < 5000);
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,166 @@
|
||||
import { create } from 'zustand';
|
||||
import type { MessageWithUser } from '@opencord/shared';
|
||||
import { api } from '../api/client';
|
||||
|
||||
interface TypingUser {
|
||||
userId: string;
|
||||
username: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ChatState {
|
||||
messages: Map<string, MessageWithUser[]>;
|
||||
currentChannelId: string | null;
|
||||
typingUsers: Map<string, TypingUser[]>;
|
||||
hasMore: Map<string, boolean>;
|
||||
isLoading: boolean;
|
||||
setCurrentChannel: (channelId: string | null) => void;
|
||||
loadMessages: (channelId: string) => Promise<void>;
|
||||
loadMoreMessages: (channelId: string) => Promise<boolean>;
|
||||
sendMessage: (channelId: string, content: string, attachmentIds?: string[]) => Promise<void>;
|
||||
editMessage: (messageId: string, content: string) => Promise<void>;
|
||||
deleteMessage: (messageId: string) => Promise<void>;
|
||||
addMessage: (channelId: string, message: MessageWithUser) => void;
|
||||
updateMessage: (message: MessageWithUser) => void;
|
||||
removeMessage: (messageId: string, channelId: string) => void;
|
||||
setTyping: (channelId: string, userId: string, username: string) => void;
|
||||
clearTyping: (channelId: string, userId: string) => void;
|
||||
getMessages: (channelId: string) => MessageWithUser[];
|
||||
getTypingUsers: (channelId: string) => TypingUser[];
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatState>((set, get) => ({
|
||||
messages: new Map(),
|
||||
currentChannelId: null,
|
||||
typingUsers: new Map(),
|
||||
hasMore: new Map(),
|
||||
isLoading: false,
|
||||
|
||||
setCurrentChannel: (channelId) => set({ currentChannelId: channelId }),
|
||||
|
||||
loadMessages: async (channelId: string) => {
|
||||
if (get().messages.has(channelId)) return;
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const messages = await api.channels.messages(channelId);
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
newMessages.set(channelId, messages);
|
||||
const newHasMore = new Map(state.hasMore);
|
||||
newHasMore.set(channelId, messages.length >= 50);
|
||||
return { messages: newMessages, hasMore: newHasMore, isLoading: false };
|
||||
});
|
||||
} catch {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadMoreMessages: async (channelId: string) => {
|
||||
const existing = get().messages.get(channelId);
|
||||
if (!existing || existing.length === 0) return false;
|
||||
if (!get().hasMore.get(channelId)) return false;
|
||||
|
||||
const oldestMessage = existing[0];
|
||||
if (!oldestMessage) return false;
|
||||
|
||||
try {
|
||||
const olderMessages = await api.channels.messages(channelId, oldestMessage.id);
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(channelId) ?? [];
|
||||
newMessages.set(channelId, [...olderMessages, ...current]);
|
||||
const newHasMore = new Map(state.hasMore);
|
||||
newHasMore.set(channelId, olderMessages.length >= 50);
|
||||
return { messages: newMessages, hasMore: newHasMore };
|
||||
});
|
||||
return olderMessages.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: async (channelId: string, content: string, attachmentIds?: string[]) => {
|
||||
await api.channels.sendMessage(channelId, { content, attachments: attachmentIds });
|
||||
// Message will arrive via WebSocket
|
||||
},
|
||||
|
||||
editMessage: async (messageId: string, content: string) => {
|
||||
await api.messages.update(messageId, { content });
|
||||
// Update will arrive via WebSocket
|
||||
},
|
||||
|
||||
deleteMessage: async (messageId: string) => {
|
||||
await api.messages.delete(messageId);
|
||||
// Deletion will arrive via WebSocket
|
||||
},
|
||||
|
||||
addMessage: (channelId: string, message: MessageWithUser) => {
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(channelId) ?? [];
|
||||
// Avoid duplicates
|
||||
if (current.find(m => m.id === message.id)) return state;
|
||||
newMessages.set(channelId, [...current, message]);
|
||||
return { messages: newMessages };
|
||||
});
|
||||
},
|
||||
|
||||
updateMessage: (message: MessageWithUser) => {
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(message.channelId);
|
||||
if (!current) return state;
|
||||
newMessages.set(
|
||||
message.channelId,
|
||||
current.map(m => m.id === message.id ? message : m),
|
||||
);
|
||||
return { messages: newMessages };
|
||||
});
|
||||
},
|
||||
|
||||
removeMessage: (messageId: string, channelId: string) => {
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(channelId);
|
||||
if (!current) return state;
|
||||
newMessages.set(channelId, current.filter(m => m.id !== messageId));
|
||||
return { messages: newMessages };
|
||||
});
|
||||
},
|
||||
|
||||
setTyping: (channelId: string, userId: string, username: string) => {
|
||||
set((state) => {
|
||||
const newTyping = new Map(state.typingUsers);
|
||||
const current = newTyping.get(channelId) ?? [];
|
||||
const filtered = current.filter(t => t.userId !== userId);
|
||||
filtered.push({ userId, username, timestamp: Date.now() });
|
||||
newTyping.set(channelId, filtered);
|
||||
return { typingUsers: newTyping };
|
||||
});
|
||||
|
||||
// Auto-clear after 5 seconds
|
||||
setTimeout(() => {
|
||||
get().clearTyping(channelId, userId);
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
clearTyping: (channelId: string, userId: string) => {
|
||||
set((state) => {
|
||||
const newTyping = new Map(state.typingUsers);
|
||||
const current = newTyping.get(channelId);
|
||||
if (!current) return state;
|
||||
newTyping.set(channelId, current.filter(t => t.userId !== userId));
|
||||
return { typingUsers: newTyping };
|
||||
});
|
||||
},
|
||||
|
||||
getMessages: (channelId: string) => {
|
||||
return get().messages.get(channelId) ?? [];
|
||||
},
|
||||
|
||||
getTypingUsers: (channelId: string) => {
|
||||
const users = get().typingUsers.get(channelId) ?? [];
|
||||
const now = Date.now();
|
||||
return users.filter(t => now - t.timestamp < 5000);
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,116 @@
|
||||
import { create } from 'zustand';
|
||||
import { api } from '../api/client';
|
||||
export const useServerStore = create((set, get) => ({
|
||||
servers: [],
|
||||
currentServerId: null,
|
||||
channels: [],
|
||||
members: [],
|
||||
setServers: (servers) => set({ servers }),
|
||||
setCurrentServer: (serverId) => set({ currentServerId: serverId }),
|
||||
setChannels: (channels) => set({ channels }),
|
||||
setMembers: (members) => set({ members }),
|
||||
loadServers: async () => {
|
||||
try {
|
||||
const servers = await api.servers.list();
|
||||
set({ servers });
|
||||
}
|
||||
catch {
|
||||
// Silently fail - will be populated from WS ready
|
||||
}
|
||||
},
|
||||
loadServerDetail: async (serverId) => {
|
||||
try {
|
||||
const detail = await api.servers.get(serverId);
|
||||
set({
|
||||
currentServerId: serverId,
|
||||
channels: detail.channels.sort((a, b) => a.position - b.position),
|
||||
members: detail.members,
|
||||
});
|
||||
}
|
||||
catch {
|
||||
// Handle error silently
|
||||
}
|
||||
},
|
||||
createServer: async (name, icon) => {
|
||||
const server = await api.servers.create({ name, icon });
|
||||
set((state) => ({ servers: [...state.servers, server] }));
|
||||
return server;
|
||||
},
|
||||
updateServer: async (serverId, data) => {
|
||||
const updated = await api.servers.update(serverId, data);
|
||||
set((state) => ({
|
||||
servers: state.servers.map(s => s.id === serverId ? { ...s, ...updated } : s),
|
||||
}));
|
||||
},
|
||||
deleteServer: async (serverId) => {
|
||||
await api.servers.delete(serverId);
|
||||
set((state) => ({
|
||||
servers: state.servers.filter(s => s.id !== serverId),
|
||||
currentServerId: state.currentServerId === serverId ? null : state.currentServerId,
|
||||
}));
|
||||
},
|
||||
joinServer: async (serverId, inviteCode) => {
|
||||
const server = await api.servers.join(serverId, { inviteCode });
|
||||
set((state) => {
|
||||
if (state.servers.find(s => s.id === server.id))
|
||||
return state;
|
||||
return { servers: [...state.servers, server] };
|
||||
});
|
||||
},
|
||||
generateInvite: async (serverId) => {
|
||||
const result = await api.servers.invite(serverId);
|
||||
return result.inviteCode;
|
||||
},
|
||||
createChannel: async (serverId, name, type, topic) => {
|
||||
const channel = await api.channels.create(serverId, { name, type, topic });
|
||||
set((state) => ({
|
||||
channels: [...state.channels, channel].sort((a, b) => a.position - b.position),
|
||||
}));
|
||||
return channel;
|
||||
},
|
||||
deleteChannel: async (channelId) => {
|
||||
await api.channels.delete(channelId);
|
||||
set((state) => ({
|
||||
channels: state.channels.filter(c => c.id !== channelId),
|
||||
}));
|
||||
},
|
||||
addServer: (server) => {
|
||||
set((state) => {
|
||||
if (state.servers.find(s => s.id === server.id))
|
||||
return state;
|
||||
return { servers: [...state.servers, server] };
|
||||
});
|
||||
},
|
||||
removeServer: (serverId) => {
|
||||
set((state) => ({
|
||||
servers: state.servers.filter(s => s.id !== serverId),
|
||||
currentServerId: state.currentServerId === serverId ? null : state.currentServerId,
|
||||
}));
|
||||
},
|
||||
updateMemberPresence: (userId, status) => {
|
||||
set((state) => ({
|
||||
members: state.members.map(m => m.userId === userId ? { ...m, user: { ...m.user, status: status } } : m),
|
||||
}));
|
||||
},
|
||||
addMember: (member) => {
|
||||
set((state) => ({
|
||||
members: [...state.members.filter(m => m.userId !== member.userId), member],
|
||||
}));
|
||||
},
|
||||
removeMember: (userId) => {
|
||||
set((state) => ({
|
||||
members: state.members.filter(m => m.userId !== userId),
|
||||
}));
|
||||
},
|
||||
populateFromReady: (servers) => {
|
||||
const simpleServers = servers.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
icon: s.icon,
|
||||
ownerId: s.ownerId,
|
||||
inviteCode: s.inviteCode,
|
||||
createdAt: s.createdAt,
|
||||
}));
|
||||
set({ servers: simpleServers });
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,158 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Server, Channel, MemberWithUser, ServerWithChannelsAndMembers } from '@opencord/shared';
|
||||
import { api } from '../api/client';
|
||||
|
||||
interface ServerState {
|
||||
servers: Server[];
|
||||
currentServerId: string | null;
|
||||
channels: Channel[];
|
||||
members: MemberWithUser[];
|
||||
setServers: (servers: Server[]) => void;
|
||||
setCurrentServer: (serverId: string | null) => void;
|
||||
setChannels: (channels: Channel[]) => void;
|
||||
setMembers: (members: MemberWithUser[]) => void;
|
||||
loadServers: () => Promise<void>;
|
||||
loadServerDetail: (serverId: string) => Promise<void>;
|
||||
createServer: (name: string, icon?: string) => Promise<Server>;
|
||||
updateServer: (serverId: string, data: { name?: string; icon?: string }) => Promise<void>;
|
||||
deleteServer: (serverId: string) => Promise<void>;
|
||||
joinServer: (serverId: string, inviteCode: string) => Promise<void>;
|
||||
generateInvite: (serverId: string) => Promise<string>;
|
||||
createChannel: (serverId: string, name: string, type: 'text' | 'voice' | 'video', topic?: string) => Promise<Channel>;
|
||||
deleteChannel: (channelId: string) => Promise<void>;
|
||||
addServer: (server: Server) => void;
|
||||
removeServer: (serverId: string) => void;
|
||||
updateMemberPresence: (userId: string, status: string) => void;
|
||||
addMember: (member: MemberWithUser) => void;
|
||||
removeMember: (userId: string) => void;
|
||||
populateFromReady: (servers: ServerWithChannelsAndMembers[]) => void;
|
||||
}
|
||||
|
||||
export const useServerStore = create<ServerState>((set, get) => ({
|
||||
servers: [],
|
||||
currentServerId: null,
|
||||
channels: [],
|
||||
members: [],
|
||||
|
||||
setServers: (servers) => set({ servers }),
|
||||
setCurrentServer: (serverId) => set({ currentServerId: serverId }),
|
||||
setChannels: (channels) => set({ channels }),
|
||||
setMembers: (members) => set({ members }),
|
||||
|
||||
loadServers: async () => {
|
||||
try {
|
||||
const servers = await api.servers.list();
|
||||
set({ servers });
|
||||
} catch {
|
||||
// Silently fail - will be populated from WS ready
|
||||
}
|
||||
},
|
||||
|
||||
loadServerDetail: async (serverId: string) => {
|
||||
try {
|
||||
const detail = await api.servers.get(serverId);
|
||||
set({
|
||||
currentServerId: serverId,
|
||||
channels: detail.channels.sort((a, b) => a.position - b.position),
|
||||
members: detail.members,
|
||||
});
|
||||
} catch {
|
||||
// Handle error silently
|
||||
}
|
||||
},
|
||||
|
||||
createServer: async (name: string, icon?: string) => {
|
||||
const server = await api.servers.create({ name, icon });
|
||||
set((state) => ({ servers: [...state.servers, server] }));
|
||||
return server;
|
||||
},
|
||||
|
||||
updateServer: async (serverId: string, data: { name?: string; icon?: string }) => {
|
||||
const updated = await api.servers.update(serverId, data);
|
||||
set((state) => ({
|
||||
servers: state.servers.map(s => s.id === serverId ? { ...s, ...updated } : s),
|
||||
}));
|
||||
},
|
||||
|
||||
deleteServer: async (serverId: string) => {
|
||||
await api.servers.delete(serverId);
|
||||
set((state) => ({
|
||||
servers: state.servers.filter(s => s.id !== serverId),
|
||||
currentServerId: state.currentServerId === serverId ? null : state.currentServerId,
|
||||
}));
|
||||
},
|
||||
|
||||
joinServer: async (serverId: string, inviteCode: string) => {
|
||||
const server = await api.servers.join(serverId, { inviteCode });
|
||||
set((state) => {
|
||||
if (state.servers.find(s => s.id === server.id)) return state;
|
||||
return { servers: [...state.servers, server] };
|
||||
});
|
||||
},
|
||||
|
||||
generateInvite: async (serverId: string) => {
|
||||
const result = await api.servers.invite(serverId);
|
||||
return result.inviteCode;
|
||||
},
|
||||
|
||||
createChannel: async (serverId: string, name: string, type: 'text' | 'voice' | 'video', topic?: string) => {
|
||||
const channel = await api.channels.create(serverId, { name, type, topic });
|
||||
set((state) => ({
|
||||
channels: [...state.channels, channel].sort((a, b) => a.position - b.position),
|
||||
}));
|
||||
return channel;
|
||||
},
|
||||
|
||||
deleteChannel: async (channelId: string) => {
|
||||
await api.channels.delete(channelId);
|
||||
set((state) => ({
|
||||
channels: state.channels.filter(c => c.id !== channelId),
|
||||
}));
|
||||
},
|
||||
|
||||
addServer: (server: Server) => {
|
||||
set((state) => {
|
||||
if (state.servers.find(s => s.id === server.id)) return state;
|
||||
return { servers: [...state.servers, server] };
|
||||
});
|
||||
},
|
||||
|
||||
removeServer: (serverId: string) => {
|
||||
set((state) => ({
|
||||
servers: state.servers.filter(s => s.id !== serverId),
|
||||
currentServerId: state.currentServerId === serverId ? null : state.currentServerId,
|
||||
}));
|
||||
},
|
||||
|
||||
updateMemberPresence: (userId: string, status: string) => {
|
||||
set((state) => ({
|
||||
members: state.members.map(m =>
|
||||
m.userId === userId ? { ...m, user: { ...m.user, status: status as 'online' | 'idle' | 'dnd' | 'offline' } } : m
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
addMember: (member: MemberWithUser) => {
|
||||
set((state) => ({
|
||||
members: [...state.members.filter(m => m.userId !== member.userId), member],
|
||||
}));
|
||||
},
|
||||
|
||||
removeMember: (userId: string) => {
|
||||
set((state) => ({
|
||||
members: state.members.filter(m => m.userId !== userId),
|
||||
}));
|
||||
},
|
||||
|
||||
populateFromReady: (servers: ServerWithChannelsAndMembers[]) => {
|
||||
const simpleServers: Server[] = servers.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
icon: s.icon,
|
||||
ownerId: s.ownerId,
|
||||
inviteCode: s.inviteCode,
|
||||
createdAt: s.createdAt,
|
||||
}));
|
||||
set({ servers: simpleServers });
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,22 @@
|
||||
import { create } from 'zustand';
|
||||
export const useUIStore = create((set) => ({
|
||||
sidebarOpen: true,
|
||||
memberListOpen: true,
|
||||
activeModal: null,
|
||||
modalData: {},
|
||||
isMobile: false,
|
||||
showDms: false,
|
||||
imagePreviewUrl: null,
|
||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||
toggleMemberList: () => set((state) => ({ memberListOpen: !state.memberListOpen })),
|
||||
openModal: (modal, data = {}) => set({ activeModal: modal, modalData: data }),
|
||||
closeModal: () => set({ activeModal: null, modalData: {} }),
|
||||
setIsMobile: (isMobile) => set({
|
||||
isMobile,
|
||||
sidebarOpen: !isMobile,
|
||||
memberListOpen: !isMobile,
|
||||
}),
|
||||
setShowDms: (show) => set({ showDms: show }),
|
||||
openImagePreview: (url) => set({ activeModal: 'imagePreview', imagePreviewUrl: url }),
|
||||
closeImagePreview: () => set({ activeModal: null, imagePreviewUrl: null }),
|
||||
}));
|
||||
@@ -0,0 +1,56 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type ModalType =
|
||||
| 'createServer'
|
||||
| 'joinServer'
|
||||
| 'createChannel'
|
||||
| 'invite'
|
||||
| 'userSettings'
|
||||
| 'serverSettings'
|
||||
| 'imagePreview'
|
||||
| null;
|
||||
|
||||
interface UIState {
|
||||
sidebarOpen: boolean;
|
||||
memberListOpen: boolean;
|
||||
activeModal: ModalType;
|
||||
modalData: Record<string, unknown>;
|
||||
isMobile: boolean;
|
||||
showDms: boolean;
|
||||
imagePreviewUrl: string | null;
|
||||
toggleSidebar: () => void;
|
||||
toggleMemberList: () => void;
|
||||
openModal: (modal: ModalType, data?: Record<string, unknown>) => void;
|
||||
closeModal: () => void;
|
||||
setIsMobile: (isMobile: boolean) => void;
|
||||
setShowDms: (show: boolean) => void;
|
||||
openImagePreview: (url: string) => void;
|
||||
closeImagePreview: () => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>((set) => ({
|
||||
sidebarOpen: true,
|
||||
memberListOpen: true,
|
||||
activeModal: null,
|
||||
modalData: {},
|
||||
isMobile: false,
|
||||
showDms: false,
|
||||
imagePreviewUrl: null,
|
||||
|
||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||
toggleMemberList: () => set((state) => ({ memberListOpen: !state.memberListOpen })),
|
||||
|
||||
openModal: (modal, data = {}) => set({ activeModal: modal, modalData: data }),
|
||||
closeModal: () => set({ activeModal: null, modalData: {} }),
|
||||
|
||||
setIsMobile: (isMobile) => set({
|
||||
isMobile,
|
||||
sidebarOpen: !isMobile,
|
||||
memberListOpen: !isMobile,
|
||||
}),
|
||||
|
||||
setShowDms: (show) => set({ showDms: show }),
|
||||
|
||||
openImagePreview: (url) => set({ activeModal: 'imagePreview', imagePreviewUrl: url }),
|
||||
closeImagePreview: () => set({ activeModal: null, imagePreviewUrl: null }),
|
||||
}));
|
||||
@@ -0,0 +1,48 @@
|
||||
import { create } from 'zustand';
|
||||
export const useVoiceStore = create((set, get) => ({
|
||||
voiceUsers: new Map(),
|
||||
currentVoiceChannelId: null,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
isCameraOn: false,
|
||||
isScreenSharing: false,
|
||||
setVoiceUsers: (channelId, userIds) => {
|
||||
set((state) => {
|
||||
const newMap = new Map(state.voiceUsers);
|
||||
newMap.set(channelId, userIds);
|
||||
return { voiceUsers: newMap };
|
||||
});
|
||||
},
|
||||
addVoiceUser: (channelId, userId) => {
|
||||
set((state) => {
|
||||
const newMap = new Map(state.voiceUsers);
|
||||
const current = newMap.get(channelId) ?? [];
|
||||
if (!current.includes(userId)) {
|
||||
newMap.set(channelId, [...current, userId]);
|
||||
}
|
||||
return { voiceUsers: newMap };
|
||||
});
|
||||
},
|
||||
removeVoiceUser: (channelId, userId) => {
|
||||
set((state) => {
|
||||
const newMap = new Map(state.voiceUsers);
|
||||
const current = newMap.get(channelId) ?? [];
|
||||
newMap.set(channelId, current.filter(id => id !== userId));
|
||||
return { voiceUsers: newMap };
|
||||
});
|
||||
},
|
||||
setCurrentVoiceChannel: (channelId) => set({ currentVoiceChannelId: channelId }),
|
||||
toggleMute: () => set((state) => ({ isMuted: !state.isMuted })),
|
||||
toggleDeafen: () => set((state) => ({ isDeafened: !state.isDeafened })),
|
||||
toggleCamera: () => set((state) => ({ isCameraOn: !state.isCameraOn })),
|
||||
toggleScreenShare: () => set((state) => ({ isScreenSharing: !state.isScreenSharing })),
|
||||
getVoiceUsers: (channelId) => get().voiceUsers.get(channelId) ?? [],
|
||||
reset: () => set({
|
||||
voiceUsers: new Map(),
|
||||
currentVoiceChannelId: null,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
isCameraOn: false,
|
||||
isScreenSharing: false,
|
||||
}),
|
||||
}));
|
||||
@@ -0,0 +1,75 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface VoiceState {
|
||||
voiceUsers: Map<string, string[]>; // channelId → userIds
|
||||
currentVoiceChannelId: string | null;
|
||||
isMuted: boolean;
|
||||
isDeafened: boolean;
|
||||
isCameraOn: boolean;
|
||||
isScreenSharing: boolean;
|
||||
setVoiceUsers: (channelId: string, userIds: string[]) => void;
|
||||
addVoiceUser: (channelId: string, userId: string) => void;
|
||||
removeVoiceUser: (channelId: string, userId: string) => void;
|
||||
setCurrentVoiceChannel: (channelId: string | null) => void;
|
||||
toggleMute: () => void;
|
||||
toggleDeafen: () => void;
|
||||
toggleCamera: () => void;
|
||||
toggleScreenShare: () => void;
|
||||
getVoiceUsers: (channelId: string) => string[];
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const useVoiceStore = create<VoiceState>((set, get) => ({
|
||||
voiceUsers: new Map(),
|
||||
currentVoiceChannelId: null,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
isCameraOn: false,
|
||||
isScreenSharing: false,
|
||||
|
||||
setVoiceUsers: (channelId, userIds) => {
|
||||
set((state) => {
|
||||
const newMap = new Map(state.voiceUsers);
|
||||
newMap.set(channelId, userIds);
|
||||
return { voiceUsers: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
addVoiceUser: (channelId, userId) => {
|
||||
set((state) => {
|
||||
const newMap = new Map(state.voiceUsers);
|
||||
const current = newMap.get(channelId) ?? [];
|
||||
if (!current.includes(userId)) {
|
||||
newMap.set(channelId, [...current, userId]);
|
||||
}
|
||||
return { voiceUsers: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
removeVoiceUser: (channelId, userId) => {
|
||||
set((state) => {
|
||||
const newMap = new Map(state.voiceUsers);
|
||||
const current = newMap.get(channelId) ?? [];
|
||||
newMap.set(channelId, current.filter(id => id !== userId));
|
||||
return { voiceUsers: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
setCurrentVoiceChannel: (channelId) => set({ currentVoiceChannelId: channelId }),
|
||||
|
||||
toggleMute: () => set((state) => ({ isMuted: !state.isMuted })),
|
||||
toggleDeafen: () => set((state) => ({ isDeafened: !state.isDeafened })),
|
||||
toggleCamera: () => set((state) => ({ isCameraOn: !state.isCameraOn })),
|
||||
toggleScreenShare: () => set((state) => ({ isScreenSharing: !state.isScreenSharing })),
|
||||
|
||||
getVoiceUsers: (channelId) => get().voiceUsers.get(channelId) ?? [],
|
||||
|
||||
reset: () => set({
|
||||
voiceUsers: new Map(),
|
||||
currentVoiceChannelId: null,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
isCameraOn: false,
|
||||
isScreenSharing: false,
|
||||
}),
|
||||
}));
|
||||
Reference in New Issue
Block a user