Init Chat Folders Settings UI
This commit is contained in:
parent
791ccda7aa
commit
157496f822
14 changed files with 1829 additions and 13 deletions
|
@ -1714,6 +1714,222 @@
|
|||
"messageformat": "Emoji skin tone",
|
||||
"description": "Preferences Window > Chats Tab > Emoji skin tone default setting > Label"
|
||||
},
|
||||
"icu:Preferences__ChatsPage__ChatFoldersSection__Title": {
|
||||
"messageformat": "Chat folders",
|
||||
"description": "Preferences > Chats Page > Chat Folders Section > Title"
|
||||
},
|
||||
"icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Title": {
|
||||
"messageformat": "Add a chat folder",
|
||||
"description": "Preferences > Chats Page > Chat Folders Section > Add Chat Folder Item > Title"
|
||||
},
|
||||
"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__ChatFolderItem__ContextMenu__EditFolder": {
|
||||
"messageformat": "Edit folder",
|
||||
"description": "Preferences > Chats Page > Chat Folders Section > Chat Folder Item > Context Menu > Edit Folder"
|
||||
},
|
||||
"icu:Preferences__ChatsPage__ChatFoldersSection__ChatFolderItem__ContextMenu__DeleteFolder": {
|
||||
"messageformat": "Delete",
|
||||
"description": "Preferences > Chats Page > Chat Folders Section > Chat Folder Item > Context Menu > Delete Folder"
|
||||
},
|
||||
"icu:Preferences__ChatsPage__DeleteChatFolderDialog__Title": {
|
||||
"messageformat": "Delete folder?",
|
||||
"description": "Preferences > Chats Page > Delete Chat Folder Dialog > Title"
|
||||
},
|
||||
"icu:Preferences__ChatsPage__DeleteChatFolderDialog__Description": {
|
||||
"messageformat": "Do you want to delete the folder “{chatFolderTitle}”?",
|
||||
"description": "Preferences > Chats Page > Delete Chat Folder Dialog > Description"
|
||||
},
|
||||
"icu:Preferences__ChatsPage__DeleteChatFolderDialog__DeleteButton": {
|
||||
"messageformat": "Delete",
|
||||
"description": "Preferences > Chats Page > Delete Chat Folder Dialog > Delete Button"
|
||||
},
|
||||
"icu:Preferences__ChatsPage__DeleteChatFolderDialog__CancelButton": {
|
||||
"messageformat": "Cancel",
|
||||
"description": "Preferences > Chats Page > Delete Chat Folder Dialog > Cancel Button"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__Title": {
|
||||
"messageformat": "Chat folders",
|
||||
"description": "Preferences > Chat Folders Page > Title"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__Description": {
|
||||
"messageformat": "Organize your chats into folders and quickly switch between them on your chat list",
|
||||
"description": "Preferences > Chat Folders Page > Description"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__FoldersSection__Title": {
|
||||
"messageformat": "Folders",
|
||||
"description": "Preferences > Chat Folders Page > Folders > Title"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__FoldersSection__AllChatsFolder__Title": {
|
||||
"messageformat": "All chats",
|
||||
"description": "Preferences > Chat Folders Page > Folders > All Chats Folder (Default folder, always present)"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__FoldersSection__CreateAFolderButton": {
|
||||
"messageformat": "Create a folder",
|
||||
"description": "Preferences > Chat Folders Page > Folders > Create A Folder Button"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__Title": {
|
||||
"messageformat": "Suggested folders",
|
||||
"description": "Preferences > Chat Folders Page > Suggested Folders > Title"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__AddButton": {
|
||||
"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"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__UnreadFolder__Description": {
|
||||
"messageformat": "Unread messages from all chats",
|
||||
"description": "Preferences > Chat Folders Page > Suggested Folders > Unread Folder > Description"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__DirectChatsFolder__Title": {
|
||||
"messageformat": "1:1 chats",
|
||||
"description": "Preferences > Chat Folders Page > Suggested Folders > Direct chats Folder > Title"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__DirectChatsFolder__Description": {
|
||||
"messageformat": "Only messages from direct chats",
|
||||
"description": "Preferences > Chat Folders Page > Suggested Folders > Direct chats Folder > Description"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__GroupChatsFolder__Title": {
|
||||
"messageformat": "Groups",
|
||||
"description": "Preferences > Chat Folders Page > Suggested Folders > Group chats Folder > Title"
|
||||
},
|
||||
"icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__GroupChatsFolder__Description": {
|
||||
"messageformat": "Only messages from group chats",
|
||||
"description": "Preferences > Chat Folders Page > Suggested Folders > Group chats Folder > Description"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__Title": {
|
||||
"messageformat": "Create a folder",
|
||||
"description": "Preferences > Edit Chat Folder Page > Title"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__FolderNameField__Label": {
|
||||
"messageformat": "Folder name",
|
||||
"description": "Preferences > Edit Chat Folder Page > Folder Name Field > Label"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__FolderNameField__Placeholder": {
|
||||
"messageformat": "Folder name (required)",
|
||||
"description": "Preferences > Edit Chat Folder Page > Folder Name Field > Placeholder"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__IncludedChatsSection__Title": {
|
||||
"messageformat": "Included chats",
|
||||
"description": "Preferences > Edit Chat Folder Page > Included Chats Section > Title"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__IncludedChatsSection__Help": {
|
||||
"messageformat": "Choose chats that you want to appear in this folder",
|
||||
"description": "Preferences > Edit Chat Folder Page > Included Chats Section > Help"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__IncludedChatsSection__AddChatsButton": {
|
||||
"messageformat": "Add chats",
|
||||
"description": "Preferences > Edit Chat Folder Page > Included Chats Section > Add Chats Button"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SelectChatsDialog--IncludedChats__Title": {
|
||||
"messageformat": "Included chats",
|
||||
"description": "Preferences > Edit Chat Folder Page > Select Chats Dialog (Included Chats Mode) > Title"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SelectChatsDialog--ExcludedChats__Title": {
|
||||
"messageformat": "Excluded chats",
|
||||
"description": "Preferences > Edit Chat Folder Page > Select Chats Dialog (Excluded Chats Mode) > Title"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SelectChatsDialog__Search__Placeholder": {
|
||||
"messageformat": "Search",
|
||||
"description": "Preferences > Edit Chat Folder Page > Select Chats Dialog > Search > Placeholder"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SelectChatsDialog__ChatTypesSection__Title": {
|
||||
"messageformat": "Chat types",
|
||||
"description": "Preferences > Edit Chat Folder Page > Select Chats Dialog > Chat Types Section > Title"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SelectChatsDialog__ChatTypesSection__DirectChats": {
|
||||
"messageformat": "1:1 chats",
|
||||
"description": "Preferences > Edit Chat Folder Page > Select Chats Dialog > Chat Types Section > Direct Chats"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SelectChatsDialog__ChatTypesSection__GroupChats": {
|
||||
"messageformat": "Groups",
|
||||
"description": "Preferences > Edit Chat Folder Page > Select Chats Dialog > Chat Types Section > Group Chats"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SelectChatsDialog__RecentChats__Title": {
|
||||
"messageformat": "Recent chats",
|
||||
"description": "Preferences > Edit Chat Folder Page > Select Chats Dialog > Recent Chats Section > Title"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SelectChatsDialog__DoneButton": {
|
||||
"messageformat": "Done",
|
||||
"description": "Preferences > Edit Chat Folder Page > Select Chats Dialog > Done Button"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__ExceptionsSection__Title": {
|
||||
"messageformat": "Exceptions",
|
||||
"description": "Preferences > Edit Chat Folder Page > Exceptions Section > Title"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__ExceptionsSection__Help": {
|
||||
"messageformat": "Choose chats that you do not want to appear in this folder",
|
||||
"description": "Preferences > Edit Chat Folder Page > Exceptions Section > Help"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__ExceptionsSection__ExcludeChatsButton": {
|
||||
"messageformat": "Exclude chats",
|
||||
"description": "Preferences > Edit Chat Folder Page > Exceptions Section > Exclude chats button"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__OnlyShowUnreadChatsCheckbox__Label": {
|
||||
"messageformat": "Only show unread chats",
|
||||
"description": "Preferences > Edit Chat Folder Page > Only Show Unread Chats Checkbox > Label"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__OnlyShowUnreadChatsCheckbox__Description": {
|
||||
"messageformat": "Only chats with unread messages will be shown in this folder.",
|
||||
"description": "Preferences > Edit Chat Folder Page > Only Show Unread Chats Checkbox > Description"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__IncludeMutedChatsCheckbox__Label": {
|
||||
"messageformat": "Include muted chats",
|
||||
"description": "Preferences > Edit Chat Folder Page > Include Muted Chats Checkbox > Label"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__DeleteFolderButton": {
|
||||
"messageformat": "Delete folder",
|
||||
"description": "Preferences > Edit Chat Folder Page > Delete Folder Button"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__Title": {
|
||||
"messageformat": "Delete this chat folder?",
|
||||
"description": "Preferences > Delete Chat Folder Dialog > Title"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__Description": {
|
||||
"messageformat": "This folder will be removed from your chat list.",
|
||||
"description": "Preferences > Delete Chat Folder Dialog > Description"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__DeleteButton": {
|
||||
"messageformat": "Delete",
|
||||
"description": "Preferences > Delete Chat Folder Dialog > Delete Button"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__CancelButton": {
|
||||
"messageformat": "Cancel",
|
||||
"description": "Preferences > Delete Chat Folder Dialog > Cancel Button"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__Title": {
|
||||
"messageformat": "Save changes?",
|
||||
"description": "Preferences > Edit Chat Folder Page > Save Changes Folder Dialog > Title"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__Description": {
|
||||
"messageformat": "Do you want to save the changes you’ve made to this chat folder?",
|
||||
"description": "Preferences > Edit Chat Folder Page > Save Changes Folder Dialog > Description"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__SaveButton": {
|
||||
"messageformat": "Save",
|
||||
"description": "Preferences > Edit Chat Folder Page > Save Changes Folder Dialog > Save Button"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__DiscardButton": {
|
||||
"messageformat": "Discard",
|
||||
"description": "Preferences > Edit Chat Folder Page > Save Changes Folder Dialog > Discard Button"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__SaveButton": {
|
||||
"messageformat": "Save",
|
||||
"description": "Preferences > Edit Chat Folder Page > Save Button"
|
||||
},
|
||||
"icu:Preferences__EditChatFolderPage__CancelButton": {
|
||||
"messageformat": "Cancel",
|
||||
"description": "Preferences > Edit Chat Folder Page > Cancel Button"
|
||||
},
|
||||
"icu:initialSync": {
|
||||
"messageformat": "Syncing contacts and groups",
|
||||
"description": "Shown during initial link while contacts and groups are being pulled from mobile device"
|
||||
|
|
|
@ -5098,7 +5098,8 @@ button.module-calling-participants-list__contact {
|
|||
$normal-row-height: 72px;
|
||||
|
||||
@include mixins.NavTabs__Scroller;
|
||||
padding-inline: 10px;
|
||||
padding-inline-start: 10px;
|
||||
padding-inline-end: 1px; /* leaving room for scrollbar */
|
||||
|
||||
// list tiles in choose-group-members and compose extend to the edge
|
||||
.module-left-pane--mode-choose-group-members &,
|
||||
|
@ -5797,6 +5798,48 @@ button.module-calling-participants-list__contact {
|
|||
}
|
||||
}
|
||||
|
||||
.module-conversation-list__empty-results {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-block: 48px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.module-conversation-list__generic-checkbox-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
background-color: light-dark(
|
||||
variables.$color-black-alpha-06,
|
||||
variables.$color-white-alpha-12
|
||||
);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&--contact::before {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/person/person.svg',
|
||||
light-dark(variables.$color-black, variables.$color-gray-05)
|
||||
);
|
||||
}
|
||||
|
||||
&--group::before {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/group/group.svg',
|
||||
light-dark(variables.$color-black, variables.$color-gray-05)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Module: Left Pane
|
||||
|
||||
.module-left-pane {
|
||||
|
|
|
@ -289,6 +289,16 @@ $secondary-text-color: light-dark(
|
|||
container-type: inline-size;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&__settings-pane {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
@ -1297,3 +1307,118 @@ $secondary-text-color: light-dark(
|
|||
user-select: text;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__List {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
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 {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-block: 8px;
|
||||
padding-inline: 24px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__ItemAvatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 9999px;
|
||||
background: light-dark(variables.$color-gray-05, variables.$color-gray-90);
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__ItemAvatar--Add::before {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/plus/plus.svg',
|
||||
light-dark(variables.$color-gray-75, variables.$color-gray-15)
|
||||
);
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__ItemAvatar--Folder::before {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/folder/folder.svg',
|
||||
light-dark(variables.$color-gray-75, variables.$color-gray-15)
|
||||
);
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__ItemAvatar--UnreadChats::before {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/chat/chat-badge.svg',
|
||||
light-dark(variables.$color-gray-75, variables.$color-gray-15)
|
||||
);
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__ItemAvatar--DirectChats::before {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/person/person.svg',
|
||||
light-dark(variables.$color-gray-75, variables.$color-gray-15)
|
||||
);
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__ItemAvatar--GroupChats::before {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/group/group.svg',
|
||||
light-dark(variables.$color-gray-75, variables.$color-gray-15)
|
||||
);
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__ItemBody {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__ItemTitle {
|
||||
@include mixins.font-body-1;
|
||||
color: light-dark(variables.$color-gray-90, variables.$color-gray-05);
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatSelection__ItemDescription {
|
||||
@include mixins.font-body-2;
|
||||
color: light-dark(variables.$color-gray-60, variables.$color-gray-25);
|
||||
}
|
||||
|
||||
.Preferences__ChatFolders__ChatList__DeleteButton {
|
||||
@include mixins.button-reset();
|
||||
& {
|
||||
color: variables.$color-accent-red;
|
||||
border-radius: 1px;
|
||||
}
|
||||
@include mixins.keyboard-mode {
|
||||
&:focus {
|
||||
outline: 2px solid variables.$color-ultramarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Preferences__EditChatFolderPage__SelectChatsDialog__width-container {
|
||||
// Override .module-modal-host__width-container
|
||||
&.module-modal-host__width-container {
|
||||
max-width: 360px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ export type ConfigKeyType =
|
|||
| 'desktop.calling.ringrtcAdmFull.3'
|
||||
| 'desktop.calling.ringrtcAdmInternal'
|
||||
| 'desktop.calling.ringrtcAdmPreStable'
|
||||
| 'desktop.chatFolders.alpha'
|
||||
| 'desktop.chatFolders.beta'
|
||||
| 'desktop.chatFolders.prod'
|
||||
| 'desktop.clientExpiration'
|
||||
| 'desktop.backup.credentialFetch'
|
||||
| 'desktop.donations'
|
||||
|
|
|
@ -37,6 +37,7 @@ import { UsernameSearchResultListItem } from './conversationList/UsernameSearchR
|
|||
import { GroupListItem } from './conversationList/GroupListItem';
|
||||
import { ListView } from './ListView';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { ListTile } from './ListTile';
|
||||
|
||||
export enum RowType {
|
||||
ArchiveButton = 'ArchiveButton',
|
||||
|
@ -46,6 +47,7 @@ export enum RowType {
|
|||
ContactCheckbox = 'ContactCheckbox',
|
||||
PhoneNumberCheckbox = 'PhoneNumberCheckbox',
|
||||
UsernameCheckbox = 'UsernameCheckbox',
|
||||
GenericCheckbox = 'GenericCheckbox',
|
||||
Conversation = 'Conversation',
|
||||
CreateNewGroup = 'CreateNewGroup',
|
||||
FindByUsername = 'FindByUsername',
|
||||
|
@ -58,6 +60,7 @@ export enum RowType {
|
|||
SelectSingleGroup = 'SelectSingleGroup',
|
||||
StartNewConversation = 'StartNewConversation',
|
||||
UsernameSearchResult = 'UsernameSearchResult',
|
||||
EmptyResults = 'EmptyResults',
|
||||
}
|
||||
|
||||
type ArchiveButtonRowType = {
|
||||
|
@ -100,6 +103,19 @@ type UsernameCheckboxRowType = {
|
|||
isFetching: boolean;
|
||||
};
|
||||
|
||||
export enum GenericCheckboxRowIcon {
|
||||
Contact = 'contact',
|
||||
Group = 'group',
|
||||
}
|
||||
|
||||
type GenericCheckboxRowType = {
|
||||
type: RowType.GenericCheckbox;
|
||||
icon: GenericCheckboxRowIcon;
|
||||
label: string;
|
||||
isChecked: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
type ConversationRowType = {
|
||||
type: RowType.Conversation;
|
||||
conversation: ConversationListItemPropsType;
|
||||
|
@ -160,6 +176,11 @@ type UsernameRowType = {
|
|||
isFetchingUsername: boolean;
|
||||
};
|
||||
|
||||
type EmptyResultsRowType = {
|
||||
type: RowType.EmptyResults;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type Row =
|
||||
| ArchiveButtonRowType
|
||||
| BlankRowType
|
||||
|
@ -168,6 +189,7 @@ export type Row =
|
|||
| ClearFilterButtonRowType
|
||||
| PhoneNumberCheckboxRowType
|
||||
| UsernameCheckboxRowType
|
||||
| GenericCheckboxRowType
|
||||
| ConversationRowType
|
||||
| CreateNewGroupRowType
|
||||
| FindByUsername
|
||||
|
@ -178,7 +200,8 @@ export type Row =
|
|||
| SearchResultsLoadingFakeRowType
|
||||
| StartNewConversationRowType
|
||||
| SelectSingleGroupRowType
|
||||
| UsernameRowType;
|
||||
| UsernameRowType
|
||||
| EmptyResultsRowType;
|
||||
|
||||
export type PropsType = {
|
||||
dimensions?: {
|
||||
|
@ -222,6 +245,7 @@ export type PropsType = {
|
|||
const NORMAL_ROW_HEIGHT = 76;
|
||||
const SELECT_ROW_HEIGHT = 52;
|
||||
const HEADER_ROW_HEIGHT = 40;
|
||||
const EMPTY_RESULTS_ROW_HEIGHT = 48 + 20 + 48;
|
||||
|
||||
export function ConversationList({
|
||||
dimensions,
|
||||
|
@ -260,19 +284,34 @@ export function ConversationList({
|
|||
assertDev(false, `Expected a row at index ${index}`);
|
||||
return NORMAL_ROW_HEIGHT;
|
||||
}
|
||||
switch (row.type) {
|
||||
const { type } = row;
|
||||
switch (type) {
|
||||
case RowType.Header:
|
||||
case RowType.SearchResultsLoadingFakeHeader:
|
||||
return HEADER_ROW_HEIGHT;
|
||||
case RowType.SelectSingleGroup:
|
||||
case RowType.ContactCheckbox:
|
||||
case RowType.GenericCheckbox:
|
||||
case RowType.Contact:
|
||||
case RowType.CreateNewGroup:
|
||||
case RowType.FindByUsername:
|
||||
case RowType.FindByPhoneNumber:
|
||||
return SELECT_ROW_HEIGHT;
|
||||
default:
|
||||
case RowType.ArchiveButton:
|
||||
case RowType.Blank:
|
||||
case RowType.ClearFilterButton:
|
||||
case RowType.PhoneNumberCheckbox:
|
||||
case RowType.UsernameCheckbox:
|
||||
case RowType.Conversation:
|
||||
case RowType.MessageSearchResult:
|
||||
case RowType.SearchResultsLoadingFakeRow:
|
||||
case RowType.StartNewConversation:
|
||||
case RowType.UsernameSearchResult:
|
||||
return NORMAL_ROW_HEIGHT;
|
||||
case RowType.EmptyResults:
|
||||
return EMPTY_RESULTS_ROW_HEIGHT;
|
||||
default:
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
},
|
||||
[getRow]
|
||||
|
@ -400,6 +439,21 @@ export function ConversationList({
|
|||
/>
|
||||
);
|
||||
break;
|
||||
case RowType.GenericCheckbox:
|
||||
result = (
|
||||
<ListTile.checkbox
|
||||
leading={
|
||||
<i
|
||||
className={`module-conversation-list__generic-checkbox-icon module-conversation-list__generic-checkbox-icon--${row.icon}`}
|
||||
/>
|
||||
}
|
||||
title={row.label}
|
||||
isChecked={row.isChecked}
|
||||
onClick={row.onClick}
|
||||
clickable
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case RowType.Conversation: {
|
||||
const itemProps = pick(row.conversation, [
|
||||
'avatarPlaceholderGradient',
|
||||
|
@ -539,6 +593,13 @@ export function ConversationList({
|
|||
/>
|
||||
);
|
||||
break;
|
||||
case RowType.EmptyResults:
|
||||
result = (
|
||||
<div className="module-conversation-list__empty-results">
|
||||
{row.message}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(row);
|
||||
}
|
||||
|
|
|
@ -52,10 +52,9 @@ export function ListView({
|
|||
|
||||
const style: React.CSSProperties = useMemo(() => {
|
||||
return {
|
||||
// See `<Timeline>` for an explanation of this `any` cast.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
overflowY: scrollable ? ('overlay' as any) : 'hidden',
|
||||
overflowY: scrollable ? 'auto' : 'hidden',
|
||||
direction: 'inherit',
|
||||
scrollbarGutter: 'stable',
|
||||
};
|
||||
}, [scrollable]);
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { Meta, StoryFn } from '@storybook/react';
|
|||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { shuffle } from 'lodash';
|
||||
import { Page, Preferences } from './Preferences';
|
||||
import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors';
|
||||
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
||||
|
@ -14,7 +15,10 @@ import { DAY, DurationInSeconds, WEEK } from '../util/durations';
|
|||
import { DialogUpdate } from './DialogUpdate';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import {
|
||||
getDefaultConversation,
|
||||
getDefaultGroup,
|
||||
} from '../test-both/helpers/getDefaultConversation';
|
||||
import { EditState, ProfileEditor } from './ProfileEditor';
|
||||
import {
|
||||
UsernameEditState,
|
||||
|
@ -25,6 +29,7 @@ import type { PropsType } from './Preferences';
|
|||
import type { WidthBreakpoint } from './_util';
|
||||
import type { MessageAttributesType } from '../model-types';
|
||||
import { PreferencesDonations } from './PreferencesDonations';
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
|
@ -34,6 +39,20 @@ const me = {
|
|||
username: 'someone.243',
|
||||
};
|
||||
|
||||
const conversations = shuffle([
|
||||
...Array.from(Array(20), getDefaultGroup),
|
||||
...Array.from(Array(20), getDefaultConversation),
|
||||
]);
|
||||
|
||||
function conversationSelector(conversationId?: string) {
|
||||
strictAssert(conversationId, 'Missing conversation id');
|
||||
const found = conversations.find(conversation => {
|
||||
return conversation.id === conversationId;
|
||||
});
|
||||
strictAssert(found, 'Missing conversation');
|
||||
return found;
|
||||
}
|
||||
|
||||
const availableMicrophones = [
|
||||
{
|
||||
name: 'DefAuLt (Headphones)',
|
||||
|
@ -164,6 +183,9 @@ export default {
|
|||
args: {
|
||||
i18n,
|
||||
|
||||
conversations,
|
||||
conversationSelector,
|
||||
|
||||
accountEntropyPool:
|
||||
'uy38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t',
|
||||
autoDownloadAttachment: {
|
||||
|
@ -273,6 +295,7 @@ export default {
|
|||
renderToastManager,
|
||||
renderUpdateDialog,
|
||||
getConversationsWithCustomColor: () => [],
|
||||
getPreferredBadge: () => undefined,
|
||||
|
||||
addCustomColor: action('addCustomColor'),
|
||||
doDeleteAllData: action('doDeleteAllData'),
|
||||
|
@ -379,6 +402,14 @@ export const Chats = Template.bind({});
|
|||
Chats.args = {
|
||||
page: Page.Chats,
|
||||
};
|
||||
export const ChatFolders = Template.bind({});
|
||||
ChatFolders.args = {
|
||||
page: Page.ChatFolders,
|
||||
};
|
||||
export const EditChatFolder = Template.bind({});
|
||||
EditChatFolder.args = {
|
||||
page: Page.EditChatFolder,
|
||||
};
|
||||
export const Calls = Template.bind({});
|
||||
Calls.args = {
|
||||
page: Page.Calls,
|
||||
|
|
|
@ -13,9 +13,7 @@ import React, {
|
|||
import { isNumber, noop, partition } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { ChatColorPicker } from './ChatColorPicker';
|
||||
import { Checkbox } from './Checkbox';
|
||||
|
@ -37,7 +35,7 @@ import { focusableSelector } from '../util/focusableSelectors';
|
|||
import { Modal } from './Modal';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { removeDiacritics } from '../util/removeDiacritics';
|
||||
import { assertDev } from '../util/assert';
|
||||
import { assertDev, strictAssert } from '../util/assert';
|
||||
import { I18n } from './I18n';
|
||||
import { FunSkinTonesList } from './fun/FunSkinTones';
|
||||
import { emojiParentKeyConstant, type EmojiSkinTone } from './fun/data/emojis';
|
||||
|
@ -89,11 +87,27 @@ import type {
|
|||
PromptOSAuthReasonType,
|
||||
PromptOSAuthResultType,
|
||||
} from '../util/os/promptOSAuthMain';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import { EditChatFoldersPage } from './preferences/EditChatFoldersPage';
|
||||
import { ChatFoldersPage } from './preferences/ChatFoldersPage';
|
||||
import type {
|
||||
ChatFolderId,
|
||||
ChatFolderParams,
|
||||
ChatFolderRecord,
|
||||
} from '../types/ChatFolder';
|
||||
import {
|
||||
CHAT_FOLDER_DEFAULTS,
|
||||
isChatFoldersEnabled,
|
||||
} from '../types/ChatFolder';
|
||||
import type { GetConversationByIdType } from '../state/selectors/conversations';
|
||||
|
||||
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
||||
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
||||
|
||||
export type PropsDataType = {
|
||||
conversations: ReadonlyArray<ConversationType>;
|
||||
conversationSelector: GetConversationByIdType;
|
||||
|
||||
// Settings
|
||||
accountEntropyPool: string | undefined;
|
||||
autoDownloadAttachment: AutoDownloadAttachmentType;
|
||||
|
@ -207,6 +221,7 @@ type PropsFunctionType = {
|
|||
version: number
|
||||
) => Promise<Array<MessageAttributesType>>;
|
||||
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
makeSyncRequest: () => unknown;
|
||||
onStartUpdate: () => unknown;
|
||||
pickLocalBackupFolder: () => Promise<string | undefined>;
|
||||
|
@ -296,6 +311,8 @@ export enum Page {
|
|||
|
||||
// Sub pages
|
||||
ChatColor = 'ChatColor',
|
||||
ChatFolders = 'ChatFolders',
|
||||
EditChatFolder = 'EditChatFolder',
|
||||
PNP = 'PNP',
|
||||
BackupsDetails = 'BackupsDetails',
|
||||
LocalBackups = 'LocalBackups',
|
||||
|
@ -333,6 +350,8 @@ const DEFAULT_ZOOM_FACTORS = [
|
|||
];
|
||||
|
||||
export function Preferences({
|
||||
conversations,
|
||||
conversationSelector,
|
||||
accountEntropyPool,
|
||||
addCustomColor,
|
||||
autoDownloadAttachment,
|
||||
|
@ -358,6 +377,7 @@ export function Preferences({
|
|||
getConversationsWithCustomColor,
|
||||
getMessageCountBySchemaVersion,
|
||||
getMessageSampleForSchemaVersion,
|
||||
getPreferredBadge,
|
||||
hasAudioNotifications,
|
||||
hasAutoConvertEmoji,
|
||||
hasAutoDownloadUpdate,
|
||||
|
@ -497,6 +517,54 @@ export function Preferences({
|
|||
const [confirmPnpNotDiscoverable, setConfirmPnpNoDiscoverable] =
|
||||
useState(false);
|
||||
|
||||
const [chatFolders, setChatFolders] = useState<
|
||||
ReadonlyArray<ChatFolderRecord>
|
||||
>([]);
|
||||
|
||||
const [editChatFolderPageId, setEditChatFolderPageId] =
|
||||
useState<ChatFolderId | null>(null);
|
||||
|
||||
const handleOpenEditChatFoldersPage = useCallback(
|
||||
(chatFolderId: ChatFolderId | null) => {
|
||||
setPage(Page.EditChatFolder);
|
||||
setEditChatFolderPageId(chatFolderId);
|
||||
},
|
||||
[setPage]
|
||||
);
|
||||
|
||||
const handleCloseEditChatFoldersPage = useCallback(() => {
|
||||
setPage(Page.ChatFolders);
|
||||
setEditChatFolderPageId(null);
|
||||
}, [setPage]);
|
||||
|
||||
const handleCreateChatFolder = useCallback((params: ChatFolderParams) => {
|
||||
setChatFolders(prev => {
|
||||
return [...prev, { ...params, id: String(prev.length) as ChatFolderId }];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleUpdateChatFolder = useCallback(
|
||||
(chatFolderId: ChatFolderId, chatFolderParams: ChatFolderParams) => {
|
||||
setChatFolders(prev => {
|
||||
return prev.map(chatFolder => {
|
||||
if (chatFolder.id === chatFolderId) {
|
||||
return { id: chatFolderId, ...chatFolderParams };
|
||||
}
|
||||
return chatFolder;
|
||||
});
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeleteChatFolder = useCallback((chatFolderId: ChatFolderId) => {
|
||||
setChatFolders(prev => {
|
||||
return prev.filter(chatFolder => {
|
||||
return chatFolder.id !== chatFolderId;
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
function closeLanguageDialog() {
|
||||
setLanguageDialog(null);
|
||||
setSelectedLanguageLocale(localeOverride);
|
||||
|
@ -1103,6 +1171,33 @@ export function Preferences({
|
|||
/>
|
||||
</SettingsRow>
|
||||
</SettingsRow>
|
||||
{isChatFoldersEnabled() && (
|
||||
<SettingsRow
|
||||
title={i18n(
|
||||
'icu:Preferences__ChatsPage__ChatFoldersSection__Title'
|
||||
)}
|
||||
>
|
||||
<Control
|
||||
left={
|
||||
<>
|
||||
<div>
|
||||
{i18n(
|
||||
'icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Title'
|
||||
)}
|
||||
</div>
|
||||
<div className="Preferences__description">
|
||||
{i18n(
|
||||
'icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Description'
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
right={null}
|
||||
onClick={() => setPage(Page.ChatFolders)}
|
||||
/>
|
||||
</SettingsRow>
|
||||
)}
|
||||
|
||||
{isSyncSupported && (
|
||||
<SettingsRow>
|
||||
<Control
|
||||
|
@ -1828,6 +1923,44 @@ export function Preferences({
|
|||
title={i18n('icu:ChatColorPicker__menu-title')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.ChatFolders) {
|
||||
content = (
|
||||
<ChatFoldersPage
|
||||
i18n={i18n}
|
||||
settingsPaneRef={settingsPaneRef}
|
||||
onBack={() => setPage(Page.Chats)}
|
||||
onOpenEditChatFoldersPage={handleOpenEditChatFoldersPage}
|
||||
chatFolders={chatFolders}
|
||||
onCreateChatFolder={handleCreateChatFolder}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.EditChatFolder) {
|
||||
let initChatFolderParam: ChatFolderParams;
|
||||
if (editChatFolderPageId != null) {
|
||||
const found = chatFolders.find(chatFolder => {
|
||||
return chatFolder.id === editChatFolderPageId;
|
||||
});
|
||||
strictAssert(found, 'Missing chat folder');
|
||||
initChatFolderParam = found;
|
||||
} else {
|
||||
initChatFolderParam = CHAT_FOLDER_DEFAULTS;
|
||||
}
|
||||
content = (
|
||||
<EditChatFoldersPage
|
||||
i18n={i18n}
|
||||
settingsPaneRef={settingsPaneRef}
|
||||
onBack={handleCloseEditChatFoldersPage}
|
||||
conversations={conversations}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
theme={theme}
|
||||
existingChatFolderId={editChatFolderPageId}
|
||||
initChatFolderParams={initChatFolderParam}
|
||||
conversationSelector={conversationSelector}
|
||||
onCreateChatFolder={handleCreateChatFolder}
|
||||
onUpdateChatFolder={handleUpdateChatFolder}
|
||||
onDeleteChatFolder={handleDeleteChatFolder}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.PNP) {
|
||||
let sharingDescription: string;
|
||||
|
||||
|
@ -2248,11 +2381,13 @@ export function PreferencesContent({
|
|||
contents,
|
||||
contentsRef,
|
||||
title,
|
||||
actions,
|
||||
}: {
|
||||
backButton?: JSX.Element | undefined;
|
||||
contents: JSX.Element | undefined;
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
title: string | undefined;
|
||||
actions?: ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="Preferences__content">
|
||||
|
@ -2267,6 +2402,7 @@ export function PreferencesContent({
|
|||
</div>
|
||||
<div className="Preferences__settings-pane-spacer" />
|
||||
</div>
|
||||
{actions && <div className="Preferences__actions">{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
272
ts/components/preferences/ChatFoldersPage.tsx
Normal file
272
ts/components/preferences/ChatFoldersPage.tsx
Normal file
|
@ -0,0 +1,272 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import { ListBox, ListBoxItem } from 'react-aria-components';
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
import { PreferencesContent } from '../Preferences';
|
||||
import { SettingsRow } from '../PreferencesUtil';
|
||||
import type { ChatFolderId } from '../../types/ChatFolder';
|
||||
import {
|
||||
CHAT_FOLDER_PRESETS,
|
||||
matchesChatFolderPreset,
|
||||
type ChatFolderParams,
|
||||
type ChatFolderPreset,
|
||||
type ChatFolderRecord,
|
||||
} from '../../types/ChatFolder';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
// import { showToast } from '../../state/ducks/toast';
|
||||
|
||||
export type ChatFoldersPageProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onBack: () => void;
|
||||
onOpenEditChatFoldersPage: (chatFolderId: ChatFolderId | null) => void;
|
||||
chatFolders: ReadonlyArray<ChatFolderRecord>;
|
||||
onCreateChatFolder: (params: ChatFolderParams) => void;
|
||||
settingsPaneRef: MutableRefObject<HTMLDivElement | null>;
|
||||
}>;
|
||||
|
||||
export function ChatFoldersPage(props: ChatFoldersPageProps): JSX.Element {
|
||||
const { i18n, onOpenEditChatFoldersPage } = props;
|
||||
|
||||
// showToast(
|
||||
// i18n("icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__AddButton__Toast")
|
||||
// )
|
||||
|
||||
const handleOpenEditChatFoldersPageForNew = useCallback(() => {
|
||||
onOpenEditChatFoldersPage(null);
|
||||
}, [onOpenEditChatFoldersPage]);
|
||||
|
||||
return (
|
||||
<PreferencesContent
|
||||
backButton={
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="Preferences__back-icon"
|
||||
onClick={props.onBack}
|
||||
type="button"
|
||||
/>
|
||||
}
|
||||
contents={
|
||||
<>
|
||||
<p className="Preferences__description Preferences__padding">
|
||||
{i18n('icu:Preferences__ChatFoldersPage__Description')}
|
||||
</p>
|
||||
<SettingsRow
|
||||
title={i18n(
|
||||
'icu:Preferences__ChatFoldersPage__FoldersSection__Title'
|
||||
)}
|
||||
>
|
||||
<ListBox>
|
||||
<ListBoxItem
|
||||
className="Preferences__ChatFolders__ChatSelection__Item Preferences__ChatFolders__ChatSelection__Item--Button"
|
||||
onAction={handleOpenEditChatFoldersPageForNew}
|
||||
>
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--Add" />
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
|
||||
{i18n(
|
||||
'icu:Preferences__ChatFoldersPage__FoldersSection__CreateAFolderButton'
|
||||
)}
|
||||
</span>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem className="Preferences__ChatFolders__ChatSelection__Item">
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--Folder" />
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
|
||||
{i18n(
|
||||
'icu:Preferences__ChatFoldersPage__FoldersSection__AllChatsFolder__Title'
|
||||
)}
|
||||
</span>
|
||||
</ListBoxItem>
|
||||
{props.chatFolders.map(chatFolder => {
|
||||
return (
|
||||
<ChatFolderListItem
|
||||
key={chatFolder.id}
|
||||
chatFolder={chatFolder}
|
||||
onOpenEditChatFoldersPage={props.onOpenEditChatFoldersPage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ListBox>
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={i18n(
|
||||
'icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__Title'
|
||||
)}
|
||||
>
|
||||
<ul className="Preferences__ChatFolders__ChatSelection__List">
|
||||
<ChatFolderPresetItem
|
||||
i18n={i18n}
|
||||
icon="UnreadChats"
|
||||
title={i18n(
|
||||
'icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__UnreadFolder__Title'
|
||||
)}
|
||||
description={i18n(
|
||||
'icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__UnreadFolder__Description'
|
||||
)}
|
||||
preset={CHAT_FOLDER_PRESETS.UNREAD_CHATS}
|
||||
chatFolders={props.chatFolders}
|
||||
onCreateChatFolder={props.onCreateChatFolder}
|
||||
/>
|
||||
|
||||
<ChatFolderPresetItem
|
||||
i18n={i18n}
|
||||
icon="DirectChats"
|
||||
title={i18n(
|
||||
'icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__DirectChatsFolder__Title'
|
||||
)}
|
||||
description={i18n(
|
||||
'icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__DirectChatsFolder__Description'
|
||||
)}
|
||||
preset={CHAT_FOLDER_PRESETS.INDIVIDUAL_CHATS}
|
||||
chatFolders={props.chatFolders}
|
||||
onCreateChatFolder={props.onCreateChatFolder}
|
||||
/>
|
||||
|
||||
<ChatFolderPresetItem
|
||||
i18n={i18n}
|
||||
icon="GroupChats"
|
||||
title={i18n(
|
||||
'icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__GroupChatsFolder__Title'
|
||||
)}
|
||||
description={i18n(
|
||||
'icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__GroupChatsFolder__Description'
|
||||
)}
|
||||
preset={CHAT_FOLDER_PRESETS.GROUP_CHATS}
|
||||
chatFolders={props.chatFolders}
|
||||
onCreateChatFolder={props.onCreateChatFolder}
|
||||
/>
|
||||
</ul>
|
||||
</SettingsRow>
|
||||
</>
|
||||
}
|
||||
contentsRef={props.settingsPaneRef}
|
||||
title={i18n('icu:Preferences__ChatFoldersPage__Title')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatFolderPresetItem(props: {
|
||||
i18n: LocalizerType;
|
||||
icon: 'UnreadChats' | 'DirectChats' | 'GroupChats';
|
||||
title: string;
|
||||
description: string;
|
||||
preset: ChatFolderPreset;
|
||||
chatFolders: ReadonlyArray<ChatFolderRecord>;
|
||||
onCreateChatFolder: (params: ChatFolderParams) => void;
|
||||
}) {
|
||||
const { i18n, title, preset, chatFolders, onCreateChatFolder } = props;
|
||||
|
||||
const handleCreateChatFolder = useCallback(() => {
|
||||
onCreateChatFolder({ ...preset, name: title });
|
||||
}, [onCreateChatFolder, title, preset]);
|
||||
|
||||
const hasPreset = useMemo(() => {
|
||||
return chatFolders.some(chatFolder => {
|
||||
return matchesChatFolderPreset(chatFolder, preset);
|
||||
});
|
||||
}, [chatFolders, preset]);
|
||||
|
||||
if (hasPreset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="Preferences__ChatFolders__ChatSelection__Item">
|
||||
<span
|
||||
className={`Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--${props.icon}`}
|
||||
/>
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemBody">
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
|
||||
{props.title}
|
||||
</span>
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemDescription">
|
||||
{props.description}
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
variant={ButtonVariant.Secondary}
|
||||
onClick={handleCreateChatFolder}
|
||||
>
|
||||
{i18n(
|
||||
'icu:Preferences__ChatFoldersPage__SuggestedFoldersSection__AddButton'
|
||||
)}
|
||||
</Button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatFolderListItem(props: {
|
||||
chatFolder: ChatFolderRecord;
|
||||
onOpenEditChatFoldersPage: (chatFolderId: ChatFolderId) => void;
|
||||
}): JSX.Element {
|
||||
const { chatFolder, onOpenEditChatFoldersPage } = props;
|
||||
const handleAction = useCallback(() => {
|
||||
onOpenEditChatFoldersPage(chatFolder.id);
|
||||
}, [chatFolder, onOpenEditChatFoldersPage]);
|
||||
return (
|
||||
<ListBoxItem
|
||||
className="Preferences__ChatFolders__ChatSelection__Item Preferences__ChatFolders__ChatSelection__Item--Button"
|
||||
onAction={handleAction}
|
||||
>
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--Folder" />
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
|
||||
{props.chatFolder.name}
|
||||
</span>
|
||||
</ListBoxItem>
|
||||
);
|
||||
}
|
||||
|
||||
// function ChatFolderContextMenu(props: {
|
||||
// i18n: LocalizerType;
|
||||
// children: ReactNode;
|
||||
// }) {
|
||||
// const { i18n } = props;
|
||||
// return (
|
||||
// <AxoContextMenu.Root>
|
||||
// <AxoContextMenu.Trigger>{props.children}</AxoContextMenu.Trigger>
|
||||
// <AxoContextMenu.Content>
|
||||
// <AxoContextMenu.Item>
|
||||
// {i18n(
|
||||
// eslint-disable-next-line max-len
|
||||
// 'icu:Preferences__ChatsPage__ChatFoldersSection__ChatFolderItem__ContextMenu__EditFolder'
|
||||
// )}
|
||||
// </AxoContextMenu.Item>
|
||||
// <AxoContextMenu.Item>
|
||||
// {i18n(
|
||||
// eslint-disable-next-line max-len
|
||||
// 'icu:Preferences__ChatsPage__ChatFoldersSection__ChatFolderItem__ContextMenu__DeleteFolder'
|
||||
// )}
|
||||
// </AxoContextMenu.Item>
|
||||
// </AxoContextMenu.Content>
|
||||
// </AxoContextMenu.Root>
|
||||
// );
|
||||
// }
|
||||
|
||||
// function DeleteChatFolderDialog(props: { i18n: LocalizerType }): JSX.Element {
|
||||
// const { i18n } = props;
|
||||
// return (
|
||||
// <ConfirmationDialog
|
||||
// i18n={i18n}
|
||||
// dialogName="Preferences__ChatsPage__DeleteChatFolderDialog"
|
||||
// title={i18n('icu:Preferences__ChatsPage__DeleteChatFolderDialog__Title')}
|
||||
// cancelText={i18n(
|
||||
// 'icu:Preferences__ChatsPage__DeleteChatFolderDialog__CancelButton'
|
||||
// )}
|
||||
// actions={[
|
||||
// {
|
||||
// text: i18n(
|
||||
// 'icu:Preferences__ChatsPage__DeleteChatFolderDialog__DeleteButton'
|
||||
// ),
|
||||
// style: 'affirmative',
|
||||
// action: () => null,
|
||||
// },
|
||||
// ]}
|
||||
// onClose={() => null}
|
||||
// >
|
||||
// {i18n('icu:Preferences__ChatsPage__DeleteChatFolderDialog__Description', {
|
||||
// chatFolderTitle: '',
|
||||
// })}
|
||||
// </ConfirmationDialog>
|
||||
// );
|
||||
// }
|
507
ts/components/preferences/EditChatFoldersPage.tsx
Normal file
507
ts/components/preferences/EditChatFoldersPage.tsx
Normal file
|
@ -0,0 +1,507 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { MutableRefObject } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
import type { ThemeType } from '../../types/Util';
|
||||
import { Input } from '../Input';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import type { ChatFolderSelection } from './EditChatFoldersSelectChatsDialog';
|
||||
import { EditChatFoldersSelectChatsDialog } from './EditChatFoldersSelectChatsDialog';
|
||||
import { SettingsRow } from '../PreferencesUtil';
|
||||
import { Checkbox } from '../Checkbox';
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { PreferencesContent } from '../Preferences';
|
||||
import type { ChatFolderId } from '../../types/ChatFolder';
|
||||
import {
|
||||
CHAT_FOLDER_NAME_MAX_CHAR_LENGTH,
|
||||
isSameChatFolderParams,
|
||||
normalizeChatFolderParams,
|
||||
validateChatFolderParams,
|
||||
type ChatFolderParams,
|
||||
} from '../../types/ChatFolder';
|
||||
import type { GetConversationByIdType } from '../../state/selectors/conversations';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
|
||||
export function EditChatFoldersPage(props: {
|
||||
i18n: LocalizerType;
|
||||
existingChatFolderId: ChatFolderId | null;
|
||||
initChatFolderParams: ChatFolderParams;
|
||||
onBack: () => void;
|
||||
conversations: ReadonlyArray<ConversationType>;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
theme: ThemeType;
|
||||
settingsPaneRef: MutableRefObject<HTMLDivElement | null>;
|
||||
conversationSelector: GetConversationByIdType;
|
||||
onDeleteChatFolder: (chatFolderId: ChatFolderId) => void;
|
||||
onCreateChatFolder: (chatFolderParams: ChatFolderParams) => void;
|
||||
onUpdateChatFolder: (
|
||||
chatFolderId: ChatFolderId,
|
||||
chatFolderParams: ChatFolderParams
|
||||
) => void;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
i18n,
|
||||
initChatFolderParams,
|
||||
existingChatFolderId,
|
||||
onCreateChatFolder,
|
||||
onUpdateChatFolder,
|
||||
onDeleteChatFolder,
|
||||
onBack,
|
||||
conversationSelector,
|
||||
} = props;
|
||||
|
||||
const [chatFolderParams, setChatFolderParams] =
|
||||
useState(initChatFolderParams);
|
||||
|
||||
const [showInclusionsDialog, setShowInclusionsDialog] = useState(false);
|
||||
const [showExclusionsDialog, setShowExclusionsDialog] = useState(false);
|
||||
const [showDeleteFolderDialog, setShowDeleteFolderDialog] = useState(false);
|
||||
const [showSaveChangesDialog, setShowSaveChangesDialog] = useState(false);
|
||||
|
||||
const normalizedChatFolderParams = useMemo(() => {
|
||||
return normalizeChatFolderParams(chatFolderParams);
|
||||
}, [chatFolderParams]);
|
||||
|
||||
const isChanged = useMemo(() => {
|
||||
return !isSameChatFolderParams(
|
||||
initChatFolderParams,
|
||||
normalizedChatFolderParams
|
||||
);
|
||||
}, [initChatFolderParams, normalizedChatFolderParams]);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
return validateChatFolderParams(normalizedChatFolderParams);
|
||||
}, [normalizedChatFolderParams]);
|
||||
|
||||
const handleNameChange = useCallback((newName: string) => {
|
||||
setChatFolderParams(prevParams => {
|
||||
return { ...prevParams, name: newName };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleShowOnlyUnreadChange = useCallback((newValue: boolean) => {
|
||||
setChatFolderParams(prevParams => {
|
||||
return { ...prevParams, showOnlyUnread: newValue };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleShowMutedChatsChange = useCallback((newValue: boolean) => {
|
||||
setChatFolderParams(prevParams => {
|
||||
return { ...prevParams, showMutedChats: newValue };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleBackInit = useCallback(() => {
|
||||
if (!isChanged) {
|
||||
onBack();
|
||||
} else {
|
||||
setShowSaveChangesDialog(true);
|
||||
}
|
||||
}, [isChanged, onBack]);
|
||||
|
||||
const handleDiscard = useCallback(() => {
|
||||
onBack();
|
||||
}, [onBack]);
|
||||
|
||||
const handleSaveClose = useCallback(() => {
|
||||
setShowSaveChangesDialog(false);
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
strictAssert(isChanged, 'tried saving when unchanged');
|
||||
strictAssert(isValid, 'tried saving when invalid');
|
||||
|
||||
if (existingChatFolderId != null) {
|
||||
onUpdateChatFolder(existingChatFolderId, chatFolderParams);
|
||||
} else {
|
||||
onCreateChatFolder(chatFolderParams);
|
||||
}
|
||||
onBack();
|
||||
}, [
|
||||
onBack,
|
||||
existingChatFolderId,
|
||||
isChanged,
|
||||
isValid,
|
||||
chatFolderParams,
|
||||
onCreateChatFolder,
|
||||
onUpdateChatFolder,
|
||||
]);
|
||||
|
||||
const handleDeleteInit = useCallback(() => {
|
||||
setShowDeleteFolderDialog(true);
|
||||
}, []);
|
||||
const handleDeleteConfirm = useCallback(() => {
|
||||
strictAssert(existingChatFolderId, 'Missing existing chat folder id');
|
||||
onDeleteChatFolder(existingChatFolderId);
|
||||
setShowDeleteFolderDialog(false);
|
||||
onBack();
|
||||
}, [existingChatFolderId, onDeleteChatFolder, onBack]);
|
||||
const handleDeleteClose = useCallback(() => {
|
||||
setShowDeleteFolderDialog(false);
|
||||
}, []);
|
||||
const handleSelectInclusions = useCallback(() => {
|
||||
setShowInclusionsDialog(true);
|
||||
}, []);
|
||||
const handleSelectExclusions = useCallback(() => {
|
||||
setShowExclusionsDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseInclusions = useCallback(
|
||||
(selection: ChatFolderSelection) => {
|
||||
setChatFolderParams(prevParams => {
|
||||
return {
|
||||
...prevParams,
|
||||
includeAllIndividualChats: selection.selectAllIndividualChats,
|
||||
includeAllGroupChats: selection.selectAllGroupChats,
|
||||
includedConversationIds: selection.selectedRecipientIds,
|
||||
};
|
||||
});
|
||||
setShowInclusionsDialog(false);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCloseExclusions = useCallback(
|
||||
(selection: ChatFolderSelection) => {
|
||||
setChatFolderParams(prevParams => {
|
||||
return {
|
||||
...prevParams,
|
||||
includeAllIndividualChats: !selection.selectAllIndividualChats,
|
||||
includeAllGroupChats: !selection.selectAllGroupChats,
|
||||
excludedConversationIds: selection.selectedRecipientIds,
|
||||
};
|
||||
});
|
||||
setShowExclusionsDialog(false);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<PreferencesContent
|
||||
backButton={
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="Preferences__back-icon"
|
||||
onClick={handleBackInit}
|
||||
type="button"
|
||||
/>
|
||||
}
|
||||
contents={
|
||||
<>
|
||||
<SettingsRow
|
||||
title={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__FolderNameField__Label'
|
||||
)}
|
||||
>
|
||||
<div className="Preferences__padding">
|
||||
<Input
|
||||
i18n={i18n}
|
||||
value={chatFolderParams.name}
|
||||
onChange={handleNameChange}
|
||||
placeholder={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__FolderNameField__Placeholder'
|
||||
)}
|
||||
maxLengthCount={CHAT_FOLDER_NAME_MAX_CHAR_LENGTH}
|
||||
whenToShowRemainingCount={CHAT_FOLDER_NAME_MAX_CHAR_LENGTH - 10}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__IncludedChatsSection__Title'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="Preferences__ChatFolders__ChatSelection__Item Preferences__ChatFolders__ChatSelection__Item--Button"
|
||||
onClick={handleSelectInclusions}
|
||||
>
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--Add" />
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
|
||||
{i18n(
|
||||
'icu:Preferences__EditChatFolderPage__IncludedChatsSection__AddChatsButton'
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<ul className="Preferences__ChatFolders__ChatSelection__List">
|
||||
{chatFolderParams.includeAllIndividualChats && (
|
||||
<li className="Preferences__ChatFolders__ChatSelection__Item">
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--DirectChats" />
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
|
||||
1:1 Chats
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{chatFolderParams.includeAllGroupChats && (
|
||||
<li className="Preferences__ChatFolders__ChatSelection__Item">
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--GroupChats" />
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
|
||||
Group Chats
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{chatFolderParams.includedConversationIds.map(conversationId => {
|
||||
const conversation = conversationSelector(conversationId);
|
||||
return (
|
||||
<li
|
||||
key={conversationId}
|
||||
className="Preferences__ChatFolders__ChatSelection__Item"
|
||||
>
|
||||
<Avatar
|
||||
i18n={i18n}
|
||||
conversationType={conversation.type}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
badge={undefined}
|
||||
{...conversation}
|
||||
/>
|
||||
<span className="Preferences__ChatFolders__ChatList__ItemTitle">
|
||||
{conversation.title}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="Preferences__padding">
|
||||
<p className="Preferences__description">
|
||||
{i18n(
|
||||
'icu:Preferences__EditChatFolderPage__IncludedChatsSection__Help'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__ExceptionsSection__Title'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="Preferences__ChatFolders__ChatSelection__Item Preferences__ChatFolders__ChatSelection__Item--Button"
|
||||
onClick={handleSelectExclusions}
|
||||
>
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--Add" />
|
||||
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
|
||||
{i18n(
|
||||
'icu:Preferences__EditChatFolderPage__ExceptionsSection__ExcludeChatsButton'
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<ul className="Preferences__ChatFolders__ChatSelection__List">
|
||||
{chatFolderParams.excludedConversationIds.map(conversationId => {
|
||||
const conversation = conversationSelector(conversationId);
|
||||
return (
|
||||
<li
|
||||
key={conversationId}
|
||||
className="Preferences__ChatFolders__ChatSelection__Item"
|
||||
>
|
||||
<Avatar
|
||||
i18n={i18n}
|
||||
conversationType={conversation.type}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
badge={undefined}
|
||||
{...conversation}
|
||||
/>
|
||||
<span className="Preferences__ChatFolders__ChatList__ItemTitle">
|
||||
{conversation.title}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="Preferences__padding">
|
||||
<p className="Preferences__description">
|
||||
{i18n(
|
||||
'icu:Preferences__EditChatFolderPage__ExceptionsSection__Help'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
<SettingsRow>
|
||||
<Checkbox
|
||||
checked={chatFolderParams.showOnlyUnread}
|
||||
label={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__OnlyShowUnreadChatsCheckbox__Label'
|
||||
)}
|
||||
description={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__OnlyShowUnreadChatsCheckbox__Description'
|
||||
)}
|
||||
moduleClassName="Preferences__checkbox"
|
||||
name="showOnlyUnread"
|
||||
onChange={handleShowOnlyUnreadChange}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={chatFolderParams.showMutedChats}
|
||||
label={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__IncludeMutedChatsCheckbox__Label'
|
||||
)}
|
||||
moduleClassName="Preferences__checkbox"
|
||||
name="showMutedChats"
|
||||
onChange={handleShowMutedChatsChange}
|
||||
/>
|
||||
</SettingsRow>
|
||||
{props.existingChatFolderId != null && (
|
||||
<SettingsRow>
|
||||
<div className="Preferences__padding">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteInit}
|
||||
className="Preferences__ChatFolders__ChatList__DeleteButton"
|
||||
>
|
||||
{i18n(
|
||||
'icu:Preferences__EditChatFolderPage__DeleteFolderButton'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
)}
|
||||
{showInclusionsDialog && (
|
||||
<EditChatFoldersSelectChatsDialog
|
||||
i18n={i18n}
|
||||
title={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SelectChatsDialog--IncludedChats__Title'
|
||||
)}
|
||||
onClose={handleCloseInclusions}
|
||||
conversations={props.conversations}
|
||||
getPreferredBadge={props.getPreferredBadge}
|
||||
theme={props.theme}
|
||||
conversationSelector={props.conversationSelector}
|
||||
initialSelection={{
|
||||
selectAllIndividualChats:
|
||||
chatFolderParams.includeAllIndividualChats,
|
||||
selectAllGroupChats: chatFolderParams.includeAllGroupChats,
|
||||
selectedRecipientIds: chatFolderParams.includedConversationIds,
|
||||
}}
|
||||
showChatTypes
|
||||
/>
|
||||
)}
|
||||
{showExclusionsDialog && (
|
||||
<EditChatFoldersSelectChatsDialog
|
||||
i18n={i18n}
|
||||
title={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SelectChatsDialog--ExcludedChats__Title'
|
||||
)}
|
||||
onClose={handleCloseExclusions}
|
||||
conversations={props.conversations}
|
||||
getPreferredBadge={props.getPreferredBadge}
|
||||
theme={props.theme}
|
||||
conversationSelector={props.conversationSelector}
|
||||
initialSelection={{
|
||||
selectAllIndividualChats:
|
||||
!chatFolderParams.includeAllIndividualChats,
|
||||
selectAllGroupChats: !chatFolderParams.includeAllGroupChats,
|
||||
selectedRecipientIds: chatFolderParams.excludedConversationIds,
|
||||
}}
|
||||
showChatTypes={false}
|
||||
/>
|
||||
)}
|
||||
{showDeleteFolderDialog && (
|
||||
<DeleteChatFolderDialog
|
||||
i18n={i18n}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onClose={handleDeleteClose}
|
||||
/>
|
||||
)}
|
||||
{showSaveChangesDialog && (
|
||||
<SaveChangesFolderDialog
|
||||
i18n={i18n}
|
||||
onSave={handleSave}
|
||||
onCancel={handleDiscard}
|
||||
onClose={handleSaveClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
contentsRef={props.settingsPaneRef}
|
||||
title={i18n('icu:Preferences__EditChatFolderPage__Title')}
|
||||
actions={
|
||||
<>
|
||||
<Button variant={ButtonVariant.Secondary} onClick={handleDiscard}>
|
||||
{i18n('icu:Preferences__EditChatFolderPage__CancelButton')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={ButtonVariant.Primary}
|
||||
onClick={handleSave}
|
||||
disabled={!(isChanged && isValid)}
|
||||
>
|
||||
{i18n('icu:Preferences__EditChatFolderPage__SaveButton')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteChatFolderDialog(props: {
|
||||
i18n: LocalizerType;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { i18n } = props;
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName="Preferences__EditChatFolderPage__DeleteChatFolderDialog"
|
||||
title={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__Title'
|
||||
)}
|
||||
cancelText={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__CancelButton'
|
||||
)}
|
||||
actions={[
|
||||
{
|
||||
text: i18n(
|
||||
'icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__DeleteButton'
|
||||
),
|
||||
style: 'affirmative',
|
||||
action: props.onConfirm,
|
||||
},
|
||||
]}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
{i18n(
|
||||
'icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__Description'
|
||||
)}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function SaveChangesFolderDialog(props: {
|
||||
i18n: LocalizerType;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { i18n } = props;
|
||||
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName="Preferences__EditChatFolderPage__SaveChangesFolderDialog"
|
||||
title={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__Title'
|
||||
)}
|
||||
cancelText={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__DiscardButton'
|
||||
)}
|
||||
actions={[
|
||||
{
|
||||
text: i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__SaveButton'
|
||||
),
|
||||
style: 'affirmative',
|
||||
action: props.onSave,
|
||||
},
|
||||
]}
|
||||
onCancel={props.onCancel}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
{i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__Description'
|
||||
)}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
270
ts/components/preferences/EditChatFoldersSelectChatsDialog.tsx
Normal file
270
ts/components/preferences/EditChatFoldersSelectChatsDialog.tsx
Normal file
|
@ -0,0 +1,270 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { ChangeEvent } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
import type { ThemeType } from '../../types/Util';
|
||||
import { filterAndSortConversations } from '../../util/filterAndSortConversations';
|
||||
import { ContactPills } from '../ContactPills';
|
||||
import { ContactPill } from '../ContactPill';
|
||||
import {
|
||||
asyncShouldNeverBeCalled,
|
||||
shouldNeverBeCalled,
|
||||
} from '../../util/shouldNeverBeCalled';
|
||||
import { SearchInput } from '../SearchInput';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { Modal } from '../Modal';
|
||||
import type { Row } from '../ConversationList';
|
||||
import {
|
||||
ConversationList,
|
||||
GenericCheckboxRowIcon,
|
||||
RowType,
|
||||
} from '../ConversationList';
|
||||
import type { GetConversationByIdType } from '../../state/selectors/conversations';
|
||||
|
||||
export type ChatFolderSelection = Readonly<{
|
||||
selectedRecipientIds: ReadonlyArray<string>;
|
||||
selectAllIndividualChats: boolean;
|
||||
selectAllGroupChats: boolean;
|
||||
}>;
|
||||
|
||||
export function EditChatFoldersSelectChatsDialog(props: {
|
||||
i18n: LocalizerType;
|
||||
title: string;
|
||||
conversations: ReadonlyArray<ConversationType>;
|
||||
conversationSelector: GetConversationByIdType;
|
||||
onClose: (selection: ChatFolderSelection) => void;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
theme: ThemeType;
|
||||
initialSelection: ChatFolderSelection;
|
||||
showChatTypes: boolean;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
i18n,
|
||||
conversations,
|
||||
conversationSelector,
|
||||
initialSelection,
|
||||
onClose,
|
||||
showChatTypes,
|
||||
} = props;
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
|
||||
const [selectAllIndividualChats, setSelectAllIndividualChats] = useState(
|
||||
initialSelection.selectAllIndividualChats
|
||||
);
|
||||
const [selectAllGroupChats, setSelectAllGroupChats] = useState(
|
||||
initialSelection.selectAllGroupChats
|
||||
);
|
||||
const [selectedRecipientIds, setSelectedRecipientIds] = useState(() => {
|
||||
return new Set(initialSelection.selectedRecipientIds);
|
||||
});
|
||||
|
||||
const handleSearchInputChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchInput(event.currentTarget.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const filteredConversations = useMemo(() => {
|
||||
return filterAndSortConversations(
|
||||
conversations,
|
||||
searchInput,
|
||||
undefined,
|
||||
false,
|
||||
undefined
|
||||
);
|
||||
}, [conversations, searchInput]);
|
||||
|
||||
const handleToggleDirectChats = useCallback(() => {
|
||||
setSelectAllIndividualChats(value => !value);
|
||||
}, []);
|
||||
|
||||
const handleToggleGroupChats = useCallback(() => {
|
||||
setSelectAllGroupChats(value => !value);
|
||||
}, []);
|
||||
|
||||
const handleToggleSelectedConversation = useCallback(
|
||||
(conversationId: string) => {
|
||||
setSelectedRecipientIds(prev => {
|
||||
const copy = new Set(prev);
|
||||
if (copy.has(conversationId)) {
|
||||
copy.delete(conversationId);
|
||||
} else {
|
||||
copy.add(conversationId);
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const rows = useMemo((): ReadonlyArray<Row> => {
|
||||
const result: Array<Row> = [];
|
||||
|
||||
if (showChatTypes && searchInput.trim() === '') {
|
||||
result.push({
|
||||
type: RowType.Header,
|
||||
getHeaderText: () => {
|
||||
return i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SelectChatsDialog__ChatTypesSection__Title'
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
result.push({
|
||||
type: RowType.GenericCheckbox,
|
||||
icon: GenericCheckboxRowIcon.Contact,
|
||||
label: i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SelectChatsDialog__ChatTypesSection__DirectChats'
|
||||
),
|
||||
isChecked: selectAllIndividualChats,
|
||||
onClick: handleToggleDirectChats,
|
||||
});
|
||||
|
||||
result.push({
|
||||
type: RowType.GenericCheckbox,
|
||||
icon: GenericCheckboxRowIcon.Group,
|
||||
label: i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SelectChatsDialog__ChatTypesSection__GroupChats'
|
||||
),
|
||||
isChecked: selectAllGroupChats,
|
||||
onClick: handleToggleGroupChats,
|
||||
});
|
||||
|
||||
result.push({
|
||||
type: RowType.Header,
|
||||
getHeaderText: () => {
|
||||
return i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SelectChatsDialog__RecentChats__Title'
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const conversation of filteredConversations) {
|
||||
result.push({
|
||||
type: RowType.ContactCheckbox,
|
||||
contact: conversation,
|
||||
isChecked: selectedRecipientIds.has(conversation.id),
|
||||
disabledReason: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (filteredConversations.length === 0) {
|
||||
result.push({
|
||||
type: RowType.EmptyResults,
|
||||
message: 'No items',
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [
|
||||
i18n,
|
||||
searchInput,
|
||||
filteredConversations,
|
||||
selectAllIndividualChats,
|
||||
selectAllGroupChats,
|
||||
selectedRecipientIds,
|
||||
handleToggleDirectChats,
|
||||
handleToggleGroupChats,
|
||||
showChatTypes,
|
||||
]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onClose({
|
||||
selectAllIndividualChats,
|
||||
selectAllGroupChats,
|
||||
selectedRecipientIds: Array.from(selectedRecipientIds),
|
||||
});
|
||||
}, [
|
||||
onClose,
|
||||
selectAllIndividualChats,
|
||||
selectAllGroupChats,
|
||||
selectedRecipientIds,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalName="Preferences__EditChatFolderPage__SelectChatsDialog"
|
||||
moduleClassName="Preferences__EditChatFolderPage__SelectChatsDialog"
|
||||
i18n={i18n}
|
||||
title={props.title}
|
||||
onClose={handleClose}
|
||||
padded={false}
|
||||
noMouseClose
|
||||
noEscapeClose
|
||||
modalFooter={
|
||||
<Button variant={ButtonVariant.Primary} onClick={handleClose}>
|
||||
{i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SelectChatsDialog__DoneButton'
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<SearchInput
|
||||
i18n={i18n}
|
||||
placeholder={i18n(
|
||||
'icu:Preferences__EditChatFolderPage__SelectChatsDialog__Search__Placeholder'
|
||||
)}
|
||||
value={searchInput}
|
||||
onChange={handleSearchInputChange}
|
||||
/>
|
||||
{selectedRecipientIds.size > 0 && (
|
||||
<ContactPills>
|
||||
{Array.from(selectedRecipientIds, conversationId => {
|
||||
const conversation = conversationSelector(conversationId);
|
||||
return (
|
||||
<ContactPill
|
||||
key={conversationId}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
color={conversation.color}
|
||||
firstName={conversation.firstName}
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
i18n={i18n}
|
||||
id={conversation.id}
|
||||
isMe={conversation.isMe}
|
||||
phoneNumber={conversation.phoneNumber}
|
||||
profileName={conversation.profileName}
|
||||
sharedGroupNames={conversation.sharedGroupNames}
|
||||
title={conversation.title}
|
||||
onClickRemove={handleToggleSelectedConversation}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ContactPills>
|
||||
)}
|
||||
<ConversationList
|
||||
dimensions={{
|
||||
width: 360,
|
||||
height: 404,
|
||||
}}
|
||||
i18n={i18n}
|
||||
getPreferredBadge={props.getPreferredBadge}
|
||||
getRow={index => rows[index]}
|
||||
onClickContactCheckbox={handleToggleSelectedConversation}
|
||||
rowCount={rows.length}
|
||||
shouldRecomputeRowHeights={false}
|
||||
theme={props.theme}
|
||||
// never called:
|
||||
blockConversation={shouldNeverBeCalled}
|
||||
lookupConversationWithoutServiceId={asyncShouldNeverBeCalled}
|
||||
onClickArchiveButton={shouldNeverBeCalled}
|
||||
onClickClearFilterButton={shouldNeverBeCalled}
|
||||
onOutgoingAudioCallInConversation={shouldNeverBeCalled}
|
||||
onOutgoingVideoCallInConversation={shouldNeverBeCalled}
|
||||
onPreloadConversation={shouldNeverBeCalled}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
removeConversation={shouldNeverBeCalled}
|
||||
setIsFetchingUUID={shouldNeverBeCalled}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
showConversation={shouldNeverBeCalled}
|
||||
showFindByPhoneNumber={shouldNeverBeCalled}
|
||||
showFindByUsername={shouldNeverBeCalled}
|
||||
showUserNotFoundModal={shouldNeverBeCalled}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -10,6 +10,8 @@ import type { MutableRefObject } from 'react';
|
|||
import { useItemsActions } from '../ducks/items';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import {
|
||||
getAllComposableConversations,
|
||||
getConversationSelector,
|
||||
getConversationsWithCustomColorSelector,
|
||||
getMe,
|
||||
} from '../selectors/conversations';
|
||||
|
@ -154,6 +156,8 @@ export function SmartPreferences(): JSX.Element | null {
|
|||
getConversationsWithCustomColorSelector
|
||||
);
|
||||
const i18n = useSelector(getIntl);
|
||||
const conversations = useSelector(getAllComposableConversations);
|
||||
const conversationSelector = useSelector(getConversationSelector);
|
||||
const items = useSelector(getItems);
|
||||
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
|
||||
const dialogType = useSelector(getUpdateDialogType);
|
||||
|
@ -163,8 +167,9 @@ export function SmartPreferences(): JSX.Element | null {
|
|||
const preferredWidthFromStorage = useSelector(getPreferredLeftPaneWidth);
|
||||
const theme = useSelector(getTheme);
|
||||
|
||||
const badge = useSelector(getPreferredBadgeSelector)(me.badges);
|
||||
const shouldShowUpdateDialog = dialogType !== DialogType.None;
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const badge = getPreferredBadge(me.badges);
|
||||
|
||||
// The weird ones
|
||||
|
||||
|
@ -676,6 +681,8 @@ export function SmartPreferences(): JSX.Element | null {
|
|||
return (
|
||||
<StrictMode>
|
||||
<Preferences
|
||||
conversations={conversations}
|
||||
conversationSelector={conversationSelector}
|
||||
accountEntropyPool={accountEntropyPool}
|
||||
addCustomColor={addCustomColor}
|
||||
autoDownloadAttachment={autoDownloadAttachment}
|
||||
|
@ -706,6 +713,7 @@ export function SmartPreferences(): JSX.Element | null {
|
|||
getMessageSampleForSchemaVersion={
|
||||
DataReader.getMessageSampleForSchemaVersion
|
||||
}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
hasAudioNotifications={hasAudioNotifications}
|
||||
hasAutoConvertEmoji={hasAutoConvertEmoji}
|
||||
hasAutoDownloadUpdate={hasAutoDownloadUpdate}
|
||||
|
|
144
ts/types/ChatFolder.ts
Normal file
144
ts/types/ChatFolder.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Environment, getEnvironment } from '../environment';
|
||||
import * as grapheme from '../util/grapheme';
|
||||
import * as RemoteConfig from '../RemoteConfig';
|
||||
import { isAlpha, isBeta, isProduction } from '../util/version';
|
||||
|
||||
export const CHAT_FOLDER_NAME_MAX_CHAR_LENGTH = 32;
|
||||
|
||||
export enum ChatFolderType {
|
||||
ALL = 1,
|
||||
CUSTOM = 2,
|
||||
}
|
||||
|
||||
export type ChatFolderId = string & { ChatFolderId: never };
|
||||
|
||||
export type ChatFolderRecord = Readonly<{
|
||||
id: ChatFolderId;
|
||||
name: string;
|
||||
showOnlyUnread: boolean;
|
||||
showMutedChats: boolean;
|
||||
includeAllIndividualChats: boolean;
|
||||
includeAllGroupChats: boolean;
|
||||
folderType: ChatFolderType;
|
||||
includedConversationIds: ReadonlyArray<string>;
|
||||
excludedConversationIds: ReadonlyArray<string>;
|
||||
}>;
|
||||
|
||||
export type ChatFolderParams = Omit<ChatFolderRecord, 'id'>;
|
||||
export type ChatFolderPreset = Omit<ChatFolderParams, 'name'>;
|
||||
|
||||
export const CHAT_FOLDER_DEFAULTS: ChatFolderParams = {
|
||||
name: '',
|
||||
showOnlyUnread: false,
|
||||
showMutedChats: false,
|
||||
includeAllIndividualChats: false,
|
||||
includeAllGroupChats: false,
|
||||
folderType: ChatFolderType.CUSTOM,
|
||||
includedConversationIds: [],
|
||||
excludedConversationIds: [],
|
||||
};
|
||||
|
||||
export const CHAT_FOLDER_PRESETS = {
|
||||
UNREAD_CHATS: {
|
||||
showOnlyUnread: true, // only unread
|
||||
showMutedChats: false,
|
||||
includeAllIndividualChats: true, // all 1:1's
|
||||
includeAllGroupChats: true, // all groups
|
||||
folderType: ChatFolderType.CUSTOM,
|
||||
includedConversationIds: [],
|
||||
excludedConversationIds: [],
|
||||
},
|
||||
INDIVIDUAL_CHATS: {
|
||||
showOnlyUnread: false,
|
||||
showMutedChats: false,
|
||||
includeAllIndividualChats: true, // all 1:1's
|
||||
includeAllGroupChats: false,
|
||||
folderType: ChatFolderType.CUSTOM,
|
||||
includedConversationIds: [],
|
||||
excludedConversationIds: [],
|
||||
},
|
||||
GROUP_CHATS: {
|
||||
showOnlyUnread: false,
|
||||
showMutedChats: false,
|
||||
includeAllIndividualChats: false,
|
||||
includeAllGroupChats: true, // all groups
|
||||
folderType: ChatFolderType.CUSTOM,
|
||||
includedConversationIds: [],
|
||||
excludedConversationIds: [],
|
||||
},
|
||||
} as const satisfies Record<string, ChatFolderPreset>;
|
||||
|
||||
export type ChatFolderPresetKey = keyof typeof CHAT_FOLDER_PRESETS;
|
||||
|
||||
export function normalizeChatFolderParams(
|
||||
params: ChatFolderParams
|
||||
): ChatFolderParams {
|
||||
return {
|
||||
...params,
|
||||
name: params.name.normalize().trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export function validateChatFolderParams(params: ChatFolderParams): boolean {
|
||||
return (
|
||||
params.name !== '' &&
|
||||
grapheme.count(params.name) <= CHAT_FOLDER_NAME_MAX_CHAR_LENGTH
|
||||
);
|
||||
}
|
||||
|
||||
export function matchesChatFolderPreset(
|
||||
params: ChatFolderParams,
|
||||
preset: ChatFolderPreset
|
||||
): boolean {
|
||||
return (
|
||||
params.showOnlyUnread === preset.showOnlyUnread &&
|
||||
params.showMutedChats === preset.showMutedChats &&
|
||||
params.includeAllIndividualChats === preset.includeAllIndividualChats &&
|
||||
params.includeAllGroupChats === preset.includeAllGroupChats &&
|
||||
params.folderType === preset.folderType &&
|
||||
isSameConversationIds(
|
||||
params.includedConversationIds,
|
||||
preset.includedConversationIds
|
||||
) &&
|
||||
isSameConversationIds(
|
||||
params.excludedConversationIds,
|
||||
preset.excludedConversationIds
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isSameChatFolderParams(
|
||||
a: ChatFolderParams,
|
||||
b: ChatFolderParams
|
||||
): boolean {
|
||||
return a.name === b.name && matchesChatFolderPreset(a, b);
|
||||
}
|
||||
|
||||
function isSameConversationIds(
|
||||
a: ReadonlyArray<string>,
|
||||
b: ReadonlyArray<string>
|
||||
): boolean {
|
||||
return new Set(a).symmetricDifference(new Set(b)).size === 0;
|
||||
}
|
||||
|
||||
export function isChatFoldersEnabled(): boolean {
|
||||
const version = window.getVersion?.();
|
||||
|
||||
if (version != null) {
|
||||
if (isProduction(version)) {
|
||||
return RemoteConfig.isEnabled('desktop.chatFolders.prod');
|
||||
}
|
||||
if (isBeta(version)) {
|
||||
return RemoteConfig.isEnabled('desktop.chatFolders.beta');
|
||||
}
|
||||
if (isAlpha(version)) {
|
||||
return RemoteConfig.isEnabled('desktop.chatFolders.alpha');
|
||||
}
|
||||
}
|
||||
|
||||
const env = getEnvironment();
|
||||
return env === Environment.Development || env === Environment.Test;
|
||||
}
|
1
ts/window.d.ts
vendored
1
ts/window.d.ts
vendored
|
@ -324,6 +324,7 @@ declare global {
|
|||
interface Set<T> {
|
||||
// Needed until TS upgrade
|
||||
difference<U>(other: ReadonlySet<U>): Set<T>;
|
||||
symmetricDifference<U>(other: ReadonlySet<U>): Set<T>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue