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 { useParams } from 'react-router-dom';
|
||||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
|
||||||
|
|
||||||
|
import { ComposerActionsMenu } from '../components/ComposerActionsMenu';
|
||||||
import { ConversationHeader } from '../components/ConversationHeader';
|
import { ConversationHeader } from '../components/ConversationHeader';
|
||||||
import { EmojiPicker } from '../components/EmojiPicker';
|
import { EmojiPicker } from '../components/EmojiPicker';
|
||||||
import { EmptyState } from '../components/EmptyState';
|
import { EmptyState } from '../components/EmptyState';
|
||||||
@@ -16,10 +17,8 @@ import {
|
|||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronUpIcon,
|
ChevronUpIcon,
|
||||||
EyeOffIcon,
|
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
PollIcon,
|
|
||||||
ReplyIcon,
|
ReplyIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
SendIcon,
|
SendIcon,
|
||||||
@@ -237,6 +236,8 @@ export function ConversationPage() {
|
|||||||
const [mentionState, setMentionState] = useState<{ query: string; start: number } | null>(null);
|
const [mentionState, setMentionState] = useState<{ query: string; start: number } | null>(null);
|
||||||
const [emojiOpen, setEmojiOpen] = useState(false);
|
const [emojiOpen, setEmojiOpen] = useState(false);
|
||||||
const [gifPickerOpen, setGifPickerOpen] = 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.
|
// 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
|
// Auto-clears on a successful send so the composer doesn't accidentally
|
||||||
// burn the message-after-next.
|
// burn the message-after-next.
|
||||||
@@ -1243,55 +1244,32 @@ export function ConversationPage() {
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => handleFilesChosen(e.target.files)}
|
onChange={(e) => handleFilesChosen(e.target.files)}
|
||||||
/>
|
/>
|
||||||
|
{/* [+] popover trigger — opens ComposerActionsMenu (file/poll/whiteboard/watch/game) */}
|
||||||
<button
|
<button
|
||||||
|
ref={actionsMenuAnchorRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => setActionsMenuOpen((v) => !v)}
|
||||||
aria-label="Datei anhängen"
|
aria-label={t('app:composer.more_actions', { defaultValue: 'Mehr Aktionen' })}
|
||||||
title="Datei anhängen"
|
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]"
|
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" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<ComposerActionsMenu
|
||||||
type="button"
|
anchorRef={actionsMenuAnchorRef}
|
||||||
onClick={() => {
|
open={actionsMenuOpen}
|
||||||
|
onClose={() => setActionsMenuOpen(false)}
|
||||||
|
onAttachFile={() => fileInputRef.current?.click()}
|
||||||
|
onCreatePoll={() => {
|
||||||
setPollError(null);
|
setPollError(null);
|
||||||
setPollDialogOpen(true);
|
setPollDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
aria-label="Umfrage erstellen"
|
onCreateWhiteboard={() => void handleCreateWhiteboard()}
|
||||||
title="Umfrage erstellen"
|
onStartWatchTogether={() => setWatchDialogOpen(true)}
|
||||||
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]"
|
onStartGame={() => setGameDialogOpen(true)}
|
||||||
>
|
canStartGame={conversation?.members?.length === 2}
|
||||||
<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>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1337,20 +1315,6 @@ export function ConversationPage() {
|
|||||||
onPick={(gif) => void handleGifPick(gif)}
|
onPick={(gif) => void handleGifPick(gif)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<VoiceRecorder
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
onComplete={async (file) => {
|
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