diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index bb357e5ef1..1e9119a4f7 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -339,6 +339,14 @@
"messageformat": "Archived Chats",
"description": "Shown in place of the search box when showing archived conversation list"
},
+ "icu:LeftPane__MoreActionsMenu__AddChatFolder": {
+ "messageformat": "Add chat folder",
+ "description": "Chats Tab > Chats List > Header > More Actions > Menu Item > Add chat folder (when you don't have any chat folders)"
+ },
+ "icu:LeftPane__MoreActionsMenu__FolderSettings": {
+ "messageformat": "Folder settings",
+ "description": "Chats Tab > Chats List > Header > More Actions > Menu Item > Folder settings (when you have chat folders)"
+ },
"icu:LeftPane--pinned": {
"messageformat": "Pinned",
"description": "Shown as a header for pinned conversations in the left pane"
@@ -391,6 +399,14 @@
"messageformat": "Enter a username followed by a dot and its set of numbers.",
"description": "Description displayed under search input in left pane when looking up someone by username"
},
+ "icu:LeftPane__EmptyView--WithSelectedChatFolder": {
+ "messageformat": "No chats to display",
+ "description": "Left Pane > Inbox > Chat List > Empty View (when you have a custom chat folder selected)"
+ },
+ "icu:LeftPane__EmptyView--WithSelectedChatFolder__FolderSettings": {
+ "messageformat": "Folder Settings",
+ "description": "Left Pane > Inbox > Chat List > Empty View (when you have a custom chat folder selected) > Folder Settings Button"
+ },
"icu:LeftPaneChatFolders__ItemLabel--All--Short": {
"messageformat": "All",
"description": "Left Pane > Inbox > Chat List > Chat Folders > Item Label > All chats (needs to fit in very small space)"
@@ -872,6 +888,10 @@
"messageformat": "Error saving receipt. Please try again.",
"description": "Toast message shown when a donation receipt fails to save to disk"
},
+ "icu:Toast--ChatFolderCreated": {
+ "messageformat": "{chatFolderName} folder added",
+ "description": "Toast message show when a chat folder is created (in some places like creating a preset)"
+ },
"icu:cannotSelectPhotosAndVideosAlongWithFiles": {
"messageformat": "You can't select photos and videos along with files.",
"description": "An error popup when the user has attempted to add an attachment"
@@ -1802,10 +1822,26 @@
"messageformat": "Add a chat folder",
"description": "Preferences > Chats Page > Chat Folders Section > Add Chat Folder Item > Title"
},
+ "icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Title--WithChatFolders": {
+ "messageformat": "Add or edit folders",
+ "description": "Preferences > Chats Page > Chat Folders Section > Add Chat Folder Item > Title (with chat folders)"
+ },
"icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Description": {
"messageformat": "Organize your chats into folders and quickly switch between them on your chat list.",
"description": "Preferences > Chats Page > Chat Folders Section > Add Chat Folder Item > Description"
},
+ "icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Description--WithChatFolders": {
+ "messageformat": "{chatFoldersCount, plural, one {# folder} other {# folders}}",
+ "description": "Preferences > Chats Page > Chat Folders Section > Add Chat Folder Item > Description (with chat folders)"
+ },
+ "icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Button": {
+ "messageformat": "Set up",
+ "description": "Preferences > Chats Page > Chat Folders Section > Add Chat Folder Item > Button"
+ },
+ "icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Button--WithChatFolders": {
+ "messageformat": "Manage",
+ "description": "Preferences > Chats Page > Chat Folders Section > Add Chat Folder Item > Button (with chat folders)"
+ },
"icu:Preferences__ChatsPage__ChatFoldersSection__ChatFolderItem__ContextMenu__EditFolder": {
"messageformat": "Edit folder",
"description": "Preferences > Chats Page > Chat Folders Section > Chat Folder Item > Context Menu > Edit Folder"
@@ -1850,6 +1886,10 @@
"messageformat": "Create a folder",
"description": "Preferences > Chat Folders Page > Folders > Create A Folder Button"
},
+ "icu:Preferences__ChatFoldersPage__FoldersSection__DragHandle__Label": {
+ "messageformat": "Drag to move chat folder",
+ "description": "Preferences > Chat Folders Page > Folders > Item > Drag Handle > Accessibility Label"
+ },
"icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__Title": {
"messageformat": "Suggested folders",
"description": "Preferences > Chat Folders Page > Suggested Folders > Title"
@@ -1858,10 +1898,6 @@
"messageformat": "Add",
"description": "Preferences > Chat Folders Page > Suggested Folders > Add Button"
},
- "icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__AddButton__Toast": {
- "messageformat": "{chatFolderTitle} folder added",
- "description": "Preferences > Chat Folders Page > Suggested Folders > Add Button > Toast"
- },
"icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__UnreadFolder__Title": {
"messageformat": "Unread",
"description": "Preferences > Chat Folders Page > Suggested Folders > Unread Folder > Title"
diff --git a/stylesheets/components/CallsTab.scss b/stylesheets/components/CallsTab.scss
index 95384d6eb6..fdaface7d7 100644
--- a/stylesheets/components/CallsTab.scss
+++ b/stylesheets/components/CallsTab.scss
@@ -118,21 +118,6 @@
user-select: none;
}
-.CallsTab__ClearCallHistoryIcon {
- @include mixins.light-theme {
- @include mixins.color-svg(
- '../images/icons/v3/trash/trash-compact.svg',
- variables.$color-gray-90
- );
- }
- @include mixins.dark-theme {
- @include mixins.color-svg(
- '../images/icons/v3/trash/trash-compact.svg',
- variables.$color-white
- );
- }
-}
-
.CallsList__Header {
display: flex;
gap: 0px;
diff --git a/stylesheets/components/Preferences.scss b/stylesheets/components/Preferences.scss
index 399ff8a829..625c26e029 100644
--- a/stylesheets/components/Preferences.scss
+++ b/stylesheets/components/Preferences.scss
@@ -1357,29 +1357,47 @@ $secondary-text-color: light-dark(
margin: 0;
}
-.Preferences__ChatFolders__ChatSelection__Item--Button {
- @include mixins.button-reset();
- &:hover {
- background: light-dark(variables.$color-gray-02, variables.$color-gray-80);
- }
- @include mixins.keyboard-mode {
- &:focus {
- outline: 2px solid variables.$color-ultramarine;
- }
+.Preferences__ChatFolders__ChatSelection__Item {
+ &[data-dragging='true'] {
+ opacity: 0%;
+ // delay making the item transparent until the browser has a chance to take
+ // a snapshot of the item before for the drag preview
+ transition: opacity 0ms linear;
+ transition-delay: 1ms;
}
}
-.Preferences__ChatFolders__ChatSelection__Item {
+.Preferences__ChatFolders__ChatSelection__Item--Button {
+ @include mixins.button-reset();
+ & {
+ width: 100%;
+ }
+}
+
+.Preferences__ChatFolders__ChatSelection__Item--Button,
+.Preferences__ChatFolders__ChatSelection__Item--ListItem {
+ outline: 0;
+}
+
+.Preferences__ChatFolders__ChatSelection__Item--Button,
+.Preferences__ChatFolders__ChatSelection__Item--Clickable {
+ &:hover .Preferences__ChatFolders__ChatSelection__ItemContent {
+ background: light-dark(variables.$color-gray-02, variables.$color-gray-80);
+ }
+}
+
+.Preferences__ChatFolders__ChatSelection__ItemContent {
display: flex;
- width: 100%;
align-items: center;
gap: 12px;
padding-block: 8px;
padding-inline: 24px;
border-radius: 1px;
- &[data-dragging='true'] {
- opacity: 50%;
+ @include mixins.keyboard-mode {
+ .Preferences__ChatFolders__ChatSelection__Item:focus & {
+ outline: 2px solid variables.$color-ultramarine;
+ }
}
}
diff --git a/stylesheets/tailwind-config.css b/stylesheets/tailwind-config.css
index a5e423a28a..061f88791a 100644
--- a/stylesheets/tailwind-config.css
+++ b/stylesheets/tailwind-config.css
@@ -54,7 +54,6 @@
--color-background-primary: light-dark(/* */ #FFFFFF /* */, /* */ #1A1A1A /* */);
--color-background-secondary: light-dark(/* */ #F6F6F6 /* */, /* */ #262626 /* */);
--color-background-overlay: light-dark(--alpha(#000000 / 20%), --alpha(#000000 / 40%));
- --color-background-overlay-secondary: light-dark(--alpha(#000000 / 15%), --alpha(#FFFFFF / 15%));
/* Colors/Elevated Background */
--color-elevated-background-primary: light-dark(#FAFAFA, #2A2A2A);
@@ -146,7 +145,6 @@
--color-background-primary: light-dark(/* */ #FFFFFF /* */, /* */ #121212 /* */);
--color-background-secondary: light-dark(/* */ #F6F6F6 /* */, /* */ #1E1E1E /* */);
--color-background-overlay: light-dark(--alpha(#000000 / 40%), --alpha(#000000 / 60%));
- --color-background-overlay-secondary: light-dark(--alpha(#000000 / 15%), --alpha(#FFFFFF / 15%));
/* Colors/Elevated Background */
--color-elevated-background-primary: light-dark(#FFFFFF, #222222);
@@ -312,8 +310,6 @@
@theme {
/* box-shadow */
--shadow-*: initial; /* reset defaults */
- --shadow-legacy-outline:
- 0 0 0 2px #2c6bed;
--shadow-elevation-0:
0 1px 2px 0 var(--color-shadow-elevation-1);
--shadow-elevation-1:
diff --git a/ts/axo/AxoDropdownMenu.stories.tsx b/ts/axo/AxoDropdownMenu.stories.tsx
index 6df3a7e4a4..f1156b8240 100644
--- a/ts/axo/AxoDropdownMenu.stories.tsx
+++ b/ts/axo/AxoDropdownMenu.stories.tsx
@@ -1,5 +1,6 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
+import type { ReactNode } from 'react';
import React, { useState } from 'react';
import type { Meta } from '@storybook/react';
import { action } from '@storybook/addon-actions';
@@ -11,12 +12,20 @@ export default {
title: 'Axo/AxoDropdownMenu',
} satisfies Meta;
+function Container(props: { children: ReactNode }) {
+ return (
+
+ {props.children}
+
+ );
+}
+
export function Basic(): JSX.Element {
const [showBookmarks, setShowBookmarks] = useState(true);
const [showFullUrls, setShowFullUrls] = useState(false);
const [selectedPerson, setSelectedPerson] = useState('jamie');
return (
-
+
@@ -96,6 +105,135 @@ export function Basic(): JSX.Element {
-
+
+ );
+}
+
+export function WithHeader(): JSX.Element {
+ return (
+
+
+
+
+ Open Dropdown Menu
+
+
+
+
+
+ Sleep
+
+
+ Work
+
+
+ Sub-menu
+
+
+
+ Sleep
+
+
+ Work
+
+
+
+
+
+
+ );
+}
+
+const LONG_TEXT =
+ 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Cum nostrum, inventore quia tenetur sunt non ab fuga explicabo ullam tempore.';
+
+export function StressTestLongText(): JSX.Element {
+ const items = (
+ <>
+
+ Item: {LONG_TEXT}
+
+
+ Item: {LONG_TEXT}
+
+ >
+ );
+
+ const checkboxItems = (
+ <>
+
+ CheckboxItem: {LONG_TEXT}
+
+
+ CheckboxItem: {LONG_TEXT}
+
+ >
+ );
+ const content = (
+ <>
+
+ {items}
+ {checkboxItems}
+
+
+ RadioItem: {LONG_TEXT}
+
+
+ RadioItem: {LONG_TEXT}
+
+
+
+
+
+ Label: {LONG_TEXT}
+
+ {items}
+ {checkboxItems}
+
+ >
+ );
+
+ return (
+
+
+
+
+ Open Dropdown Menu
+
+
+
+ {content}
+
+ {LONG_TEXT}
+ {content}
+
+
+
+
);
}
diff --git a/ts/axo/AxoDropdownMenu.tsx b/ts/axo/AxoDropdownMenu.tsx
index 1839654c40..ff7a708c25 100644
--- a/ts/axo/AxoDropdownMenu.tsx
+++ b/ts/axo/AxoDropdownMenu.tsx
@@ -1,11 +1,16 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
-import React, { memo } from 'react';
+import React, { memo, useId } from 'react';
import { DropdownMenu } from 'radix-ui';
-import type { FC } from 'react';
+import type { FC, ReactNode } from 'react';
import { AxoSymbol } from './AxoSymbol.js';
import { AxoBaseMenu } from './_internal/AxoBaseMenu.js';
import { tw } from './tw.js';
+import {
+ AriaLabellingProvider,
+ useAriaLabellingContext,
+ useCreateAriaLabellingContext,
+} from './_internal/AriaLabellingContext.js';
const Namespace = 'AxoDropdownMenu';
@@ -57,17 +62,21 @@ export namespace AxoDropdownMenu {
* ---------------------------------
*/
- export type RootProps = AxoBaseMenu.MenuRootProps & {
- open?: boolean;
- defaultOpen?: boolean;
- onOpenChange?: (open: boolean) => void;
- };
+ export type RootProps = AxoBaseMenu.MenuRootProps &
+ Readonly<{
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ }>;
/**
* Contains all the parts of a dropdown menu.
*/
export const Root: FC = memo(props => {
- return {props.children};
+ return (
+
+ {props.children}
+
+ );
});
Root.displayName = `${Namespace}.Root`;
@@ -104,30 +113,73 @@ export namespace AxoDropdownMenu {
* Uses a portal to render the content part into the `body`.
*/
export const Content: FC = memo(props => {
+ const { context, labelId, descriptionId } = useCreateAriaLabellingContext();
return (
-
-
- {props.children}
-
-
+
+
+
+ {props.children}
+
+
+
);
});
Content.displayName = `${Namespace}.Content`;
+ /**
+ * Component:
+ * -------------------------------------
+ */
+
+ export type CustomItemProps = Pick<
+ AxoBaseMenu.MenuItemProps,
+ 'disabled' | 'textValue' | 'keyboardShortcut' | 'onSelect'
+ > &
+ Readonly<{
+ leading?: ReactNode;
+ // trailing?: ReactNode;
+ text: ReactNode;
+ // prefix?: ReactNode;
+ suffix?: ReactNode;
+ }>;
+
+ export const CustomItem: FC = memo(props => {
+ return (
+
+ {props.leading && (
+
+ {props.leading}
+
+ )}
+
+ {props.text}
+ {props.suffix}
+
+
+ );
+ });
+
+ CustomItem.displayName = `${Namespace}.CustomItem`;
+
/**
* Component:
* ---------------------------------
*/
- export type ItemProps = AxoBaseMenu.MenuItemProps & {
- customIcon?: React.ReactNode;
- };
+ export type ItemProps = AxoBaseMenu.MenuItemProps;
/**
* The component that contains the dropdown menu items.
@@ -151,11 +203,6 @@ export namespace AxoDropdownMenu {
)}
- {props.customIcon && (
-
- {props.customIcon}
-
- )}
{props.children}
{props.keyboardShortcut && (
@@ -212,6 +259,49 @@ export namespace AxoDropdownMenu {
Label.displayName = `${Namespace}.Label`;
+ /**
+ * Component:
+ * -----------------------------------
+ */
+
+ export type HeaderProps = Readonly<{
+ label: ReactNode;
+ description?: ReactNode;
+ }>;
+
+ export const Header: FC = memo(props => {
+ const labelId = useId();
+ const descriptionId = useId();
+
+ const { labelRef, descriptionRef } = useAriaLabellingContext(
+ `<${Namespace}.Header>`,
+ `<${Namespace}.Content/SubContent>`
+ );
+
+ return (
+
+
+ {props.label}
+
+ {props.description && (
+
+ {props.description}
+
+ )}
+
+ );
+ });
+
+ Header.displayName = `${Namespace}.Header`;
+
/**
* Component:
* -----------------------------------------
@@ -343,6 +433,20 @@ export namespace AxoDropdownMenu {
Separator.displayName = `${Namespace}.Separator`;
+ /**
+ * Component:
+ */
+
+ export const ContentSeparator: FC = memo(() => {
+ return (
+
+ );
+ });
+
+ ContentSeparator.displayName = `${Namespace}.ContentSeparator`;
+
/**
* Component:
* -------------------------------
@@ -402,14 +506,19 @@ export namespace AxoDropdownMenu {
* inside {@link AxoDropdownMenu.Sub}.
*/
export const SubContent: FC = memo(props => {
+ const { context, labelId, descriptionId } = useCreateAriaLabellingContext();
return (
-
- {props.children}
-
+
+
+ {props.children}
+
+
);
});
diff --git a/ts/axo/_internal/AriaLabellingContext.tsx b/ts/axo/_internal/AriaLabellingContext.tsx
new file mode 100644
index 0000000000..3c7f0a6f55
--- /dev/null
+++ b/ts/axo/_internal/AriaLabellingContext.tsx
@@ -0,0 +1,52 @@
+// Copyright 2025 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import type { RefCallback } from 'react';
+import { createContext, useContext, useMemo, useState } from 'react';
+import { assert } from './assert.js';
+
+type AriaLabellingContextType = Readonly<{
+ labelRef: RefCallback;
+ descriptionRef: RefCallback;
+}>;
+
+const AriaLabellingContext = createContext(
+ null
+);
+
+export type CreateAriaLabellingContextResult = Readonly<{
+ context: AriaLabellingContextType;
+ labelId: string | undefined;
+ descriptionId: string | undefined;
+}>;
+
+export function useCreateAriaLabellingContext(): CreateAriaLabellingContextResult {
+ const [labelId, setLabelId] = useState();
+ const [descriptionId, setDescriptionId] = useState();
+
+ const context = useMemo((): AriaLabellingContextType => {
+ function labelRef(element: HTMLElement | null) {
+ setLabelId(element?.id);
+ }
+
+ function descriptionRef(element: HTMLElement | null) {
+ setDescriptionId(element?.id);
+ }
+
+ return { labelRef, descriptionRef };
+ }, []);
+
+ return { context, labelId, descriptionId };
+}
+
+export const AriaLabellingProvider = AriaLabellingContext.Provider;
+
+export function useAriaLabellingContext(
+ componentName: string,
+ providerName: string
+): AriaLabellingContextType {
+ return assert(
+ useContext(AriaLabellingContext),
+ `${componentName} must be wrapped with a ${providerName}`
+ );
+}
diff --git a/ts/axo/_internal/AxoBaseMenu.tsx b/ts/axo/_internal/AxoBaseMenu.tsx
index 90e4d2675f..00d806e0c0 100644
--- a/ts/axo/_internal/AxoBaseMenu.tsx
+++ b/ts/axo/_internal/AxoBaseMenu.tsx
@@ -17,7 +17,7 @@ export namespace AxoBaseMenu {
'forced-colors:text-[CanvasText]'
);
- const baseContentGridStyles = tw('grid grid-cols-[min-content_auto] p-1.5');
+ const baseContentGridStyles = tw('grid grid-cols-[min-content_1fr] p-1.5');
//
const baseGroupStyles = tw('col-span-full grid grid-cols-subgrid');
@@ -252,6 +252,18 @@ export namespace AxoBaseMenu {
export const menuLabelStyles = tw(baseLabelStyles);
export const selectLabelStyles = tw(baseLabelStyles);
+ /**
+ * AxoBaseMenu: Header
+ */
+
+ export const menuHeaderStyles = tw('col-span-full col-start-1 p-1.5');
+ export const menuHeaderLabelStyles = tw(
+ 'block truncate type-title-small text-label-primary'
+ );
+ export const menuHeaderDescriptionStyles = tw(
+ 'block truncate type-caption text-label-secondary'
+ );
+
/**
* AxoBaseMenu: CheckboxItem
* -------------------------
@@ -315,13 +327,17 @@ export namespace AxoBaseMenu {
// N/A
}>;
- const baseSeparatorStyles = tw(
- baseItemStyles,
- 'mx-0.5 my-1 border-t-[0.5px] border-border-primary'
- );
+ const baseSeparatorStyles = tw('my-1 border-t-[0.5px] border-border-primary');
- export const menuSeparatorStyles = tw(baseSeparatorStyles);
- export const selectSeperatorStyles = tw(baseSeparatorStyles);
+ export const menuSeparatorStyles = tw(
+ 'col-span-full col-start-1 mx-0.5',
+ baseSeparatorStyles
+ );
+ export const menuContentSeparatorStyles = tw(
+ 'col-span-full col-start-2',
+ baseSeparatorStyles
+ );
+ export const selectSeperatorStyles = tw(baseItemStyles, baseSeparatorStyles);
/**
* AxoBaseMenu: Sub
diff --git a/ts/components/CallsTab.tsx b/ts/components/CallsTab.tsx
index 4727d99a34..f9b985d8d3 100644
--- a/ts/components/CallsTab.tsx
+++ b/ts/components/CallsTab.tsx
@@ -17,7 +17,6 @@ import type {
ActiveCallStateType,
PeekNotConnectedGroupCallType,
} from '../state/ducks/calling.js';
-import { ContextMenu } from './ContextMenu.js';
import { ConfirmationDialog } from './ConfirmationDialog.js';
import type { UnreadStats } from '../util/countUnreadStats.js';
import type { WidthBreakpoint } from './_util.js';
@@ -25,6 +24,7 @@ import type { CallLinkType } from '../types/CallLink.js';
import type { CallStateType } from '../state/selectors/calling.js';
import type { StartCallData } from './ConfirmLeaveCallModal.js';
import { I18n } from './I18n.js';
+import { AxoDropdownMenu } from '../axo/AxoDropdownMenu.js';
enum CallsTabSidebarView {
CallsListView,
@@ -235,33 +235,22 @@ export function CallsTab({
updateSidebarView(CallsTabSidebarView.NewCallView);
}}
/>
-
- {({ onClick, onKeyDown, ref }) => {
- return (
- }
- label={i18n('icu:CallsTab__MoreActionsLabel')}
- />
- );
- }}
-
+
+
+ }
+ label={i18n('icu:CallsTab__MoreActionsLabel')}
+ />
+
+
+
+ {i18n('icu:CallsTab__ClearCallHistoryLabel')}
+
+
+
>
)}
>
diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx
index a0b3c30bb3..e48875b0b7 100644
--- a/ts/components/LeftPane.stories.tsx
+++ b/ts/components/LeftPane.stories.tsx
@@ -36,6 +36,7 @@ import type { GroupListItemConversationType } from './conversationList/GroupList
import { ServerAlert } from '../types/ServerAlert.js';
import { LeftPaneChatFolders } from './leftPane/LeftPaneChatFolders.js';
import { LeftPaneConversationListItemContextMenu } from './leftPane/LeftPaneConversationListItemContextMenu.js';
+import { CurrentChatFolders } from '../types/CurrentChatFolders.js';
const { i18n } = window.SignalContext;
@@ -119,6 +120,7 @@ const defaultModeSpecificProps = {
conversations: defaultConversations,
archivedConversations: defaultArchivedConversations,
isAboutToSearch: false,
+ selectedChatFolder: null,
};
const emptySearchResultsGroup = { isLoading: false, results: [] };
@@ -195,6 +197,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
challengeStatus: 'idle',
crashReportCount: 0,
+ hasAnyCurrentCustomChatFolders: false,
hasNetworkDialog: false,
hasExpiredDialog: false,
hasRelinkDialog: false,
@@ -214,6 +217,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
preloadConversation: action('preloadConversation'),
showConversation: action('showConversation'),
blockConversation: action('blockConversation'),
+ onChatFoldersOpenSettings: action('onChatFoldersOpenSettings'),
onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation'
),
@@ -249,8 +253,8 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
{...props}
/>
),
- renderNotificationProfilesMenu: ({ trigger }) => (
- {trigger}
+ renderNotificationProfilesMenu: () => (
+
),
renderRelinkDialog: props => (
{
{
{props.children}
),
+ selectedChatFolder: null,
selectedConversationId: undefined,
targetedMessageId: undefined,
openUsernameReservationModal: action('openUsernameReservationModal'),
@@ -400,6 +405,7 @@ export function InboxNoConversations(): JSX.Element {
conversations: [],
archivedConversations: [],
isAboutToSearch: false,
+ selectedChatFolder: null,
},
})}
/>
@@ -439,6 +445,7 @@ export function InboxBackupMediaDownloadWithDialogsAndUnpinnedConversations(): J
conversations: defaultConversations,
archivedConversations: [],
isAboutToSearch: false,
+ selectedChatFolder: null,
},
})}
/>
@@ -496,6 +503,7 @@ export function InboxUsernameCorrupted(): JSX.Element {
conversations: [],
archivedConversations: [],
isAboutToSearch: false,
+ selectedChatFolder: null,
},
usernameCorrupted: true,
})}
@@ -514,6 +522,7 @@ export function InboxUsernameLinkCorrupted(): JSX.Element {
conversations: [],
archivedConversations: [],
isAboutToSearch: false,
+ selectedChatFolder: null,
},
usernameLinkCorrupted: true,
})}
@@ -532,6 +541,7 @@ export function InboxOnlyPinnedConversations(): JSX.Element {
conversations: [],
archivedConversations: [],
isAboutToSearch: false,
+ selectedChatFolder: null,
},
})}
/>
@@ -549,6 +559,7 @@ export function InboxOnlyNonPinnedConversations(): JSX.Element {
conversations: defaultConversations,
archivedConversations: [],
isAboutToSearch: false,
+ selectedChatFolder: null,
},
})}
/>
@@ -566,6 +577,7 @@ export function InboxOnlyArchivedConversations(): JSX.Element {
conversations: [],
archivedConversations: defaultArchivedConversations,
isAboutToSearch: false,
+ selectedChatFolder: null,
},
})}
/>
@@ -583,6 +595,7 @@ export function InboxPinnedAndArchivedConversations(): JSX.Element {
conversations: [],
archivedConversations: defaultArchivedConversations,
isAboutToSearch: false,
+ selectedChatFolder: null,
},
})}
/>
@@ -600,6 +613,7 @@ export function InboxNonPinnedAndArchivedConversations(): JSX.Element {
conversations: defaultConversations,
archivedConversations: defaultArchivedConversations,
isAboutToSearch: false,
+ selectedChatFolder: null,
},
})}
/>
@@ -617,6 +631,7 @@ export function InboxPinnedAndNonPinnedConversations(): JSX.Element {
conversations: defaultConversations,
archivedConversations: [],
isAboutToSearch: false,
+ selectedChatFolder: null,
},
})}
/>
@@ -634,6 +649,7 @@ export function InboxPinnedAndNonPinnedConversationsWithBackupDownload(): JSX.El
conversations: defaultConversations,
archivedConversations: [],
isAboutToSearch: false,
+ selectedChatFolder: null,
},
backupMediaDownloadProgress,
})}
@@ -1080,6 +1096,7 @@ export function CaptchaDialogRequired(): JSX.Element {
archivedConversations: [],
isAboutToSearch: false,
searchTerm: '',
+ selectedChatFolder: null,
},
challengeStatus: 'required',
})}
@@ -1099,6 +1116,7 @@ export function CaptchaDialogPending(): JSX.Element {
archivedConversations: [],
isAboutToSearch: false,
searchTerm: '',
+ selectedChatFolder: null,
},
challengeStatus: 'pending',
})}
@@ -1118,6 +1136,7 @@ export function _CrashReportDialog(): JSX.Element {
archivedConversations: [],
isAboutToSearch: false,
searchTerm: '',
+ selectedChatFolder: null,
},
crashReportCount: 42,
})}
@@ -1270,6 +1289,7 @@ export function SearchingConversation(): JSX.Element {
isAboutToSearch: false,
searchConversation: getDefaultConversation(),
searchTerm: '',
+ selectedChatFolder: null,
},
})}
/>
diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx
index f79657f2e6..2366250e15 100644
--- a/ts/components/LeftPane.tsx
+++ b/ts/components/LeftPane.tsx
@@ -5,7 +5,7 @@ import React, { useEffect, useCallback, useMemo, useRef } from 'react';
import classNames from 'classnames';
import lodash from 'lodash';
-import type { LeftPaneHelper, ToFindType } from './leftPane/LeftPaneHelper.js';
+import type { ToFindType } from './leftPane/LeftPaneHelper.js';
import { FindDirection } from './leftPane/LeftPaneHelper.js';
import type { LeftPaneInboxPropsType } from './leftPane/LeftPaneInboxHelper.js';
import { LeftPaneInboxHelper } from './leftPane/LeftPaneInboxHelper.js';
@@ -53,7 +53,6 @@ import {
NavSidebarActionButton,
NavSidebarSearchHeader,
} from './NavSidebar.js';
-import { ContextMenu } from './ContextMenu.js';
import type { UnreadStats } from '../util/countUnreadStats.js';
import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress.js';
import type { ServerAlertsType, ServerAlert } from '../types/ServerAlert.js';
@@ -61,7 +60,9 @@ import { getServerAlertDialog } from './ServerAlerts.js';
import { NavTab, SettingsPage, ProfileEditorPage } from '../types/Nav.js';
import type { Location } from '../types/Nav.js';
import type { RenderConversationListItemContextMenuProps } from './conversationList/BaseConversationListItem.js';
-import type { ExternalProps as NotificationProfilesMenuProps } from '../state/smart/NotificationProfilesMenu.js';
+import { AxoDropdownMenu } from '../axo/AxoDropdownMenu.js';
+import type { ChatFolder } from '../types/ChatFolder.js';
+import { isChatFoldersEnabled } from '../types/ChatFolder.js';
import { ProfileAvatar } from './PreferencesNotificationProfiles.js';
import { tw } from '../axo/tw.js';
@@ -77,6 +78,7 @@ export type PropsType = {
downloadBannerDismissed: boolean;
};
otherTabsUnreadStats: UnreadStats;
+ hasAnyCurrentCustomChatFolders: boolean;
hasExpiredDialog: boolean;
hasFailedStorySends: boolean;
hasNetworkDialog: boolean;
@@ -123,6 +125,7 @@ export type PropsType = {
isMacOS: boolean;
isNotificationProfileActive: boolean;
preferredWidthFromStorage: number;
+ selectedChatFolder: ChatFolder | null;
selectedConversationId: undefined | string;
targetedMessageId: undefined | string;
challengeStatus: 'idle' | 'required' | 'pending';
@@ -150,6 +153,7 @@ export type PropsType = {
endSearch: () => void;
navTabsCollapsed: boolean;
openUsernameReservationModal: () => void;
+ onChatFoldersOpenSettings: () => void;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
removeConversation: (conversationId: string) => void;
@@ -199,9 +203,7 @@ export type PropsType = {
renderCrashReportDialog: () => JSX.Element;
renderExpiredBuildDialog: (_: DialogExpiredBuildPropsType) => JSX.Element;
renderLeftPaneChatFolders: () => JSX.Element;
- renderNotificationProfilesMenu: (
- props: NotificationProfilesMenuProps
- ) => JSX.Element;
+ renderNotificationProfilesMenu: () => JSX.Element;
renderToastManager: (_: {
containerWidthBreakpoint: WidthBreakpoint;
}) => JSX.Element;
@@ -228,6 +230,7 @@ export function LeftPane({
endSearch,
getPreferredBadge,
getServerAlertToShow,
+ hasAnyCurrentCustomChatFolders,
hasExpiredDialog,
hasFailedStorySends,
hasNetworkDialog,
@@ -242,6 +245,7 @@ export function LeftPane({
isUpdateDownloaded,
modeSpecificProps,
navTabsCollapsed,
+ onChatFoldersOpenSettings,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
@@ -266,6 +270,7 @@ export function LeftPane({
saveAlerts,
savePreferredLeftPaneWidth,
searchInConversation,
+ selectedChatFolder,
selectedConversationId,
targetedMessageId,
toggleNavTabsCollapse,
@@ -320,7 +325,15 @@ export function LeftPane({
//
// Unfortunately, there's a little bit of repetition here because TypeScript isn't quite
// smart enough.
- let helper: LeftPaneHelper;
+ let helper:
+ | LeftPaneInboxHelper
+ | LeftPaneSearchHelper
+ | LeftPaneArchiveHelper
+ | LeftPaneComposeHelper
+ | LeftPaneFindByUsernameHelper
+ | LeftPaneFindByPhoneNumberHelper
+ | LeftPaneChooseGroupMembersHelper
+ | LeftPaneSetGroupMetadataHelper;
let shouldRecomputeRowHeights: boolean;
switch (modeSpecificProps.mode) {
case LeftPaneMode.Inbox: {
@@ -523,9 +536,7 @@ export function LeftPane({
startSearch,
]);
- const backgroundNode = helper.getBackgroundNode({
- i18n,
- });
+ const isEmpty = helper.getRowCount() === 0;
const preRowsNode = helper.getPreRowsNode({
clearConversationSearch,
@@ -744,21 +755,6 @@ export function LeftPane({
const hasDialogs = dialogs.length ? !hideHeader : false;
- // The notification profile menu shows in two places - under its own icon and
- // under the more actions context menu.
- const [isNotificationProfilesMenuOpen, setIsNotificationProfilesMenuOpen] =
- React.useState(false);
- const [
- isNotificationProfilesSubMenuOpen,
- setIsNotificationProfilesSubMenuOpen,
- ] = React.useState(false);
-
- React.useEffect(() => {
- if (!isNotificationProfileActive) {
- setIsNotificationProfilesMenuOpen(false);
- }
- }, [isNotificationProfileActive, setIsNotificationProfilesMenuOpen]);
-
return (
- {isNotificationProfileActive &&
- renderNotificationProfilesMenu({
- isOpen: isNotificationProfilesMenuOpen,
- onClose: () => {
- setIsNotificationProfilesMenuOpen(false);
- },
- trigger: (
+ {isNotificationProfileActive && (
+
+
- ),
- })}
+
+
+ {renderNotificationProfilesMenu()}
+
+
+ )}
}
onClick={startComposing}
/>
- setIsNotificationProfilesSubMenuOpen(true),
- },
- ]}
- popperOptions={{
- placement: 'bottom',
- strategy: 'absolute',
- }}
- portalToRoot
- >
- {({ onClick, onKeyDown, ref }) =>
- renderNotificationProfilesMenu({
- isOpen: isNotificationProfilesSubMenuOpen,
- onClose: () => {
- setIsNotificationProfilesSubMenuOpen(false);
- },
- trigger: (
-
- }
- label="More Actions"
- />
- ),
- })
- }
-
+
+
+ }
+ label="More Actions"
+ />
+
+
+
+ {i18n('icu:avatarMenuViewArchive')}
+
+ {isChatFoldersEnabled() && !hasAnyCurrentCustomChatFolders && (
+
+ {i18n('icu:LeftPane__MoreActionsMenu__AddChatFolder')}
+
+ )}
+ {isChatFoldersEnabled() && hasAnyCurrentCustomChatFolders && (
+
+ {i18n('icu:LeftPane__MoreActionsMenu__FolderSettings')}
+
+ )}
+
+
+ {i18n('icu:NotificationProfileMenuItem')}
+
+
+ {renderNotificationProfilesMenu()}
+
+
+
+
>
}
>
- {backgroundNode}