From 891f2daf991b737a163eae72e1b274652393364f Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 10 Jun 2026 13:20:29 -0400 Subject: [PATCH] feat(P3-6): configurable composer toolbar buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/features/room/RoomInput.tsx | 269 ++++++++++-------- src/app/features/settings/general/General.tsx | 93 ++++++ src/app/state/settings.ts | 36 ++- 3 files changed, 275 insertions(+), 123 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index d838ede22..124ff6ebb 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -209,6 +209,15 @@ export const RoomInput = forwardRef( const imagePackRooms: Room[] = useImagePackRooms(roomId, roomToParents); 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 [locationError, setLocationError] = React.useState(null); const handleShareLocation = () => { @@ -933,88 +942,96 @@ export const RoomInput = forwardRef( } after={ <> - setToolbar(!toolbar)} - > - - - - {(emojiBoardTab: EmojiBoardTab | undefined, setEmojiBoardTab) => ( - - { - setEmojiBoardTab((t) => { - if (t) { - if (!mobileOrTablet()) ReactEditor.focus(editor); - return undefined; - } - return t; - }); - }} - /> - - } - > - {!hideStickerBtn && ( - setEmojiBoardTab(EmojiBoardTab.Sticker)} - variant="SurfaceVariant" - size="300" - radii="300" - > - - - )} - setToolbar(!toolbar)} + > + + + )} + {(showEmoji || showSticker) && ( + + {(emojiBoardTab: EmojiBoardTab | undefined, setEmojiBoardTab) => ( + + { + setEmojiBoardTab((t) => { + if (t) { + if (!mobileOrTablet()) ReactEditor.focus(editor); + return undefined; + } + return t; + }); + }} + /> + } - onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)} - variant="SurfaceVariant" - size="300" - radii="300" > - - - - )} - - {!!gifApiKey && ( + {showSticker && !hideStickerBtn && ( + setEmojiBoardTab(EmojiBoardTab.Sticker)} + variant="SurfaceVariant" + size="300" + radii="300" + > + + + )} + {showEmoji && ( + setEmojiBoardTab(EmojiBoardTab.Emoji)} + variant="SurfaceVariant" + size="300" + radii="300" + > + + + )} + + )} + + )} + {!!gifApiKey && showGif && ( {(gifOpen: boolean, setGifOpen) => ( ( {locationError} )} - - {locating ? ( - - ) : ( - - )} - - setPollOpen(true)} - aria-label="Create poll" - variant="SurfaceVariant" - size="300" - radii="300" - title="Create poll" - > - - - { - setLocationError(err); - setTimeout(() => setLocationError(null), 4000); - }} - /> + {showLocation && ( + + {locating ? ( + + ) : ( + + )} + + )} + {showPoll && ( + setPollOpen(true)} + aria-label="Create poll" + variant="SurfaceVariant" + size="300" + radii="300" + title="Create poll" + > + + + )} + {showVoice && ( + { + setLocationError(err); + setTimeout(() => setLocationError(null), 4000); + }} + /> + )} {charCount > 0 && ( ( {charCount} )} - - - + {showSchedule && ( + + + + )} { + setComposerToolbarButtons({ ...composerToolbarButtons, [key]: !composerToolbarButtons[key] }); + }; return ( @@ -881,6 +890,90 @@ function Editor() { after={} /> + Composer Toolbar Buttons + + toggleToolbarButton('showFormat')} + /> + } + /> + toggleToolbarButton('showEmoji')} + /> + } + /> + toggleToolbarButton('showSticker')} + /> + } + /> + toggleToolbarButton('showGif')} + /> + } + /> + toggleToolbarButton('showLocation')} + /> + } + /> + toggleToolbarButton('showPoll')} + /> + } + /> + toggleToolbarButton('showVoice')} + /> + } + /> + toggleToolbarButton('showSchedule')} + /> + } + /> + ); } diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 852283b15..2b37ef992 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -38,6 +38,28 @@ export enum MessageLayout { 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 { themeId?: string; useSystemTheme: boolean; @@ -101,6 +123,8 @@ export interface Settings { warnOnUnverifiedDevices: boolean; pauseAnimations: boolean; + + composerToolbarButtons: ComposerToolbarSettings; } const defaultSettings: Settings = { @@ -166,13 +190,23 @@ const defaultSettings: Settings = { warnOnUnverifiedDevices: false, pauseAnimations: false, + + composerToolbarButtons: DEFAULT_COMPOSER_TOOLBAR, }; export const getSettings = (): Settings => { try { const settings = localStorage.getItem(STORAGE_KEY); if (settings === null) return defaultSettings; - return { ...defaultSettings, ...(JSON.parse(settings) as Settings) }; + const saved = JSON.parse(settings) as Partial; + return { + ...defaultSettings, + ...saved, + composerToolbarButtons: { + ...DEFAULT_COMPOSER_TOOLBAR, + ...(saved.composerToolbarButtons ?? {}), + }, + }; } catch { localStorage.removeItem(STORAGE_KEY); return defaultSettings;