diff --git a/apps/desktop/src/components/MessageBubble.tsx b/apps/desktop/src/components/MessageBubble.tsx index 067bbf0..15e5174 100644 --- a/apps/desktop/src/components/MessageBubble.tsx +++ b/apps/desktop/src/components/MessageBubble.tsx @@ -442,6 +442,32 @@ export function MessageBubble({ defaultValue: 'Nachricht nicht lesbar', })} + ) : parsed.kind === 'watch_together' ? ( +
+
+ + + + +
+
+
Watch Together
+
YouTube synchronisiert ansehen
+
+ +
) : parsed.kind === 'whiteboard' ? (
diff --git a/apps/desktop/src/lib/conversationFeatures.ts b/apps/desktop/src/lib/conversationFeatures.ts index 2dc52a0..4fd2ed1 100644 --- a/apps/desktop/src/lib/conversationFeatures.ts +++ b/apps/desktop/src/lib/conversationFeatures.ts @@ -5,6 +5,7 @@ import { type DecryptedMessage, type PollOption, type WhiteboardPayload, + type WatchTogetherPayload, } from '@chat-app/shared/chat'; export type AttachmentBucket = 'media' | 'audio' | 'files'; @@ -107,6 +108,15 @@ export function createWhiteboardPayload(whiteboardId: string): string { return serializeMessagePayload(payload); } +export function createWatchTogetherPayload(sessionId: string): string { + const payload: WatchTogetherPayload = { + v: 1, + type: 'watch_together', + session_id: sessionId, + }; + return serializeMessagePayload(payload); +} + export function summarizePollVotes( options: PollOption[], reactions: ReactionSummaryInput[], diff --git a/apps/desktop/src/pages/ConversationPage.tsx b/apps/desktop/src/pages/ConversationPage.tsx index 83871e6..03062a6 100644 --- a/apps/desktop/src/pages/ConversationPage.tsx +++ b/apps/desktop/src/pages/ConversationPage.tsx @@ -46,9 +46,11 @@ import { collectConversationAttachments, createPollPayload, createWhiteboardPayload, + createWatchTogetherPayload, } from '../lib/conversationFeatures'; import { WhiteboardModal } from '../components/WhiteboardModal'; -import { createWhiteboard } from '@chat-app/shared/chat'; +import { WatchTogetherModal } from '../components/WatchTogetherModal'; +import { createWhiteboard, createWatchSession, parseYouTubeUrl } from '@chat-app/shared/chat'; import { compressImages } from '../lib/imageCompress'; import { ensureInstallId } from '../lib/installId'; import { searchCachedMessages } from '../lib/messageCache'; @@ -169,6 +171,11 @@ export function ConversationPage() { const [pollSending, setPollSending] = useState(false); const [openWhiteboardId, setOpenWhiteboardId] = useState(null); const [creatingWhiteboard, setCreatingWhiteboard] = useState(false); + const [openWatchSessionId, setOpenWatchSessionId] = useState(null); + const [watchDialogOpen, setWatchDialogOpen] = useState(false); + const [watchUrl, setWatchUrl] = useState(''); + const [watchError, setWatchError] = useState(null); + const [watchCreating, setWatchCreating] = useState(false); const [pollError, setPollError] = useState(null); const [replyTo, setReplyTo] = useState(null); const [forwardTarget, setForwardTarget] = useState(null); @@ -472,6 +479,15 @@ export function ConversationPage() { return () => window.removeEventListener('chatapp:open-whiteboard', onOpen); }, []); + useEffect(() => { + const onOpen = (e: Event) => { + const detail = (e as CustomEvent<{ id?: string }>).detail; + if (detail?.id) setOpenWatchSessionId(detail.id); + }; + window.addEventListener('chatapp:open-watch-together', onOpen); + return () => window.removeEventListener('chatapp:open-watch-together', onOpen); + }, []); + useEffect(() => { if (id && messages.length > 0) markRead(id); }, [id, messages.length, markRead]); @@ -622,6 +638,31 @@ export function ConversationPage() { } }, [id, creatingWhiteboard, send, replyTo?.id]); + const handleStartWatchTogether = useCallback(async () => { + if (!id) return; + const videoId = parseYouTubeUrl(watchUrl); + if (!videoId) { + setWatchError('Ungültige YouTube-URL.'); + return; + } + setWatchCreating(true); + setWatchError(null); + try { + const ws = await createWatchSession(supabase, { conversationId: id, videoId }); + const payload = createWatchTogetherPayload(ws.id); + await send(payload, [], replyTo?.id ?? null); + setReplyTo(null); + setStickToBottom(true); + setWatchDialogOpen(false); + setWatchUrl(''); + setOpenWatchSessionId(ws.id); + } catch (err: unknown) { + setWatchError(err instanceof Error ? err.message : 'Konnte Watch-Together nicht starten'); + } finally { + setWatchCreating(false); + } + }, [id, watchUrl, send, replyTo?.id]); + async function ingestFiles(files: File[]) { const compressed = await compressImages(files); const next: File[] = []; @@ -1025,6 +1066,15 @@ export function ConversationPage() { > +
+ +
+
+
+ )} ); } @@ -1520,3 +1623,13 @@ function WhiteboardIcon(props: React.SVGProps) { ); } + +function PlayBoxIcon(props: React.SVGProps) { + return ( + + + + + ); +}