chore: Initial commit of Opencord base state

This commit is contained in:
Jannis Braun
2026-02-18 02:49:21 +01:00
commit 4fd17084a5
124 changed files with 17955 additions and 0 deletions
+62
View File
@@ -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 }),
}));
+81
View File
@@ -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 }),
}));
+128
View File
@@ -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);
},
}));
+166
View File
@@ -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);
},
}));
+116
View File
@@ -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 });
},
}));
+158
View File
@@ -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 });
},
}));
+22
View File
@@ -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 }),
}));
+56
View File
@@ -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 }),
}));
+48
View File
@@ -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,
}),
}));
+75
View File
@@ -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,
}),
}));