From d6fc5ac6e3a67fd9d168228ff3ba5c3bd38ebdeb Mon Sep 17 00:00:00 2001 From: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:22:18 -0800 Subject: [PATCH] Ability to click megaphone in narrow sidebar to expand sidebar --- _locales/en/messages.json | 4 ++++ ts/components/CallsTab.preload.tsx | 6 ++---- ts/components/DebugLogWindow.dom.tsx | 2 ++ ts/components/LeftPane.dom.stories.tsx | 1 + ts/components/LeftPane.dom.tsx | 7 ++++--- ts/components/NavSidebar.dom.tsx | 19 +++++++++++++----- ts/components/RemoteMegaphone.dom.stories.tsx | 1 + ts/components/RemoteMegaphone.dom.tsx | 20 +++++++++++++++---- ts/components/ToastManager.dom.tsx | 3 +++ ts/state/smart/CallsTab.preload.tsx | 11 ++-------- ts/state/smart/InstallScreen.preload.tsx | 2 ++ ts/state/smart/LeftPane.preload.tsx | 18 ++++++++--------- ts/state/smart/Preferences.preload.tsx | 10 ++-------- ts/state/smart/StoriesTab.preload.tsx | 11 ++-------- ts/state/smart/ToastManager.preload.tsx | 16 +++++++++++++++ 15 files changed, 79 insertions(+), 52 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 3c0601f268..9b2b40c730 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -10154,6 +10154,10 @@ "messageformat": "Thanks for your feedback!", "description": "Toast message shown when call quality survey submission succeeds" }, + "icu:Megaphone__ExpandNarrowSidebar": { + "messageformat": "Expand sidebar to interact with megaphone", + "description": "Aria label for collapsed megaphone in the narrow sidebar state. Indicates that you can click it to expand the sidebar and the megaphone." + }, "icu:WhatsNew__bugfixes": { "messageformat": "This version contains a number of small tweaks and bug fixes to keep Signal running smoothly.", "description": "Release notes for releases that only include bug fixes", diff --git a/ts/components/CallsTab.preload.tsx b/ts/components/CallsTab.preload.tsx index e9224b1a90..3413177ba2 100644 --- a/ts/components/CallsTab.preload.tsx +++ b/ts/components/CallsTab.preload.tsx @@ -19,12 +19,12 @@ import type { } from '../state/ducks/calling.preload.js'; import { ConfirmationDialog } from './ConfirmationDialog.dom.js'; import type { UnreadStats } from '../util/countUnreadStats.std.js'; -import type { WidthBreakpoint } from './_util.std.js'; import type { CallLinkType } from '../types/CallLink.std.js'; import type { CallStateType } from '../state/selectors/calling.std.js'; import type { StartCallData } from './ConfirmLeaveCallModal.dom.js'; import { I18n } from './I18n.dom.js'; import { AxoDropdownMenu } from '../axo/AxoDropdownMenu.dom.js'; +import type { SmartPropsType as SmartToastManagerPropsType } from '../state/smart/ToastManager.preload.js'; enum CallsTabSidebarView { CallsListView, @@ -70,9 +70,7 @@ type CallsTabProps = Readonly<{ conversationId: string, callHistoryGroup: CallHistoryGroup | null ) => React.JSX.Element; - renderToastManager: (_: { - containerWidthBreakpoint: WidthBreakpoint; - }) => React.JSX.Element; + renderToastManager: (_: SmartToastManagerPropsType) => React.JSX.Element; regionCode: string | undefined; savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void; startCallLinkLobbyByRoomId: (options: { roomId: string }) => void; diff --git a/ts/components/DebugLogWindow.dom.tsx b/ts/components/DebugLogWindow.dom.tsx index d1aa43b61a..22dc7eb7d4 100644 --- a/ts/components/DebugLogWindow.dom.tsx +++ b/ts/components/DebugLogWindow.dom.tsx @@ -164,6 +164,7 @@ export function DebugLogWindow({ setDidResumeDonation={shouldNeverBeCalled} toast={toast} containerWidthBreakpoint={null} + expandNarrowLeftPane={shouldNeverBeCalled} isInFullScreenCall={false} /> @@ -230,6 +231,7 @@ export function DebugLogWindow({ setDidResumeDonation={shouldNeverBeCalled} toast={toast} containerWidthBreakpoint={null} + expandNarrowLeftPane={shouldNeverBeCalled} isInFullScreenCall={false} /> diff --git a/ts/components/LeftPane.dom.stories.tsx b/ts/components/LeftPane.dom.stories.tsx index ec25fd3cf3..40ca8507a2 100644 --- a/ts/components/LeftPane.dom.stories.tsx +++ b/ts/components/LeftPane.dom.stories.tsx @@ -318,6 +318,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => { toast={undefined} megaphone={undefined} containerWidthBreakpoint={containerWidthBreakpoint} + expandNarrowLeftPane={action('expandNarrowLeftPane')} isInFullScreenCall={false} /> ), diff --git a/ts/components/LeftPane.dom.tsx b/ts/components/LeftPane.dom.tsx index 9ba4f6f51f..11d02618cd 100644 --- a/ts/components/LeftPane.dom.tsx +++ b/ts/components/LeftPane.dom.tsx @@ -36,6 +36,7 @@ import * as KeyboardLayout from '../services/keyboardLayout.dom.js'; import type { LookupConversationWithoutServiceIdActionsType } from '../util/lookupConversationWithoutServiceId.preload.js'; import type { ShowConversationType } from '../state/ducks/conversations.preload.js'; import type { PropsType as UnsupportedOSDialogPropsType } from '../state/smart/UnsupportedOSDialog.preload.js'; +import type { SmartPropsType as SmartToastManagerPropsType } from '../state/smart/ToastManager.preload.js'; import { ConversationList } from './ConversationList.dom.js'; import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox.dom.js'; @@ -209,9 +210,9 @@ export type PropsType = { ) => React.JSX.Element; renderLeftPaneChatFolders: () => React.JSX.Element; renderNotificationProfilesMenu: () => React.JSX.Element; - renderToastManager: (_: { - containerWidthBreakpoint: WidthBreakpoint; - }) => React.JSX.Element; + renderToastManager: ( + _: Readonly + ) => React.JSX.Element; } & LookupConversationWithoutServiceIdActionsType; export function LeftPane({ diff --git a/ts/components/NavSidebar.dom.tsx b/ts/components/NavSidebar.dom.tsx index 114da51b59..42e3b9829e 100644 --- a/ts/components/NavSidebar.dom.tsx +++ b/ts/components/NavSidebar.dom.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ButtonHTMLAttributes, ReactNode } from 'react'; -import React, { createContext, useEffect, useState } from 'react'; +import React, { createContext, useCallback, useEffect, useState } from 'react'; import classNames from 'classnames'; import { useMove } from 'react-aria'; import { NavTabsToggle } from './NavTabs.dom.js'; @@ -15,6 +15,7 @@ import { } from '../util/leftPaneWidth.std.js'; import { WidthBreakpoint, getNavSidebarWidthBreakpoint } from './_util.std.js'; import type { UnreadStats } from '../util/countUnreadStats.std.js'; +import type { SmartPropsType as SmartToastManagerPropsType } from '../state/smart/ToastManager.preload.js'; export const NavSidebarWidthBreakpointContext = createContext(null); @@ -60,9 +61,7 @@ export type NavSidebarProps = Readonly<{ savePreferredLeftPaneWidth: (width: number) => void; title: string; otherTabsUnreadStats: UnreadStats; - renderToastManager: (_: { - containerWidthBreakpoint: WidthBreakpoint; - }) => React.JSX.Element; + renderToastManager: (_: SmartToastManagerPropsType) => React.JSX.Element; }>; enum DragState { @@ -103,6 +102,13 @@ export function NavSidebar({ const widthBreakpoint = getNavSidebarWidthBreakpoint(width); + const expandNarrowLeftPane = useCallback(() => { + if (preferredWidth < MIN_FULL_WIDTH) { + setPreferredWidth(MIN_FULL_WIDTH); + savePreferredLeftPaneWidth(MIN_FULL_WIDTH); + } + }, [preferredWidth, savePreferredLeftPaneWidth]); + // `useMove` gives us keyboard and mouse dragging support. const { moveProps } = useMove({ onMoveStart() { @@ -229,7 +235,10 @@ export function NavSidebar({ {...moveProps} /> - {renderToastManager({ containerWidthBreakpoint: widthBreakpoint })} + {renderToastManager({ + containerWidthBreakpoint: widthBreakpoint, + expandNarrowLeftPane, + })} ); diff --git a/ts/components/RemoteMegaphone.dom.stories.tsx b/ts/components/RemoteMegaphone.dom.stories.tsx index 4abb3ead64..ef089572b9 100644 --- a/ts/components/RemoteMegaphone.dom.stories.tsx +++ b/ts/components/RemoteMegaphone.dom.stories.tsx @@ -28,6 +28,7 @@ export default { body: 'Signal is powered by people like you. Show your support today!', imagePath: '/fixtures/donate-heart.png', isFullSize: true, + onClickNarrowMegaphone: action('onClickNarrowMegaphone'), onInteractWithMegaphone: action('onInteractWithMegaphone'), }, } satisfies ComponentMeta; diff --git a/ts/components/RemoteMegaphone.dom.tsx b/ts/components/RemoteMegaphone.dom.tsx index bd4e227660..a85a8bba3a 100644 --- a/ts/components/RemoteMegaphone.dom.tsx +++ b/ts/components/RemoteMegaphone.dom.tsx @@ -12,6 +12,7 @@ import { offsetDistanceModifier } from '../util/popperUtil.std.js'; export type PropsType = Omit & { isFullSize: boolean; i18n: LocalizerType; + onClickNarrowMegaphone: () => void; }; export function RemoteMegaphone({ @@ -25,6 +26,7 @@ export function RemoteMegaphone({ secondaryCtaText, remoteMegaphoneId, isFullSize, + onClickNarrowMegaphone, onInteractWithMegaphone, }: PropsType): React.JSX.Element { const isRTL = i18n.getLocaleDirection() === 'rtl'; @@ -37,7 +39,12 @@ export function RemoteMegaphone({ 'bg-elevated-background-primary dark:bg-elevated-background-tertiary' ); const image: React.JSX.Element = ( -
+

{title}

@@ -112,11 +118,17 @@ export function RemoteMegaphone({ -
{image}
+
); } diff --git a/ts/components/ToastManager.dom.tsx b/ts/components/ToastManager.dom.tsx index eabaa229ae..ecda977837 100644 --- a/ts/components/ToastManager.dom.tsx +++ b/ts/components/ToastManager.dom.tsx @@ -27,6 +27,7 @@ import { RemoteMegaphone } from './RemoteMegaphone.dom.js'; export type PropsType = { changeLocation: (newLocation: Location) => unknown; + expandNarrowLeftPane: () => void; hideToast: () => unknown; i18n: LocalizerType; openFileInFolder: (target: string) => unknown; @@ -948,6 +949,7 @@ export function renderMegaphone({ i18n, megaphone, containerWidthBreakpoint, + expandNarrowLeftPane, }: PropsType): React.JSX.Element | null { if (!megaphone) { return null; @@ -963,6 +965,7 @@ export function renderMegaphone({ {...megaphone} i18n={i18n} isFullSize={containerWidthBreakpoint !== WidthBreakpoint.Narrow} + onClickNarrowMegaphone={expandNarrowLeftPane} /> ); } diff --git a/ts/state/smart/CallsTab.preload.tsx b/ts/state/smart/CallsTab.preload.tsx index f1fc81a080..f91612f623 100644 --- a/ts/state/smart/CallsTab.preload.tsx +++ b/ts/state/smart/CallsTab.preload.tsx @@ -9,7 +9,6 @@ import { getPreferredLeftPaneWidth, } from '../selectors/items.dom.js'; import { getIntl, getRegionCode } from '../selectors/user.std.js'; -import type { WidthBreakpoint } from '../../components/_util.std.js'; import { CallsTab } from '../../components/CallsTab.preload.js'; import { getAllConversations, @@ -24,7 +23,7 @@ import type { } from '../../types/CallDisposition.std.js'; import type { ConversationType } from '../ducks/conversations.preload.js'; import { SmartConversationDetails } from './ConversationDetails.preload.js'; -import { SmartToastManager } from './ToastManager.preload.js'; +import { renderToastManagerWithoutMegaphone } from './ToastManager.preload.js'; import { useCallingActions } from '../ducks/calling.preload.js'; import { getActiveCallState, @@ -131,12 +130,6 @@ function renderConversationDetails( ); } -function renderToastManager(props: { - containerWidthBreakpoint: WidthBreakpoint; -}): React.JSX.Element { - return ; -} - export const SmartCallsTab = memo(function SmartCallsTab() { const i18n = useSelector(getIntl); const navTabsCollapsed = useSelector(getNavTabsCollapsed); @@ -253,7 +246,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() { preferredLeftPaneWidth={preferredLeftPaneWidth} renderCallLinkDetails={renderCallLinkDetails} renderConversationDetails={renderConversationDetails} - renderToastManager={renderToastManager} + renderToastManager={renderToastManagerWithoutMegaphone} regionCode={regionCode} savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId} diff --git a/ts/state/smart/InstallScreen.preload.tsx b/ts/state/smart/InstallScreen.preload.tsx index 3b58eb8d01..60dc270d49 100644 --- a/ts/state/smart/InstallScreen.preload.tsx +++ b/ts/state/smart/InstallScreen.preload.tsx @@ -20,6 +20,7 @@ import OS from '../../util/os/osMain.node.js'; import { isStagingServer } from '../../util/isStagingServer.dom.js'; import { createLogger } from '../../logging/log.std.js'; import { SmartToastManager } from './ToastManager.preload.js'; +import { shouldNeverBeCalled } from '../../util/shouldNeverBeCalled.std.js'; const log = createLogger('InstallScreen'); @@ -103,6 +104,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() { diff --git a/ts/state/smart/LeftPane.preload.tsx b/ts/state/smart/LeftPane.preload.tsx index 31218cbbdd..95be03c96c 100644 --- a/ts/state/smart/LeftPane.preload.tsx +++ b/ts/state/smart/LeftPane.preload.tsx @@ -103,7 +103,11 @@ import { SmartCrashReportDialog } from './CrashReportDialog.preload.js'; import { SmartMessageSearchResult } from './MessageSearchResult.preload.js'; import { SmartNetworkStatus } from './NetworkStatus.preload.js'; import { SmartRelinkDialog } from './RelinkDialog.dom.js'; -import { SmartToastManager } from './ToastManager.preload.js'; +import { + renderToastManagerWithoutMegaphone, + SmartToastManager, +} from './ToastManager.preload.js'; +import type { SmartPropsType as SmartToastManagerPropsType } from './ToastManager.preload.js'; import type { PropsType as SmartUnsupportedOSDialogPropsType } from './UnsupportedOSDialog.preload.js'; import { SmartUnsupportedOSDialog } from './UnsupportedOSDialog.preload.js'; import { SmartUpdateDialog } from './UpdateDialog.preload.js'; @@ -172,18 +176,12 @@ function renderUnsupportedOSDialog( ): React.JSX.Element { return ; } -function renderToastManagerWithMegaphone(props: { - containerWidthBreakpoint: WidthBreakpoint; -}): React.JSX.Element { +function renderToastManagerWithMegaphone( + props: Readonly +): React.JSX.Element { return ; } -function renderToastManagerWithoutMegaphone(props: { - containerWidthBreakpoint: WidthBreakpoint; -}): React.JSX.Element { - return ; -} - function renderNotificationProfilesMenu(): React.JSX.Element { return ; } diff --git a/ts/state/smart/Preferences.preload.tsx b/ts/state/smart/Preferences.preload.tsx index 6d30ee02da..bebe658e8f 100644 --- a/ts/state/smart/Preferences.preload.tsx +++ b/ts/state/smart/Preferences.preload.tsx @@ -74,7 +74,7 @@ import { getPreferredBadgeSelector } from '../selectors/badges.preload.js'; import { SmartProfileEditor } from './ProfileEditor.preload.js'; import { useNavActions } from '../ducks/nav.std.js'; import { NavTab } from '../../types/Nav.std.js'; -import { SmartToastManager } from './ToastManager.preload.js'; +import { renderToastManagerWithoutMegaphone } from './ToastManager.preload.js'; import { useToastActions } from '../ducks/toast.preload.js'; import { DataReader, DataWriter } from '../../sql/Client.preload.js'; import { deleteAllMyStories } from '../../util/deleteAllMyStories.preload.js'; @@ -157,12 +157,6 @@ function renderProfileEditor(options: { return ; } -function renderToastManager(props: { - containerWidthBreakpoint: WidthBreakpoint; -}): React.JSX.Element { - return ; -} - function renderDonationsPane({ contentsRef, settingsLocation, @@ -937,7 +931,7 @@ export function SmartPreferences(): React.JSX.Element | null { renderNotificationProfilesCreateFlow } renderProfileEditor={renderProfileEditor} - renderToastManager={renderToastManager} + renderToastManager={renderToastManagerWithoutMegaphone} renderUpdateDialog={renderUpdateDialog} renderPreferencesChatFoldersPage={renderPreferencesChatFoldersPage} renderPreferencesEditChatFolderPage={ diff --git a/ts/state/smart/StoriesTab.preload.tsx b/ts/state/smart/StoriesTab.preload.tsx index be265672ab..3909b8c8eb 100644 --- a/ts/state/smart/StoriesTab.preload.tsx +++ b/ts/state/smart/StoriesTab.preload.tsx @@ -4,8 +4,7 @@ import React, { memo, useCallback, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { SmartStoryCreator } from './StoryCreator.preload.js'; -import { SmartToastManager } from './ToastManager.preload.js'; -import type { WidthBreakpoint } from '../../components/_util.std.js'; +import { renderToastManagerWithoutMegaphone } from './ToastManager.preload.js'; import { StoriesTab } from '../../components/StoriesTab.dom.js'; import { getMaximumOutgoingAttachmentSizeInKb } from '../../types/AttachmentSize.std.js'; import type { ConfigKeyType } from '../../RemoteConfig.dom.js'; @@ -40,12 +39,6 @@ function renderStoryCreator(): React.JSX.Element { return ; } -function renderToastManager(props: { - containerWidthBreakpoint: WidthBreakpoint; -}): React.JSX.Element { - return ; -} - export const SmartStoriesTab = memo(function SmartStoriesTab() { const storiesActions = useStoriesActions(); const { @@ -133,7 +126,7 @@ export const SmartStoriesTab = memo(function SmartStoriesTab() { preferredLeftPaneWidth={preferredLeftPaneWidth} preferredWidthFromStorage={preferredWidthFromStorage} renderStoryCreator={renderStoryCreator} - renderToastManager={renderToastManager} + renderToastManager={renderToastManagerWithoutMegaphone} retryMessageSend={retryMessageSend} savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} showConversation={showConversation} diff --git a/ts/state/smart/ToastManager.preload.tsx b/ts/state/smart/ToastManager.preload.tsx index bd09fc5635..adace9f3f2 100644 --- a/ts/state/smart/ToastManager.preload.tsx +++ b/ts/state/smart/ToastManager.preload.tsx @@ -35,19 +35,34 @@ import { useDonationsActions } from '../ducks/donations.preload.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; import { getVisibleMegaphonesForDisplay } from '../selectors/megaphones.preload.js'; import { useMegaphonesActions } from '../ducks/megaphones.preload.js'; +import { shouldNeverBeCalled } from '../../util/shouldNeverBeCalled.std.js'; export type SmartPropsType = Readonly<{ disableMegaphone?: boolean; containerWidthBreakpoint: WidthBreakpoint; + expandNarrowLeftPane: () => void; }>; function handleShowDebugLog() { window.IPC.showDebugLog(); } +export function renderToastManagerWithoutMegaphone(props: { + containerWidthBreakpoint: WidthBreakpoint; +}): React.JSX.Element { + return ( + + ); +} + export const SmartToastManager = memo(function SmartToastManager({ disableMegaphone = false, containerWidthBreakpoint, + expandNarrowLeftPane, }: SmartPropsType) { const i18n = useSelector(getIntl); const hasCompletedUsernameOnboarding = useSelector( @@ -119,6 +134,7 @@ export const SmartToastManager = memo(function SmartToastManager({ setDidResumeDonation={setDidResume} centerToast={centerToast} containerWidthBreakpoint={containerWidthBreakpoint} + expandNarrowLeftPane={expandNarrowLeftPane} isCompositionAreaVisible={isCompositionAreaVisible} isInFullScreenCall={isInFullScreenCall} />