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
@@ -1,5 +1,6 @@
import React, { useState, useRef, useCallback } from 'react';
import { useChatStore } from '../../stores/chatStore';
import { useUIStore } from '../../stores/uiStore';
import { wsSend } from '../../hooks/useWebSocket';
import { api } from '../../api/client';
@@ -21,7 +22,12 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
const handleTyping = useCallback(() => {
if (typingTimeoutRef.current) return;
wsSend({ type: 'typing_start', channelId });
const isDm = useUIStore.getState().showDms;
if (isDm) {
wsSend({ type: 'dm_typing_start', dmChannelId: channelId });
} else {
wsSend({ type: 'typing_start', channelId });
}
typingTimeoutRef.current = setTimeout(() => {
typingTimeoutRef.current = undefined;
}, 3000);
@@ -107,7 +113,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
return (
<div className="px-4 pb-6 flex-shrink-0">
{replyTo && (
<div className="bg-[#2e3035] rounded-t-lg px-4 py-2 flex items-center justify-between border-b border-discord-bg-tertiary/50">
<div className="bg-discord-bg-hover rounded-t-lg px-4 py-2 flex items-center justify-between border-b border-discord-bg-tertiary/50">
<div className="flex items-center gap-1 text-[14px] text-discord-text-normal truncate">
<span className="opacity-60">Replying to</span>
<span className="font-bold">{replyTo.user.displayName ?? replyTo.user.username}</span>
@@ -131,7 +137,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
{files.length > 0 && (
<div className="p-4 flex flex-wrap gap-4 bg-discord-bg-secondary/30">
{files.map((file, i) => (
<div key={i} className="relative group bg-discord-bg-secondary rounded-lg p-2 max-w-[200px] shadow-sm border border-discord-bg-tertiary">
<div key={i} className="relative group bg-discord-bg-secondary rounded-lg p-2 max-w-[200px] shadow-elevation-low border border-discord-bg-tertiary">
{file.type.startsWith('image/') ? (
<img
src={URL.createObjectURL(file)}
@@ -148,7 +154,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
)}
<button
onClick={() => removeFile(i)}
className="absolute -top-2 -right-2 w-7 h-7 bg-discord-red hover:bg-discord-red-hover shadow-lg rounded-lg flex items-center justify-center text-white transition-colors z-10"
className="absolute -top-2 -right-2 w-7 h-7 bg-discord-red hover:bg-discord-red-hover shadow-elevation-high rounded-lg flex items-center justify-center text-white transition-colors z-10"
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M5 2a1 1 0 011-1h4a1 1 0 011 1v1h3a1 1 0 110 2h-.08L13 14a2 2 0 01-2 2H5a2 2 0 01-2-2L2.08 5H2a1 1 0 110-2h3V2zm2 0v1h2V2H7z" />
@@ -193,7 +199,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
onChange={handleChange}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder={`Message #${channelName}`}
placeholder={`Message ${channelName.startsWith('@') ? channelName : `#${channelName}`}`}
className="flex-1 py-[11px] px-1 bg-transparent text-discord-text-primary placeholder-discord-text-muted/60 outline-none resize-none text-[15px] leading-[1.375rem] max-h-[50vh] scrollbar-thin"
rows={1}
disabled={isUploading}