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() {
>
+
)}
+ {openGameId && (
+
setOpenGameId(null)}
+ />
+ )}
+
+ {gameDialogOpen && (
+ {
+ if (e.target === e.currentTarget) setGameDialogOpen(false);
+ }}
+ >
+
+
+ {t('app:game.pick_title', { defaultValue: 'Spiel auswählen' })}
+
+ {gameError && (
+
{gameError}
+ )}
+
+
+
+
+
+
+
+
+
+ )}
+
{watchDialogOpen && (
) {
);
}
+
+function GameIcon(props: React.SVGProps
) {
+ return (
+
+ );
+}