feat(P7.T3): composer toolbar — 5 inline buttons + popover for Bild/Poll/Aktivitäten
This commit is contained in:
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
import { ComposerActionsMenu } from '../components/ComposerActionsMenu';
|
||||
import { ConversationHeader } from '../components/ConversationHeader';
|
||||
import { EmojiPicker } from '../components/EmojiPicker';
|
||||
import { EmptyState } from '../components/EmptyState';
|
||||
@@ -16,10 +17,8 @@ import {
|
||||
ArrowRightIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
EyeOffIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
PollIcon,
|
||||
ReplyIcon,
|
||||
SearchIcon,
|
||||
SendIcon,
|
||||
@@ -237,6 +236,8 @@ export function ConversationPage() {
|
||||
const [mentionState, setMentionState] = useState<{ query: string; start: number } | null>(null);
|
||||
const [emojiOpen, setEmojiOpen] = useState(false);
|
||||
const [gifPickerOpen, setGifPickerOpen] = useState(false);
|
||||
const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
|
||||
const actionsMenuAnchorRef = useRef<HTMLButtonElement>(null);
|
||||
// Sticky toggle: when on, the next image(s) sent are marked view-once.
|
||||
// Auto-clears on a successful send so the composer doesn't accidentally
|
||||
// burn the message-after-next.
|
||||
@@ -1243,55 +1244,32 @@ export function ConversationPage() {
|
||||
className="hidden"
|
||||
onChange={(e) => handleFilesChosen(e.target.files)}
|
||||
/>
|
||||
{/* [+] popover trigger — opens ComposerActionsMenu (file/poll/whiteboard/watch/game) */}
|
||||
<button
|
||||
ref={actionsMenuAnchorRef}
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
aria-label="Datei anhängen"
|
||||
title="Datei anhängen"
|
||||
onClick={() => setActionsMenuOpen((v) => !v)}
|
||||
aria-label={t('app:composer.more_actions', { defaultValue: 'Mehr Aktionen' })}
|
||||
title={t('app:composer.more_actions', { defaultValue: 'Mehr Aktionen' })}
|
||||
aria-expanded={actionsMenuOpen}
|
||||
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 dark:hover:bg-[#313338]"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
<ComposerActionsMenu
|
||||
anchorRef={actionsMenuAnchorRef}
|
||||
open={actionsMenuOpen}
|
||||
onClose={() => setActionsMenuOpen(false)}
|
||||
onAttachFile={() => fileInputRef.current?.click()}
|
||||
onCreatePoll={() => {
|
||||
setPollError(null);
|
||||
setPollDialogOpen(true);
|
||||
}}
|
||||
aria-label="Umfrage erstellen"
|
||||
title="Umfrage erstellen"
|
||||
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 dark:hover:bg-[#313338]"
|
||||
>
|
||||
<PollIcon className="h-4 w-4" />
|
||||
</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>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setWatchDialogOpen(true)}
|
||||
title={t('app:composer.watch_together', { defaultValue: 'Watch Together' })}
|
||||
aria-label={t('app:composer.watch_together', { defaultValue: 'Watch Together' })}
|
||||
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 dark:hover:bg-[#313338]"
|
||||
>
|
||||
<PlayBoxIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGameDialogOpen(true)}
|
||||
title={t('app:composer.game', { defaultValue: 'Spielen' })}
|
||||
aria-label={t('app:composer.game', { defaultValue: 'Spielen' })}
|
||||
className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-fg-muted transition hover:bg-surface-3 hover:text-fg"
|
||||
>
|
||||
<GameIcon className="h-4 w-4" />
|
||||
</button>
|
||||
onCreateWhiteboard={() => void handleCreateWhiteboard()}
|
||||
onStartWatchTogether={() => setWatchDialogOpen(true)}
|
||||
onStartGame={() => setGameDialogOpen(true)}
|
||||
canStartGame={conversation?.members?.length === 2}
|
||||
/>
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1337,20 +1315,6 @@ export function ConversationPage() {
|
||||
onPick={(gif) => void handleGifPick(gif)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewOnceNext((v) => !v)}
|
||||
aria-pressed={viewOnceNext}
|
||||
title={viewOnceNext ? 'Nächstes Bild: einmal ansehen' : 'Nächstes Bild: normal'}
|
||||
className={
|
||||
'flex h-9 w-9 cursor-pointer items-center justify-center rounded-md transition ' +
|
||||
(viewOnceNext
|
||||
? 'bg-accent/20 text-accent'
|
||||
: 'text-fg-muted hover:bg-surface-3 hover:text-fg')
|
||||
}
|
||||
>
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<VoiceRecorder
|
||||
disabled={sending}
|
||||
onComplete={async (file) => {
|
||||
@@ -1898,32 +1862,3 @@ function AttachmentPreview({
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayBoxIcon(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="14" rx="2" />
|
||||
<path d="M10 9l5 3-5 3V9z" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function GameIcon(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="6" width="18" height="12" rx="3" />
|
||||
<path d="M8 12h4M10 10v4M16 11v.01M16 14v.01" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user