diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d5fc971..c852e4b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -21,6 +21,7 @@ "@chat-app/shared": "workspace:*", "@chat-app/ui-web": "workspace:*", "@livekit/components-react": "^2.9.0", + "@livekit/track-processors": "^0.7.2", "@supabase/supabase-js": "^2.46.0", "@tauri-apps/api": "^2.1.1", "@tauri-apps/plugin-fs": "^2.5.0", diff --git a/apps/desktop/public/favicon.svg b/apps/desktop/public/favicon.svg index 3906605..095d8ae 100644 --- a/apps/desktop/public/favicon.svg +++ b/apps/desktop/public/favicon.svg @@ -1,14 +1,7 @@ - - - - - - - - - - - + + + \ No newline at end of file diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-tauri/icons/128x128.png index 5bc9c4a..21d575e 100644 Binary files a/apps/desktop/src-tauri/icons/128x128.png and b/apps/desktop/src-tauri/icons/128x128.png differ diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-tauri/icons/128x128@2x.png index 5710d93..358035b 100644 Binary files a/apps/desktop/src-tauri/icons/128x128@2x.png and b/apps/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-tauri/icons/32x32.png index be5155e..030b29b 100644 Binary files a/apps/desktop/src-tauri/icons/32x32.png and b/apps/desktop/src-tauri/icons/32x32.png differ diff --git a/apps/desktop/src-tauri/icons/64x64.png b/apps/desktop/src-tauri/icons/64x64.png index 5c4a4fb..10a4fbe 100644 Binary files a/apps/desktop/src-tauri/icons/64x64.png and b/apps/desktop/src-tauri/icons/64x64.png differ diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-tauri/icons/Square107x107Logo.png index e0ec916..f199cc3 100644 Binary files a/apps/desktop/src-tauri/icons/Square107x107Logo.png and b/apps/desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-tauri/icons/Square142x142Logo.png index 91e9363..f09910a 100644 Binary files a/apps/desktop/src-tauri/icons/Square142x142Logo.png and b/apps/desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square150x150Logo.png b/apps/desktop/src-tauri/icons/Square150x150Logo.png index a2dd3f1..9679265 100644 Binary files a/apps/desktop/src-tauri/icons/Square150x150Logo.png and b/apps/desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-tauri/icons/Square284x284Logo.png index d6bd197..db691f2 100644 Binary files a/apps/desktop/src-tauri/icons/Square284x284Logo.png and b/apps/desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square30x30Logo.png b/apps/desktop/src-tauri/icons/Square30x30Logo.png index a0f765e..7dca92e 100644 Binary files a/apps/desktop/src-tauri/icons/Square30x30Logo.png and b/apps/desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.png b/apps/desktop/src-tauri/icons/Square310x310Logo.png index c51a48c..521146b 100644 Binary files a/apps/desktop/src-tauri/icons/Square310x310Logo.png and b/apps/desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square44x44Logo.png b/apps/desktop/src-tauri/icons/Square44x44Logo.png index 7fd26a8..2f80a33 100644 Binary files a/apps/desktop/src-tauri/icons/Square44x44Logo.png and b/apps/desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square71x71Logo.png b/apps/desktop/src-tauri/icons/Square71x71Logo.png index e6c5a9c..9463b7f 100644 Binary files a/apps/desktop/src-tauri/icons/Square71x71Logo.png and b/apps/desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-tauri/icons/Square89x89Logo.png index c78f3c0..a33106a 100644 Binary files a/apps/desktop/src-tauri/icons/Square89x89Logo.png and b/apps/desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/apps/desktop/src-tauri/icons/StoreLogo.png b/apps/desktop/src-tauri/icons/StoreLogo.png index b760a09..e2ebc7d 100644 Binary files a/apps/desktop/src-tauri/icons/StoreLogo.png and b/apps/desktop/src-tauri/icons/StoreLogo.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png index d4c4d2c..f60eb87 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png and b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png index 2b4cfd6..882f799 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png and b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png index 588cd53..f944ee1 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png and b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png index 0946d60..9150e35 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png and b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png index 283a8dd..2a3aa56 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png and b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png index 19aee93..6a6bca2 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png and b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png index ca277db..37a805a 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png and b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png index c61d53a..ed8fdb8 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png and b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png index 52654dd..52c5ebb 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png and b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png index 8ef22eb..60a0853 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png and b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png index fdf0416..db576a2 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png and b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png index 6564466..84d2a1e 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png and b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png index 6530c4a..fad2aae 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png and b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png index d0c7345..2cdbf06 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png and b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png index 0c6b7d2..a97005a 100644 Binary files a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png and b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns index 3ba2190..12480ab 100644 Binary files a/apps/desktop/src-tauri/icons/icon.icns and b/apps/desktop/src-tauri/icons/icon.icns differ diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-tauri/icons/icon.ico index 903a9f9..7372d86 100644 Binary files a/apps/desktop/src-tauri/icons/icon.ico and b/apps/desktop/src-tauri/icons/icon.ico differ diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png index 0b7b30f..9d732c1 100644 Binary files a/apps/desktop/src-tauri/icons/icon.png and b/apps/desktop/src-tauri/icons/icon.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png index 2f57f68..8ed4203 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png index d124636..a604fe4 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png index d124636..a604fe4 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png index 19990d6..bc3e437 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png index 720b9fb..4b07f22 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png index eb41d8a..2c642e3 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png index eb41d8a..2c642e3 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png index 0224adb..d6fac69 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png index d124636..a604fe4 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png index 2858238..104e055 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png index 2858238..104e055 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png index 18f4190..0825aa6 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png index 9c5fa73..7f1799e 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png index 18f4190..0825aa6 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png index 24576e4..e6322a9 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png index 2d9e6d9..14ab4fc 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png index 220094f..0925c16 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png index 7ec0ba6..676d90a 100644 Binary files a/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png and b/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/apps/desktop/src-tauri/icons/source.svg b/apps/desktop/src-tauri/icons/source.svg index 3906605..095d8ae 100644 --- a/apps/desktop/src-tauri/icons/source.svg +++ b/apps/desktop/src-tauri/icons/source.svg @@ -1,14 +1,7 @@ - - - - - - - - - - - + + + \ No newline at end of file diff --git a/apps/desktop/src/components/BackupRestoreDialog.tsx b/apps/desktop/src/components/BackupRestoreDialog.tsx new file mode 100644 index 0000000..ada38dc --- /dev/null +++ b/apps/desktop/src/components/BackupRestoreDialog.tsx @@ -0,0 +1,86 @@ +import { useEffect } from 'react'; + +import { DeviceRestore } from './DeviceRestore'; +import { AlertIcon, ShieldIcon, XIcon } from './icons'; + +interface Props { + open: boolean; + userId: string; + onClose: () => void; +} + +// Modal wrapper around DeviceRestore for the already-signed-in case. A +// successful restore swaps the local device-identity for the one embedded +// in the backup string — the app then hard-reloads so every hook +// re-initialises against the restored keys (simpler than invalidating +// supabase-realtime subscriptions, stronghold caches, livekit rooms, etc. +// individually). +export function BackupRestoreDialog({ open, userId, onClose }: Props) { + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', onKey); + const prev = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + window.removeEventListener('keydown', onKey); + document.body.style.overflow = prev; + }; + }, [open, onClose]); + + if (!open) return null; + + return ( +
+
e.stopPropagation()} + className="flex w-full max-w-md flex-col gap-4" + > +
+
+ +

+ Gerät aus Backup wiederherstellen +

+
+ +
+ +
+ +

+ Restore ersetzt das aktuelle Gerät durch das aus dem Backup. + Die App lädt danach neu. Nachrichten, die auf diesem Gerät seit + dem Backup eingegangen sind, sind erst wieder lesbar, nachdem + Peer-Geräte den Conversation-Key erneut für die wiederhergestellte + Device-ID wrappen. +

+
+ + { + // Hard reload — cleanest way to reset every hook, supabase + // realtime channel, stronghold handle, and cached state. + window.location.reload(); + }} + /> +
+
+ ); +} diff --git a/apps/desktop/src/components/MessageBubble.tsx b/apps/desktop/src/components/MessageBubble.tsx index f6842f3..639a0ad 100644 --- a/apps/desktop/src/components/MessageBubble.tsx +++ b/apps/desktop/src/components/MessageBubble.tsx @@ -56,6 +56,8 @@ interface Props { onReply?: (m: DecryptedMessage) => void; /** Hover action: parent opens forward dialog for current message. */ onForward?: (m: DecryptedMessage) => void; + /** Click on the message's avatar surfaces the author's profile card. */ + onAvatarClick?: (userId: string, ev: React.MouseEvent) => void; /** Highlighted state — set briefly after a jump. */ highlighted?: boolean; } @@ -76,6 +78,7 @@ export function MessageBubble({ onJumpToMessage, onReply, onForward, + onAvatarClick, highlighted = false, }: Props) { const { t } = useTranslation(['app']); @@ -97,6 +100,47 @@ export function MessageBubble({ const withinEditWindow = age < EDIT_WINDOW_MS; const bodyText = initialText; const attachments = initialAttachments; + + // /tempmsg ephemeral window. expireMs embedded in plaintext JSON; sender + // fires the soft-delete when the clock runs out. Receivers just watch + // the deletedAt flip via realtime. + const expireMs = parsed.kind === 'text' ? parsed.expireMs : undefined; + const [tickNow, setTickNow] = useState(() => Date.now()); + useEffect(() => { + if (expireMs === undefined) return; + const id = window.setInterval(() => setTickNow(Date.now()), 1000); + return () => window.clearInterval(id); + }, [expireMs]); + const msLeft = + expireMs !== undefined + ? Math.max(0, expireMs - (tickNow - createdAt.getTime())) + : null; + + useEffect(() => { + if (!mine) return; + if (expireMs === undefined) return; + if (message.deletedAt) return; + const remaining = Math.max(0, expireMs - age); + const timer = window.setTimeout(() => { + void softDeleteMessage(supabase, message.id).catch((err: unknown) => { + console.warn('ephemeral auto-delete failed', err); + }); + }, remaining); + return () => window.clearTimeout(timer); + }, [mine, expireMs, age, message.id, message.deletedAt]); + + // Receiver-side auto-hide when the expiry window elapses even if the + // sender's delete hasn't propagated yet (network hiccup, offline-sender). + const [localExpired, setLocalExpired] = useState( + msLeft !== null && msLeft <= 0, + ); + useEffect(() => { + if (expireMs === undefined) return; + if (localExpired) return; + const remaining = Math.max(0, expireMs - age); + const t = window.setTimeout(() => setLocalExpired(true), remaining); + return () => window.clearTimeout(t); + }, [expireMs, age, localExpired]); const canEdit = parsed.kind === 'text' && mine && @@ -181,13 +225,16 @@ export function MessageBubble({ [onToggleReaction], ); - if (message.deletedAt) { + if (message.deletedAt || localExpired) { return (
onAvatarClick(message.senderId, ev) } + : {})} />
{t('app:chats.deleted')} @@ -274,23 +321,30 @@ export function MessageBubble({ type="button" onClick={() => onJumpToMessage?.(quoted.id)} className={ - 'mb-1.5 flex w-full cursor-pointer items-stretch gap-2 rounded-md px-2 py-1.5 text-left text-xs transition hover:opacity-90 ' + + 'mb-1.5 flex w-full cursor-pointer items-stretch gap-2 rounded-md pl-2 pr-2.5 py-1.5 text-left text-xs transition hover:brightness-110 hover:shadow-sm ' + (mine - ? 'bg-white/15 text-accent-fg/90' - : 'bg-surface-2 text-fg-muted') + ? 'bg-white/10 text-accent-fg/90' + : 'bg-surface-2/80 text-fg-muted ring-1 ring-inset ring-line') } >
)} @@ -457,14 +522,29 @@ function AvatarSlot({ show, url, displayName, + onClick, }: { show: boolean; url: string | null; displayName: string | null; + onClick?: (ev: React.MouseEvent) => void; }) { if (!show) { return +
+ +
+ {error && (

{error} diff --git a/apps/desktop/src/components/UserProfilePopover.tsx b/apps/desktop/src/components/UserProfilePopover.tsx new file mode 100644 index 0000000..7ed5828 --- /dev/null +++ b/apps/desktop/src/components/UserProfilePopover.tsx @@ -0,0 +1,126 @@ +import type { ProfileBrief } from '@chat-app/shared/friends'; +import { useEffect } from 'react'; +import { createPortal } from 'react-dom'; + +import { usePeerPresence } from '../lib/usePeerPresence'; +import { Avatar } from './Avatar'; + +interface Props { + userId: string; + profile: ProfileBrief | null; + x: number; + y: number; + onClose: () => void; + onStartDm?: (userId: string) => void; +} + +const PRESENCE_DOT: Record = { + online: 'bg-emerald-500', + idle: 'bg-amber-400', + dnd: 'bg-rose-500', + invisible: 'bg-neutral-500', + offline: 'bg-neutral-400 dark:bg-neutral-600', +}; + +const CARD_W = 280; +const CARD_H = 180; + +// Hover/click card surfaced from avatars around the app. Shows display name, +// @handle, presence state + status message, plus a DM-start button when +// the clicked profile isn't the caller. +export function UserProfilePopover({ + userId, + profile, + x, + y, + onClose, + onStartDm, +}: Props) { + // Subscribe to the same presence feed that the conversation header uses, + // so status updates flow in live. + const presence = usePeerPresence(userId); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + const onDown = (e: MouseEvent) => { + const t = e.target as HTMLElement | null; + if (t?.closest('[data-user-popover]')) return; + if (t?.closest('[data-user-popover-trigger]')) return; + onClose(); + }; + window.addEventListener('keydown', onKey); + window.addEventListener('mousedown', onDown); + return () => { + window.removeEventListener('keydown', onKey); + window.removeEventListener('mousedown', onDown); + }; + }, [onClose]); + + const left = Math.min(Math.max(8, x), window.innerWidth - CARD_W - 8); + const top = Math.min(Math.max(8, y), window.innerHeight - CARD_H - 8); + + const displayName = profile?.displayName ?? '?'; + const username = profile?.username ?? ''; + const state = presence?.state ?? 'offline'; + const showPresence = state !== 'invisible'; + const statusMessage = presence?.statusMessage?.trim() ?? ''; + + return createPortal( +

+
+
+ + {showPresence && ( +
+
+

+ {displayName} +

+ {username && ( +

@{username}

+ )} +
+
+ + {statusMessage && state !== 'offline' && ( +

+ {statusMessage} +

+ )} + + {onStartDm && ( + + )} +
, + document.body, + ); +} diff --git a/apps/desktop/src/components/icons.tsx b/apps/desktop/src/components/icons.tsx index cf8793f..c51296e 100644 --- a/apps/desktop/src/components/icons.tsx +++ b/apps/desktop/src/components/icons.tsx @@ -519,86 +519,53 @@ export function AddUserIcon(props: IconProps) { ); } +// Soft-N mark: rounded-square violet tile with a stylised "N" glyph. Used +// wherever the app needs a standalone icon (sidebar rail, auth screen, +// favicon). Colour decisions sit inside the SVG so consumers just size the +// element via `className`. export function LogoMark(props: IconProps) { return ( ); } -// Full lockup: hex icon + "Netralax" wordmark. `tone` decides text colour: -// "dark" = white text (use on dark background), "light" = black text. +// Full lockup: soft-N mark + "Netralax" wordmark. `tone` decides text colour: +// "dark" = white text (use on dark background), "light" = near-black. export function LogoLockup({ tone = 'dark', ...props }: IconProps & { tone?: 'dark' | 'light' }) { - const textFill = tone === 'dark' ? '#ffffff' : '#0F172A'; + const textFill = tone === 'dark' ? '#ffffff' : '#14121c'; return (
  • + {firstUnreadRef.current === m.id && ( +
    + + + Neue Nachrichten + + +
    + )} { + ev.stopPropagation(); + setProfilePopover({ + userId: uid, + x: ev.clientX, + y: ev.clientY, + }); + }} highlighted={highlightedId === m.id} />
  • @@ -754,7 +862,7 @@ export function ConversationPage() { } if (pics.length > 0) { e.preventDefault(); - ingestFiles(pics); + void ingestFiles(pics); } }} rows={1} @@ -778,6 +886,20 @@ export function ConversationPage() { currentConversationId={id ?? null} onClose={() => setForwardTarget(null)} /> + + {profilePopover && ( + m.userId === profilePopover.userId)?.profile ?? + conversation?.peer ?? + null + } + x={profilePopover.x} + y={profilePopover.y} + onClose={() => setProfilePopover(null)} + /> + )} ); } diff --git a/apps/desktop/src/pages/SettingsPage.tsx b/apps/desktop/src/pages/SettingsPage.tsx index 7396f5c..71517cb 100644 --- a/apps/desktop/src/pages/SettingsPage.tsx +++ b/apps/desktop/src/pages/SettingsPage.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'; import { Avatar } from '../components/Avatar'; import { BackupExportDialog } from '../components/BackupExportDialog'; +import { BackupRestoreDialog } from '../components/BackupRestoreDialog'; import { RingtoneSettings } from '../components/RingtoneSettings'; import { SoundboardSettings } from '../components/SoundboardSettings'; import { LockIcon } from '../components/icons'; @@ -347,10 +348,99 @@ function AudioQualityControls() { 'Mono 48 kbps mit Noise-Suppression, Echo-Cancellation und Auto-Gain. Optimiert für Sprache im Raum.', })}

    + + + updateAudioSettings({ noiseSuppression: v })} + /> + +

    + {t('app:settings.noise_suppression_hint', { + defaultValue: + 'Unterdrückt Hintergrundgeräusche (Tastatur, Lüfter, Café-Lärm). Ausschalten nur bei Musik/Instrumenten.', + })} +

    + + + updateAudioSettings({ videoBackgroundBlur: v })} + /> + +

    + {t('app:settings.video_blur_hint', { + defaultValue: + 'Blendet den Hintergrund hinter dir aus. Braucht etwas GPU-Leistung und lädt beim ersten Aktivieren ~1,5 MB Modell nach.', + })} +

    + + +
    + + updateAudioSettings({ voiceThreshold: Number(e.target.value) }) + } + className="flex-1 accent-accent" + /> + + {(cfg.voiceThreshold * 100).toFixed(1)} + +
    +
    +

    + {t('app:settings.voice_threshold_hint', { + defaultValue: + 'Wann der grüne Sprech-Ring aufleuchtet. Niedriger = empfindlicher (leise Stimme erfassen), höher = tolerant gegen Raumlärm.', + })} +

    ); } +function InlineToggle({ + checked, + onChange, +}: { + checked: boolean; + onChange: (next: boolean) => void; +}) { + return ( + + ); +} + interface AvatarControlsProps { patchProfile: (patch: Parameters[1]) => Promise; busy: boolean; @@ -464,6 +554,7 @@ function DeviceKeyBackupControls() { const { t } = useTranslation(['app']); const { profile, device } = useAuth(); const [open, setOpen] = useState(false); + const [restoreOpen, setRestoreOpen] = useState(false); const [privateKey, setPrivateKey] = useState(null); const [err, setErr] = useState(null); const canRun = !!profile?.userId && !!device?.id; @@ -502,14 +593,24 @@ function DeviceKeyBackupControls() { 'Backup deines Geräteschlüssels. Ohne Backup kein Zugriff auf alte Nachrichten wenn Gerät oder Browser-Storage verloren geht. Wiederherstellung passiert im Gerät-Einrichtungsbildschirm.', })}

    - +
    + + +
    {err && (

    {err}

    )} @@ -523,6 +624,14 @@ function DeviceKeyBackupControls() { onClose={handleClose} /> )} + + {profile && ( + setRestoreOpen(false)} + /> + )} ); } diff --git a/packages/shared/src/chat/attachments.ts b/packages/shared/src/chat/attachments.ts index c3433b3..2746365 100644 --- a/packages/shared/src/chat/attachments.ts +++ b/packages/shared/src/chat/attachments.ts @@ -39,6 +39,10 @@ export interface TextMessagePayload { type?: 'text'; text: string; attachments: AttachmentHandle[]; + /** When set, the message self-destructs `expireMs` milliseconds after the + * server's createdAt. Sender initiates the delete; receivers hide locally + * as soon as the window elapses. Added in /tempmsg support. */ + expireMs?: number; } export interface CallEventPayload { @@ -52,7 +56,12 @@ export interface CallEventPayload { export type MessagePayload = TextMessagePayload | CallEventPayload; export type ParsedMessagePayload = - | { kind: 'text'; text: string; attachments: AttachmentHandle[] } + | { + kind: 'text'; + text: string; + attachments: AttachmentHandle[]; + expireMs?: number; + } | { kind: 'call_event'; status: CallEventStatus; @@ -91,6 +100,9 @@ export function parseMessagePayload(raw: string | null): ParsedMessagePayload { kind: 'text', text: typeof t.text === 'string' ? t.text : '', attachments: Array.isArray(t.attachments) ? t.attachments : [], + ...(typeof t.expireMs === 'number' && t.expireMs > 0 + ? { expireMs: t.expireMs } + : {}), }; } } catch { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4153a5b..58ce991 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@livekit/components-react': specifier: ^2.9.0 version: 2.9.20(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1) + '@livekit/track-processors': + specifier: ^0.7.2 + version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22)) '@supabase/supabase-js': specifier: ^2.46.0 version: 2.103.3 @@ -1354,6 +1357,15 @@ packages: '@livekit/protocol@1.45.3': resolution: {integrity: sha512-WmMxBTsy4dRBqcrswFwUUlgq3Z0nnhOqKR6tX749Rb/PcB1yBMUtrHxZvcsS6qi3/5+86zHeVG+exmu1sZqfJg==} + '@livekit/track-processors@0.7.2': + resolution: {integrity: sha512-lzARBKTbBwqycdR/SwTu6//N0l20BzfDd7grxCXl07676SwRApNtZAK1GJjL1m3dCM3KBqH1aVxjMpNcbOw5uQ==} + peerDependencies: + '@types/dom-mediacapture-transform': ^0.1.9 + livekit-client: ^1.12.0 || ^2.1.0 + + '@mediapipe/tasks-vision@0.10.14': + resolution: {integrity: sha512-vOifgZhkndgybdvoRITzRkIueWWSiCKuEUXXK6Q4FaJsFvRJuwgg++vqFUMlL0Uox62U5aEXFhHxlhV7Ja5e3Q==} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -1835,6 +1847,12 @@ packages: '@types/dom-mediacapture-record@1.0.22': resolution: {integrity: sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==} + '@types/dom-mediacapture-transform@0.1.11': + resolution: {integrity: sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==} + + '@types/dom-webcodecs@0.1.18': + resolution: {integrity: sha512-vAvE8C9DGWR+tkb19xyjk1TSUlJ7RUzzp4a9Anu7mwBT+fpyePWK1UxmH14tMO5zHmrnrRIMg5NutnnDztLxgg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -6906,6 +6924,14 @@ snapshots: dependencies: '@bufbuild/protobuf': 1.10.1 + '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))': + dependencies: + '@mediapipe/tasks-vision': 0.10.14 + '@types/dom-mediacapture-transform': 0.1.11 + livekit-client: 2.18.3(@types/dom-mediacapture-record@1.0.22) + + '@mediapipe/tasks-vision@0.10.14': {} + '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -7485,6 +7511,12 @@ snapshots: '@types/dom-mediacapture-record@1.0.22': {} + '@types/dom-mediacapture-transform@0.1.11': + dependencies: + '@types/dom-webcodecs': 0.1.18 + + '@types/dom-webcodecs@0.1.18': {} + '@types/estree@1.0.8': {} '@types/graceful-fs@4.1.9':