feat: DM infrastructure overhaul + volume sliders + LiveKit dynamic URL
- Add DM edit/delete endpoints (REST + WebSocket) - Add DM typing indicators with real-time broadcast - Fix volume sliders to control LiveKit mic/speaker - Fix Send Message button on user profile popout - Add New DM modal with user search - Fix message pagination (SQL cursor instead of in-memory) - Guard optional attachments on DM messages - Dynamic LiveKit URL from request Host header - DM sidebar auto-sorts by most recent message
This commit is contained in:
@@ -74,11 +74,21 @@ export const useChatStore = create((set, get) => ({
|
||||
// Message will arrive via WebSocket
|
||||
},
|
||||
editMessage: async (messageId, content) => {
|
||||
await api.messages.update(messageId, { content });
|
||||
const isDm = useUIStore.getState().showDms;
|
||||
if (isDm) {
|
||||
await api.dm.updateMessage(messageId, { content });
|
||||
} else {
|
||||
await api.messages.update(messageId, { content });
|
||||
}
|
||||
// Update will arrive via WebSocket
|
||||
},
|
||||
deleteMessage: async (messageId) => {
|
||||
await api.messages.delete(messageId);
|
||||
const isDm = useUIStore.getState().showDms;
|
||||
if (isDm) {
|
||||
await api.dm.deleteMessage(messageId);
|
||||
} else {
|
||||
await api.messages.delete(messageId);
|
||||
}
|
||||
// Deletion will arrive via WebSocket
|
||||
},
|
||||
addMessage: (channelId, message) => {
|
||||
@@ -93,12 +103,14 @@ export const useChatStore = create((set, get) => ({
|
||||
});
|
||||
},
|
||||
updateMessage: (message) => {
|
||||
const channelKey = message.channelId || message.dmChannelId;
|
||||
if (!channelKey) return;
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(message.channelId);
|
||||
const current = newMessages.get(channelKey);
|
||||
if (!current)
|
||||
return state;
|
||||
newMessages.set(message.channelId, current.map(m => m.id === message.id ? message : m));
|
||||
newMessages.set(channelKey, current.map(m => m.id === message.id ? message : m));
|
||||
return { messages: newMessages };
|
||||
});
|
||||
},
|
||||
|
||||
@@ -114,12 +114,22 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
},
|
||||
|
||||
editMessage: async (messageId: string, content: string) => {
|
||||
await api.messages.update(messageId, { content });
|
||||
const isDm = useUIStore.getState().showDms;
|
||||
if (isDm) {
|
||||
await api.dm.updateMessage(messageId, { content });
|
||||
} else {
|
||||
await api.messages.update(messageId, { content });
|
||||
}
|
||||
// Update will arrive via WebSocket
|
||||
},
|
||||
|
||||
deleteMessage: async (messageId: string) => {
|
||||
await api.messages.delete(messageId);
|
||||
const isDm = useUIStore.getState().showDms;
|
||||
if (isDm) {
|
||||
await api.dm.deleteMessage(messageId);
|
||||
} else {
|
||||
await api.messages.delete(messageId);
|
||||
}
|
||||
// Deletion will arrive via WebSocket
|
||||
},
|
||||
|
||||
@@ -135,12 +145,15 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
},
|
||||
|
||||
updateMessage: (message: MessageWithUser) => {
|
||||
// DM messages have dmChannelId instead of channelId — check both
|
||||
const channelKey = message.channelId || (message as any).dmChannelId;
|
||||
if (!channelKey) return;
|
||||
set((state) => {
|
||||
const newMessages = new Map(state.messages);
|
||||
const current = newMessages.get(message.channelId);
|
||||
const current = newMessages.get(channelKey);
|
||||
if (!current) return state;
|
||||
newMessages.set(
|
||||
message.channelId,
|
||||
channelKey,
|
||||
current.map(m => m.id === message.id ? message : m),
|
||||
);
|
||||
return { messages: newMessages };
|
||||
|
||||
@@ -9,6 +9,7 @@ type ModalType =
|
||||
| 'userSettings'
|
||||
| 'serverSettings'
|
||||
| 'imagePreview'
|
||||
| 'newDm'
|
||||
| null;
|
||||
|
||||
interface UIState {
|
||||
|
||||
@@ -43,6 +43,18 @@ export const useVoiceStore = create((set, get) => ({
|
||||
toggleCamera: () => set((state) => ({ isCameraOn: !state.isCameraOn })),
|
||||
toggleScreenShare: () => set((state) => ({ isScreenSharing: !state.isScreenSharing })),
|
||||
getVoiceUsers: (channelId) => get().voiceUsers.get(channelId) ?? [],
|
||||
clearAllVoiceUsers: () => set({ voiceUsers: new Map() }),
|
||||
// Leave voice without wiping the voiceUsers map (so sidebar still shows others)
|
||||
leaveVoice: () => set({
|
||||
currentVoiceChannelId: null,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
isCameraOn: false,
|
||||
isScreenSharing: false,
|
||||
participants: [],
|
||||
connectionError: null,
|
||||
isLiveKitConnected: false,
|
||||
}),
|
||||
reset: () => set({
|
||||
voiceUsers: new Map(),
|
||||
currentVoiceChannelId: null,
|
||||
|
||||
@@ -11,6 +11,8 @@ interface VoiceState {
|
||||
participants: ParticipantInfo[];
|
||||
connectionError: string | null;
|
||||
isLiveKitConnected: boolean;
|
||||
inputVolume: number; // 0-200 (100 = default)
|
||||
outputVolume: number; // 0-200 (100 = default)
|
||||
setVoiceUsers: (channelId: string, userIds: string[]) => void;
|
||||
addVoiceUser: (channelId: string, userId: string) => void;
|
||||
removeVoiceUser: (channelId: string, userId: string) => void;
|
||||
@@ -18,11 +20,15 @@ interface VoiceState {
|
||||
setParticipants: (participants: ParticipantInfo[]) => void;
|
||||
setConnectionError: (error: string | null) => void;
|
||||
setIsLiveKitConnected: (connected: boolean) => void;
|
||||
setInputVolume: (volume: number) => void;
|
||||
setOutputVolume: (volume: number) => void;
|
||||
toggleMic: () => void;
|
||||
toggleCamera: () => void;
|
||||
toggleScreenShare: () => void;
|
||||
toggleDeafen: () => void;
|
||||
getVoiceUsers: (channelId: string) => string[];
|
||||
clearAllVoiceUsers: () => void;
|
||||
leaveVoice: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -36,6 +42,8 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
|
||||
participants: [],
|
||||
connectionError: null,
|
||||
isLiveKitConnected: false,
|
||||
inputVolume: 100,
|
||||
outputVolume: 100,
|
||||
|
||||
setVoiceUsers: (channelId, userIds) => {
|
||||
set((state) => {
|
||||
@@ -71,6 +79,9 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
|
||||
setConnectionError: (error) => set({ connectionError: error }),
|
||||
setIsLiveKitConnected: (connected) => set({ isLiveKitConnected: connected }),
|
||||
|
||||
setInputVolume: (volume) => set({ inputVolume: volume }),
|
||||
setOutputVolume: (volume) => set({ outputVolume: volume }),
|
||||
|
||||
toggleMic: () => set((state) => ({ isMuted: !state.isMuted })),
|
||||
toggleDeafen: () => set((state) => ({ isDeafened: !state.isDeafened })),
|
||||
toggleCamera: () => set((state) => ({ isCameraOn: !state.isCameraOn })),
|
||||
@@ -78,6 +89,22 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
|
||||
|
||||
getVoiceUsers: (channelId) => get().voiceUsers.get(channelId) ?? [],
|
||||
|
||||
clearAllVoiceUsers: () => set({ voiceUsers: new Map() }),
|
||||
|
||||
// Leave voice without wiping the voiceUsers map (so sidebar still shows others)
|
||||
leaveVoice: () => set({
|
||||
currentVoiceChannelId: null,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
isCameraOn: false,
|
||||
isScreenSharing: false,
|
||||
participants: [],
|
||||
connectionError: null,
|
||||
isLiveKitConnected: false,
|
||||
inputVolume: 100,
|
||||
outputVolume: 100,
|
||||
}),
|
||||
|
||||
reset: () => set({
|
||||
voiceUsers: new Map(),
|
||||
currentVoiceChannelId: null,
|
||||
@@ -88,5 +115,7 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
|
||||
participants: [],
|
||||
connectionError: null,
|
||||
isLiveKitConnected: false,
|
||||
inputVolume: 100,
|
||||
outputVolume: 100,
|
||||
}),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user