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:
Jannis Braun
2026-02-18 17:29:39 +01:00
parent 4f65ea9c86
commit 7168149d98
31 changed files with 1189 additions and 136 deletions
+33 -7
View File
@@ -16,7 +16,7 @@ function handleEvent(event: ServerEvent): void {
const { setUser } = useAuthStore.getState();
const { populateFromReady, loadServerDetail, currentServerId, updateMemberPresence, addMember, removeMember } = useServerStore.getState();
const { addMessage, updateMessage, removeMessage, setTyping, onReactionAdded, onReactionRemoved } = useChatStore.getState();
const { addVoiceUser, removeVoiceUser } = useVoiceStore.getState();
const { addVoiceUser, removeVoiceUser, clearAllVoiceUsers, setVoiceUsers } = useVoiceStore.getState();
switch (event.type) {
case 'ready':
@@ -25,11 +25,10 @@ function handleEvent(event: ServerEvent): void {
if (currentServerId) {
loadServerDetail(currentServerId);
}
// Populate voice channel state so users see who's in voice on load
if ((event as any).voiceStates) {
const vs = (event as any).voiceStates as Record<string, string[]>;
const { setVoiceUsers } = useVoiceStore.getState();
for (const [channelId, userIds] of Object.entries(vs)) {
// Clear stale voice state, then populate from server truth
clearAllVoiceUsers();
if (event.voiceStates) {
for (const [channelId, userIds] of Object.entries(event.voiceStates)) {
setVoiceUsers(channelId, userIds);
}
}
@@ -71,8 +70,35 @@ function handleEvent(event: ServerEvent): void {
removeMember(event.userId);
break;
case 'dm_message_created':
case 'dm_message_created': {
addMessage(event.message.dmChannelId, event.message as any);
// Update lastMessage on the DM channel so the sidebar sorts correctly
const { dmChannels, setDmChannels } = useServerStore.getState();
const updatedDms = dmChannels.map(dm =>
dm.id === event.message.dmChannelId
? { ...dm, lastMessage: event.message }
: dm
);
// Re-sort by most recent message
updatedDms.sort((a, b) => {
const aTime = a.lastMessage?.createdAt ?? a.createdAt;
const bTime = b.lastMessage?.createdAt ?? b.createdAt;
return bTime - aTime;
});
setDmChannels(updatedDms);
break;
}
case 'dm_message_updated':
updateMessage(event.message as any);
break;
case 'dm_message_deleted':
removeMessage(event.messageId, event.dmChannelId);
break;
case 'dm_typing':
setTyping(event.dmChannelId, event.userId, event.username);
break;
case 'reaction_added':