feat(P4B.T6): composer 'Whiteboard' button creates board + sends bubble

This commit is contained in:
byGalax
2026-05-16 20:36:03 +02:00
parent 5e59ee22f6
commit 383245ec90
2 changed files with 63 additions and 1 deletions
@@ -4,6 +4,7 @@ import {
type AttachmentHandle, type AttachmentHandle,
type DecryptedMessage, type DecryptedMessage,
type PollOption, type PollOption,
type WhiteboardPayload,
} from '@chat-app/shared/chat'; } from '@chat-app/shared/chat';
export type AttachmentBucket = 'media' | 'audio' | 'files'; export type AttachmentBucket = 'media' | 'audio' | 'files';
@@ -97,6 +98,15 @@ export function createPollPayload(question: string, optionTexts: string[]): stri
}); });
} }
export function createWhiteboardPayload(whiteboardId: string): string {
const payload: WhiteboardPayload = {
v: 1,
type: 'whiteboard',
whiteboard_id: whiteboardId,
};
return serializeMessagePayload(payload);
}
export function summarizePollVotes( export function summarizePollVotes(
options: PollOption[], options: PollOption[],
reactions: ReactionSummaryInput[], reactions: ReactionSummaryInput[],
+53 -1
View File
@@ -42,7 +42,13 @@ import type { DecryptedMessage } from '@chat-app/shared/chat';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useCall } from '../context/CallContext'; import { useCall } from '../context/CallContext';
import { useConversationsContext } from '../context/ConversationsContext'; import { useConversationsContext } from '../context/ConversationsContext';
import { collectConversationAttachments, createPollPayload } from '../lib/conversationFeatures'; import {
collectConversationAttachments,
createPollPayload,
createWhiteboardPayload,
} from '../lib/conversationFeatures';
import { WhiteboardModal } from '../components/WhiteboardModal';
import { createWhiteboard } from '@chat-app/shared/chat';
import { compressImages } from '../lib/imageCompress'; import { compressImages } from '../lib/imageCompress';
import { ensureInstallId } from '../lib/installId'; import { ensureInstallId } from '../lib/installId';
import { searchCachedMessages } from '../lib/messageCache'; import { searchCachedMessages } from '../lib/messageCache';
@@ -161,6 +167,8 @@ export function ConversationPage() {
const [mediaDrawerOpen, setMediaDrawerOpen] = useState(false); const [mediaDrawerOpen, setMediaDrawerOpen] = useState(false);
const [pollDialogOpen, setPollDialogOpen] = useState(false); const [pollDialogOpen, setPollDialogOpen] = useState(false);
const [pollSending, setPollSending] = useState(false); const [pollSending, setPollSending] = useState(false);
const [openWhiteboardId, setOpenWhiteboardId] = useState<string | null>(null);
const [creatingWhiteboard, setCreatingWhiteboard] = useState(false);
const [pollError, setPollError] = useState<string | null>(null); const [pollError, setPollError] = useState<string | null>(null);
const [replyTo, setReplyTo] = useState<DecryptedMessage | null>(null); const [replyTo, setReplyTo] = useState<DecryptedMessage | null>(null);
const [forwardTarget, setForwardTarget] = useState<DecryptedMessage | null>(null); const [forwardTarget, setForwardTarget] = useState<DecryptedMessage | null>(null);
@@ -588,6 +596,23 @@ export function ConversationPage() {
[send, replyTo?.id, notifyStopTyping], [send, replyTo?.id, notifyStopTyping],
); );
const handleCreateWhiteboard = useCallback(async () => {
if (!id || creatingWhiteboard) return;
setCreatingWhiteboard(true);
try {
const board = await createWhiteboard(supabase, id);
const payload = createWhiteboardPayload(board.id);
await send(payload, [], replyTo?.id ?? null);
setReplyTo(null);
setStickToBottom(true);
setOpenWhiteboardId(board.id);
} catch (err: unknown) {
setSendError(err instanceof Error ? err.message : 'Whiteboard konnte nicht angelegt werden');
} finally {
setCreatingWhiteboard(false);
}
}, [id, creatingWhiteboard, send, replyTo?.id]);
async function ingestFiles(files: File[]) { async function ingestFiles(files: File[]) {
const compressed = await compressImages(files); const compressed = await compressImages(files);
const next: File[] = []; const next: File[] = [];
@@ -981,6 +1006,16 @@ export function ConversationPage() {
> >
<PollIcon className="h-4 w-4" /> <PollIcon className="h-4 w-4" />
</button> </button>
<button
type="button"
onClick={() => void handleCreateWhiteboard()}
disabled={creatingWhiteboard}
title={t('app:composer.whiteboard', { defaultValue: 'Whiteboard' })}
aria-label={t('app:composer.whiteboard', { defaultValue: 'Whiteboard' })}
className="inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-lg text-fg-muted transition hover:bg-surface-3 hover:text-fg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-[#313338]"
>
<WhiteboardIcon className="h-4 w-4" />
</button>
<div className="relative"> <div className="relative">
<button <button
type="button" type="button"
@@ -1180,6 +1215,13 @@ export function ConversationPage() {
}} }}
/> />
)} )}
{openWhiteboardId && (
<WhiteboardModal
whiteboardId={openWhiteboardId}
onClose={() => setOpenWhiteboardId(null)}
/>
)}
</div> </div>
); );
} }
@@ -1459,3 +1501,13 @@ function AttachmentPreview({
</div> </div>
); );
} }
function WhiteboardIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}
strokeLinecap="round" strokeLinejoin="round" {...props}>
<rect x="3" y="4" width="18" height="13" rx="2" />
<path d="M8 21h8M12 17v4" />
</svg>
);
}