feat(P3-6): configurable composer toolbar buttons
CI / Build & Quality Checks (push) Successful in 10m24s
Trigger Desktop Build / trigger (push) Successful in 7s

Each button (Format, Emoji, Sticker, GIF, Location, Poll, Voice,
Schedule) can be individually hidden in Settings → Editor.
All default to on, stored in composerToolbarButtons object.
getSettings deep-merges the nested object so new buttons default
to true for existing users with saved settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:20:29 -04:00
parent b7daabe2e0
commit 891f2daf99
3 changed files with 275 additions and 123 deletions
+28 -3
View File
@@ -209,6 +209,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const imagePackRooms: Room[] = useImagePackRooms(roomId, roomToParents); const imagePackRooms: Room[] = useImagePackRooms(roomId, roomToParents);
const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar'); const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
const [composerToolbarButtons] = useSetting(settingsAtom, 'composerToolbarButtons');
const showFormat = composerToolbarButtons?.showFormat ?? true;
const showEmoji = composerToolbarButtons?.showEmoji ?? true;
const showSticker = composerToolbarButtons?.showSticker ?? true;
const showGif = composerToolbarButtons?.showGif ?? true;
const showLocation = composerToolbarButtons?.showLocation ?? true;
const showPoll = composerToolbarButtons?.showPoll ?? true;
const showVoice = composerToolbarButtons?.showVoice ?? true;
const showSchedule = composerToolbarButtons?.showSchedule ?? true;
const [locating, setLocating] = React.useState(false); const [locating, setLocating] = React.useState(false);
const [locationError, setLocationError] = React.useState<string | null>(null); const [locationError, setLocationError] = React.useState<string | null>(null);
const handleShareLocation = () => { const handleShareLocation = () => {
@@ -933,6 +942,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
} }
after={ after={
<> <>
{showFormat && (
<IconButton <IconButton
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
@@ -943,6 +953,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
> >
<Icon src={toolbar ? Icons.AlphabetUnderline : Icons.Alphabet} /> <Icon src={toolbar ? Icons.AlphabetUnderline : Icons.Alphabet} />
</IconButton> </IconButton>
)}
{(showEmoji || showSticker) && (
<UseStateProvider initial={undefined}> <UseStateProvider initial={undefined}>
{(emojiBoardTab: EmojiBoardTab | undefined, setEmojiBoardTab) => ( {(emojiBoardTab: EmojiBoardTab | undefined, setEmojiBoardTab) => (
<PopOut <PopOut
@@ -978,7 +990,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
</React.Suspense> </React.Suspense>
} }
> >
{!hideStickerBtn && ( {showSticker && !hideStickerBtn && (
<IconButton <IconButton
aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker} aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
aria-label="Insert sticker" aria-label="Insert sticker"
@@ -993,6 +1005,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
/> />
</IconButton> </IconButton>
)} )}
{showEmoji && (
<IconButton <IconButton
ref={emojiBtnRef} ref={emojiBtnRef}
aria-label="Insert emoji" aria-label="Insert emoji"
@@ -1007,14 +1020,18 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
<Icon <Icon
src={Icons.Smile} src={Icons.Smile}
filled={ filled={
hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji hideStickerBtn
? !!emojiBoardTab
: emojiBoardTab === EmojiBoardTab.Emoji
} }
/> />
</IconButton> </IconButton>
)}
</PopOut> </PopOut>
)} )}
</UseStateProvider> </UseStateProvider>
{!!gifApiKey && ( )}
{!!gifApiKey && showGif && (
<UseStateProvider initial={false}> <UseStateProvider initial={false}>
{(gifOpen: boolean, setGifOpen) => ( {(gifOpen: boolean, setGifOpen) => (
<PopOut <PopOut
@@ -1093,6 +1110,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
{locationError} {locationError}
</Text> </Text>
)} )}
{showLocation && (
<IconButton <IconButton
onClick={handleShareLocation} onClick={handleShareLocation}
disabled={locating} disabled={locating}
@@ -1108,6 +1126,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
<Icon src={Icons.SpaceGlobe} size="100" /> <Icon src={Icons.SpaceGlobe} size="100" />
)} )}
</IconButton> </IconButton>
)}
{showPoll && (
<IconButton <IconButton
onClick={() => setPollOpen(true)} onClick={() => setPollOpen(true)}
aria-label="Create poll" aria-label="Create poll"
@@ -1118,6 +1138,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
> >
<Icon src={Icons.OrderList} size="100" /> <Icon src={Icons.OrderList} size="100" />
</IconButton> </IconButton>
)}
{showVoice && (
<VoiceMessageRecorder <VoiceMessageRecorder
onSend={handleVoiceSend} onSend={handleVoiceSend}
onError={(err) => { onError={(err) => {
@@ -1125,6 +1147,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
setTimeout(() => setLocationError(null), 4000); setTimeout(() => setLocationError(null), 4000);
}} }}
/> />
)}
{charCount > 0 && ( {charCount > 0 && (
<Text <Text
size="T200" size="T200"
@@ -1141,6 +1164,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
{charCount} {charCount}
</Text> </Text>
)} )}
{showSchedule && (
<IconButton <IconButton
onClick={handleScheduleClick} onClick={handleScheduleClick}
variant="SurfaceVariant" variant="SurfaceVariant"
@@ -1151,6 +1175,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
> >
<Icon src={Icons.Clock} size="100" /> <Icon src={Icons.Clock} size="100" />
</IconButton> </IconButton>
)}
<IconButton <IconButton
onClick={submit} onClick={submit}
variant="SurfaceVariant" variant="SurfaceVariant"
@@ -37,6 +37,7 @@ import { SequenceCard } from '../../../components/sequence-card';
import { useSetting } from '../../../state/hooks/settings'; import { useSetting } from '../../../state/hooks/settings';
import { import {
ChatBackground, ChatBackground,
ComposerToolbarSettings,
DateFormat, DateFormat,
MessageLayout, MessageLayout,
MessageSpacing, MessageSpacing,
@@ -855,6 +856,14 @@ function Editor() {
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const [editorToolbar, setEditorToolbar] = useSetting(settingsAtom, 'editorToolbar'); const [editorToolbar, setEditorToolbar] = useSetting(settingsAtom, 'editorToolbar');
const [composerToolbarButtons, setComposerToolbarButtons] = useSetting(
settingsAtom,
'composerToolbarButtons',
);
const toggleToolbarButton = (key: keyof ComposerToolbarSettings) => {
setComposerToolbarButtons({ ...composerToolbarButtons, [key]: !composerToolbarButtons[key] });
};
return ( return (
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
@@ -881,6 +890,90 @@ function Editor() {
after={<Switch variant="Primary" value={editorToolbar} onChange={setEditorToolbar} />} after={<Switch variant="Primary" value={editorToolbar} onChange={setEditorToolbar} />}
/> />
</SequenceCard> </SequenceCard>
<Text size="L400" style={{ marginTop: '8px' }}>Composer Toolbar Buttons</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Format Toggle"
description="Button to show/hide the text formatting toolbar."
after={
<Switch
variant="Primary"
value={composerToolbarButtons?.showFormat ?? true}
onChange={() => toggleToolbarButton('showFormat')}
/>
}
/>
<SettingTile
title="Emoji"
after={
<Switch
variant="Primary"
value={composerToolbarButtons?.showEmoji ?? true}
onChange={() => toggleToolbarButton('showEmoji')}
/>
}
/>
<SettingTile
title="Sticker"
after={
<Switch
variant="Primary"
value={composerToolbarButtons?.showSticker ?? true}
onChange={() => toggleToolbarButton('showSticker')}
/>
}
/>
<SettingTile
title="GIF"
after={
<Switch
variant="Primary"
value={composerToolbarButtons?.showGif ?? true}
onChange={() => toggleToolbarButton('showGif')}
/>
}
/>
<SettingTile
title="Location"
after={
<Switch
variant="Primary"
value={composerToolbarButtons?.showLocation ?? true}
onChange={() => toggleToolbarButton('showLocation')}
/>
}
/>
<SettingTile
title="Poll"
after={
<Switch
variant="Primary"
value={composerToolbarButtons?.showPoll ?? true}
onChange={() => toggleToolbarButton('showPoll')}
/>
}
/>
<SettingTile
title="Voice Message"
after={
<Switch
variant="Primary"
value={composerToolbarButtons?.showVoice ?? true}
onChange={() => toggleToolbarButton('showVoice')}
/>
}
/>
<SettingTile
title="Schedule Message"
after={
<Switch
variant="Primary"
value={composerToolbarButtons?.showSchedule ?? true}
onChange={() => toggleToolbarButton('showSchedule')}
/>
}
/>
</SequenceCard>
</Box> </Box>
); );
} }
+35 -1
View File
@@ -38,6 +38,28 @@ export enum MessageLayout {
Bubble = 2, Bubble = 2,
} }
export interface ComposerToolbarSettings {
showFormat: boolean;
showEmoji: boolean;
showSticker: boolean;
showGif: boolean;
showLocation: boolean;
showPoll: boolean;
showVoice: boolean;
showSchedule: boolean;
}
export const DEFAULT_COMPOSER_TOOLBAR: ComposerToolbarSettings = {
showFormat: true,
showEmoji: true,
showSticker: true,
showGif: true,
showLocation: true,
showPoll: true,
showVoice: true,
showSchedule: true,
};
export interface Settings { export interface Settings {
themeId?: string; themeId?: string;
useSystemTheme: boolean; useSystemTheme: boolean;
@@ -101,6 +123,8 @@ export interface Settings {
warnOnUnverifiedDevices: boolean; warnOnUnverifiedDevices: boolean;
pauseAnimations: boolean; pauseAnimations: boolean;
composerToolbarButtons: ComposerToolbarSettings;
} }
const defaultSettings: Settings = { const defaultSettings: Settings = {
@@ -166,13 +190,23 @@ const defaultSettings: Settings = {
warnOnUnverifiedDevices: false, warnOnUnverifiedDevices: false,
pauseAnimations: false, pauseAnimations: false,
composerToolbarButtons: DEFAULT_COMPOSER_TOOLBAR,
}; };
export const getSettings = (): Settings => { export const getSettings = (): Settings => {
try { try {
const settings = localStorage.getItem(STORAGE_KEY); const settings = localStorage.getItem(STORAGE_KEY);
if (settings === null) return defaultSettings; if (settings === null) return defaultSettings;
return { ...defaultSettings, ...(JSON.parse(settings) as Settings) }; const saved = JSON.parse(settings) as Partial<Settings>;
return {
...defaultSettings,
...saved,
composerToolbarButtons: {
...DEFAULT_COMPOSER_TOOLBAR,
...(saved.composerToolbarButtons ?? {}),
},
};
} catch { } catch {
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
return defaultSettings; return defaultSettings;