diff --git a/apps/desktop/src/components/MessageBubble.tsx b/apps/desktop/src/components/MessageBubble.tsx index 15e5174..0b1eee9 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 === 'game' ? ( +
+
+ + + + +
+
+
{parsed.gameType === 'c4' ? 'Vier-Gewinnt' : 'Tic-Tac-Toe'}
+
Gemeinsam spielen
+
+ +
) : parsed.kind === 'watch_together' ? (
diff --git a/apps/desktop/src/lib/conversationFeatures.ts b/apps/desktop/src/lib/conversationFeatures.ts index 4fd2ed1..bc1f023 100644 --- a/apps/desktop/src/lib/conversationFeatures.ts +++ b/apps/desktop/src/lib/conversationFeatures.ts @@ -6,6 +6,7 @@ import { type PollOption, type WhiteboardPayload, type WatchTogetherPayload, + type GamePayload, } from '@chat-app/shared/chat'; export type AttachmentBucket = 'media' | 'audio' | 'files'; @@ -117,6 +118,16 @@ export function createWatchTogetherPayload(sessionId: string): string { return serializeMessagePayload(payload); } +export function createGamePayload(gameId: string, gameType: 'ttt' | 'c4'): string { + const payload: GamePayload = { + v: 1, + type: 'game', + game_id: gameId, + game_type: gameType, + }; + 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 03062a6..214a014 100644 --- a/apps/desktop/src/pages/ConversationPage.tsx +++ b/apps/desktop/src/pages/ConversationPage.tsx @@ -47,10 +47,12 @@ import { createPollPayload, createWhiteboardPayload, createWatchTogetherPayload, + createGamePayload, } from '../lib/conversationFeatures'; import { WhiteboardModal } from '../components/WhiteboardModal'; import { WatchTogetherModal } from '../components/WatchTogetherModal'; -import { createWhiteboard, createWatchSession, parseYouTubeUrl } from '@chat-app/shared/chat'; +import { GameModal } from '../components/GameModal'; +import { createWhiteboard, createWatchSession, parseYouTubeUrl, createGame, type GameType } from '@chat-app/shared/chat'; import { compressImages } from '../lib/imageCompress'; import { ensureInstallId } from '../lib/installId'; import { searchCachedMessages } from '../lib/messageCache'; @@ -176,6 +178,10 @@ export function ConversationPage() { const [watchUrl, setWatchUrl] = useState(''); const [watchError, setWatchError] = useState(null); const [watchCreating, setWatchCreating] = useState(false); + const [openGameId, setOpenGameId] = useState(null); + const [gameDialogOpen, setGameDialogOpen] = useState(false); + const [gameError, setGameError] = useState(null); + const [gameCreating, setGameCreating] = useState(false); const [pollError, setPollError] = useState(null); const [replyTo, setReplyTo] = useState(null); const [forwardTarget, setForwardTarget] = useState(null); @@ -488,6 +494,15 @@ export function ConversationPage() { return () => window.removeEventListener('chatapp:open-watch-together', onOpen); }, []); + useEffect(() => { + const onOpen = (e: Event) => { + const detail = (e as CustomEvent<{ id?: string }>).detail; + if (detail?.id) setOpenGameId(detail.id); + }; + window.addEventListener('chatapp:open-game', onOpen); + return () => window.removeEventListener('chatapp:open-game', onOpen); + }, []); + useEffect(() => { if (id && messages.length > 0) markRead(id); }, [id, messages.length, markRead]); @@ -663,6 +678,38 @@ export function ConversationPage() { } }, [id, watchUrl, send, replyTo?.id]); + const handleStartGame = useCallback(async (gameType: GameType) => { + if (!id) return; + if (!conversation || conversation.members.length !== 2) { + setGameError('Spiele aktuell nur in 1:1-Chats.'); + return; + } + const opponent = conversation.members.find((m) => m.userId !== myId); + if (!opponent) { + setGameError('Kein Gegner gefunden.'); + return; + } + setGameCreating(true); + setGameError(null); + try { + const game = await createGame(supabase, { + conversationId: id, + gameType, + opponentUserId: opponent.userId, + }); + const payload = createGamePayload(game.id, gameType); + await send(payload, [], replyTo?.id ?? null); + setReplyTo(null); + setStickToBottom(true); + setGameDialogOpen(false); + setOpenGameId(game.id); + } catch (err: unknown) { + setGameError(err instanceof Error ? err.message : 'Konnte Spiel nicht starten'); + } finally { + setGameCreating(false); + } + }, [id, conversation, myId, send, replyTo?.id]); + async function ingestFiles(files: File[]) { const compressed = await compressImages(files); const next: File[] = []; @@ -1075,6 +1122,15 @@ export function ConversationPage() { > +
+ +
+
+ +
+
+
+ )} + {watchDialogOpen && (
) { ); } + +function GameIcon(props: React.SVGProps) { + return ( + + + + + ); +}