feat(P4B.T6): composer 'Whiteboard' button creates board + sends bubble
This commit is contained in:
@@ -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[],
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user