feat: real-time sync, notification & social system overhaul
- Add WS events: dm_channel_created, dm_channel_closed, friend_removed, channel_created/updated/deleted, server_updated - Fix first-ever DM: broadcast dm_channel_created to recipient - Add DELETE /api/dm/:id for closing DMs with re-open support - Wire Close DM button in sidebar - Fix dm_message_created for unknown channels (safety net) - Broadcast friend_removed on friend deletion - Sound system: track realtimeMessageEvents separately from API loads, play notification for messages in all channels, not just current - Optimistic updates: send/edit/delete messages appear instantly with rollback on failure, temp message deduplication on WS echo - Channel CRUD broadcasts to all server members - Server update broadcast on PATCH - Server join registers user in connectionManager immediately - Reconnect: only reload current channel, preserve other channel caches - Activity panel, right panel, member list toggle components
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import type { User } from '@opencord/shared';
|
||||
|
||||
type ModalType =
|
||||
@@ -43,48 +44,65 @@ interface UIState {
|
||||
setPipCollapsed: (collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>((set, get) => ({
|
||||
sidebarOpen: true,
|
||||
memberListOpen: true,
|
||||
activeModal: null,
|
||||
modalData: {},
|
||||
isMobile: false,
|
||||
showDms: false,
|
||||
imagePreviewUrl: null,
|
||||
userProfilePopout: {
|
||||
user: null,
|
||||
position: null,
|
||||
},
|
||||
export const useUIStore = create<UIState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
sidebarOpen: true,
|
||||
memberListOpen: true,
|
||||
activeModal: null,
|
||||
modalData: {},
|
||||
isMobile: false,
|
||||
showDms: false,
|
||||
imagePreviewUrl: null,
|
||||
userProfilePopout: {
|
||||
user: null,
|
||||
position: null,
|
||||
},
|
||||
|
||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||
toggleMemberList: () => set((state) => ({ memberListOpen: !state.memberListOpen })),
|
||||
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: {} }),
|
||||
openModal: (modal, data = {}) => set({ activeModal: modal, modalData: data }),
|
||||
closeModal: () => set({ activeModal: null, modalData: {} }),
|
||||
|
||||
setIsMobile: (isMobile) => {
|
||||
const prev = get().isMobile;
|
||||
if (prev === isMobile) return;
|
||||
set({ isMobile, sidebarOpen: !isMobile, memberListOpen: !isMobile });
|
||||
},
|
||||
setIsMobile: (isMobile) => {
|
||||
const prev = get().isMobile;
|
||||
if (prev === isMobile) return;
|
||||
if (isMobile) {
|
||||
set({ isMobile, sidebarOpen: false, memberListOpen: false });
|
||||
} else {
|
||||
// On desktop transition, restore sidebarOpen but leave memberListOpen
|
||||
// at its persisted/toggled value — don't override user preference
|
||||
set({ isMobile, sidebarOpen: true });
|
||||
}
|
||||
},
|
||||
|
||||
setShowDms: (show) => set({ showDms: show }),
|
||||
setShowDms: (show) => set({ showDms: show }),
|
||||
|
||||
openImagePreview: (url) => set({ activeModal: 'imagePreview', imagePreviewUrl: url }),
|
||||
closeImagePreview: () => set({ activeModal: null, imagePreviewUrl: null }),
|
||||
openImagePreview: (url) => set({ activeModal: 'imagePreview', imagePreviewUrl: url }),
|
||||
closeImagePreview: () => set({ activeModal: null, imagePreviewUrl: null }),
|
||||
|
||||
openUserProfile: (user, position) => set({
|
||||
userProfilePopout: { user, position }
|
||||
}),
|
||||
closeUserProfile: () => set({
|
||||
userProfilePopout: { user: null, position: null }
|
||||
}),
|
||||
openUserProfile: (user, position) => set({
|
||||
userProfilePopout: { user, position }
|
||||
}),
|
||||
closeUserProfile: () => set({
|
||||
userProfilePopout: { user: null, position: null }
|
||||
}),
|
||||
|
||||
voiceChatOpen: false,
|
||||
voiceFullscreen: false,
|
||||
pipCollapsed: false,
|
||||
toggleVoiceChat: () => set((state) => ({ voiceChatOpen: !state.voiceChatOpen })),
|
||||
toggleVoiceFullscreen: () => set((state) => ({ voiceFullscreen: !state.voiceFullscreen })),
|
||||
setVoiceFullscreen: (fullscreen) => set({ voiceFullscreen: fullscreen }),
|
||||
setPipCollapsed: (collapsed) => set({ pipCollapsed: collapsed }),
|
||||
}));
|
||||
voiceChatOpen: false,
|
||||
voiceFullscreen: false,
|
||||
pipCollapsed: false,
|
||||
toggleVoiceChat: () => set((state) => ({ voiceChatOpen: !state.voiceChatOpen })),
|
||||
toggleVoiceFullscreen: () => set((state) => ({ voiceFullscreen: !state.voiceFullscreen })),
|
||||
setVoiceFullscreen: (fullscreen) => set({ voiceFullscreen: fullscreen }),
|
||||
setPipCollapsed: (collapsed) => set({ pipCollapsed: collapsed }),
|
||||
}),
|
||||
{
|
||||
name: 'opencord-ui-settings',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
memberListOpen: state.memberListOpen,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user