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",
|
"messageformat": "Emoji skin tone",
|
||||||
"description": "Preferences Window > Chats Tab > Emoji skin tone default setting > Label"
|
"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": {
|
"icu:initialSync": {
|
||||||
"messageformat": "Syncing contacts and groups",
|
"messageformat": "Syncing contacts and groups",
|
||||||
"description": "Shown during initial link while contacts and groups are being pulled from mobile device"
|
"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;
|
$normal-row-height: 72px;
|
||||||
|
|
||||||
@include mixins.NavTabs__Scroller;
|
@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
|
// list tiles in choose-group-members and compose extend to the edge
|
||||||
.module-left-pane--mode-choose-group-members &,
|
.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
|
||||||
|
|
||||||
.module-left-pane {
|
.module-left-pane {
|
||||||
|
|
|
@ -289,6 +289,16 @@ $secondary-text-color: light-dark(
|
||||||
container-type: inline-size;
|
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 {
|
&__settings-pane {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -1297,3 +1307,118 @@ $secondary-text-color: light-dark(
|
||||||
user-select: text;
|
user-select: text;
|
||||||
overflow-x: scroll;
|
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.ringrtcAdmFull.3'
|
||||||
| 'desktop.calling.ringrtcAdmInternal'
|
| 'desktop.calling.ringrtcAdmInternal'
|
||||||
| 'desktop.calling.ringrtcAdmPreStable'
|
| 'desktop.calling.ringrtcAdmPreStable'
|
||||||
|
| 'desktop.chatFolders.alpha'
|
||||||
|
| 'desktop.chatFolders.beta'
|
||||||
|
| 'desktop.chatFolders.prod'
|
||||||
| 'desktop.clientExpiration'
|
| 'desktop.clientExpiration'
|
||||||
| 'desktop.backup.credentialFetch'
|
| 'desktop.backup.credentialFetch'
|
||||||
| 'desktop.donations'
|
| 'desktop.donations'
|
||||||
|
|
|
@ -37,6 +37,7 @@ import { UsernameSearchResultListItem } from './conversationList/UsernameSearchR
|
||||||
import { GroupListItem } from './conversationList/GroupListItem';
|
import { GroupListItem } from './conversationList/GroupListItem';
|
||||||
import { ListView } from './ListView';
|
import { ListView } from './ListView';
|
||||||
import { Button, ButtonVariant } from './Button';
|
import { Button, ButtonVariant } from './Button';
|
||||||
|
import { ListTile } from './ListTile';
|
||||||
|
|
||||||
export enum RowType {
|
export enum RowType {
|
||||||
ArchiveButton = 'ArchiveButton',
|
ArchiveButton = 'ArchiveButton',
|
||||||
|
@ -46,6 +47,7 @@ export enum RowType {
|
||||||
ContactCheckbox = 'ContactCheckbox',
|
ContactCheckbox = 'ContactCheckbox',
|
||||||
PhoneNumberCheckbox = 'PhoneNumberCheckbox',
|
PhoneNumberCheckbox = 'PhoneNumberCheckbox',
|
||||||
UsernameCheckbox = 'UsernameCheckbox',
|
UsernameCheckbox = 'UsernameCheckbox',
|
||||||
|
GenericCheckbox = 'GenericCheckbox',
|
||||||
Conversation = 'Conversation',
|
Conversation = 'Conversation',
|
||||||
CreateNewGroup = 'CreateNewGroup',
|
CreateNewGroup = 'CreateNewGroup',
|
||||||
FindByUsername = 'FindByUsername',
|
FindByUsername = 'FindByUsername',
|
||||||
|
@ -58,6 +60,7 @@ export enum RowType {
|
||||||
SelectSingleGroup = 'SelectSingleGroup',
|
SelectSingleGroup = 'SelectSingleGroup',
|
||||||
StartNewConversation = 'StartNewConversation',
|
StartNewConversation = 'StartNewConversation',
|
||||||
UsernameSearchResult = 'UsernameSearchResult',
|
UsernameSearchResult = 'UsernameSearchResult',
|
||||||
|
EmptyResults = 'EmptyResults',
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArchiveButtonRowType = {
|
type ArchiveButtonRowType = {
|
||||||
|
@ -100,6 +103,19 @@ type UsernameCheckboxRowType = {
|
||||||
isFetching: boolean;
|
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 ConversationRowType = {
|
||||||
type: RowType.Conversation;
|
type: RowType.Conversation;
|
||||||
conversation: ConversationListItemPropsType;
|
conversation: ConversationListItemPropsType;
|
||||||
|
@ -160,6 +176,11 @@ type UsernameRowType = {
|
||||||
isFetchingUsername: boolean;
|
isFetchingUsername: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EmptyResultsRowType = {
|
||||||
|
type: RowType.EmptyResults;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Row =
|
export type Row =
|
||||||
| ArchiveButtonRowType
|
| ArchiveButtonRowType
|
||||||
| BlankRowType
|
| BlankRowType
|
||||||
|
@ -168,6 +189,7 @@ export type Row =
|
||||||
| ClearFilterButtonRowType
|
| ClearFilterButtonRowType
|
||||||
| PhoneNumberCheckboxRowType
|
| PhoneNumberCheckboxRowType
|
||||||
| UsernameCheckboxRowType
|
| UsernameCheckboxRowType
|
||||||
|
| GenericCheckboxRowType
|
||||||
| ConversationRowType
|
| ConversationRowType
|
||||||
| CreateNewGroupRowType
|
| CreateNewGroupRowType
|
||||||
| FindByUsername
|
| FindByUsername
|
||||||
|
@ -178,7 +200,8 @@ export type Row =
|
||||||
| SearchResultsLoadingFakeRowType
|
| SearchResultsLoadingFakeRowType
|
||||||
| StartNewConversationRowType
|
| StartNewConversationRowType
|
||||||
| SelectSingleGroupRowType
|
| SelectSingleGroupRowType
|
||||||
| UsernameRowType;
|
| UsernameRowType
|
||||||
|
| EmptyResultsRowType;
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
dimensions?: {
|
dimensions?: {
|
||||||
|
@ -222,6 +245,7 @@ export type PropsType = {
|
||||||
const NORMAL_ROW_HEIGHT = 76;
|
const NORMAL_ROW_HEIGHT = 76;
|
||||||
const SELECT_ROW_HEIGHT = 52;
|
const SELECT_ROW_HEIGHT = 52;
|
||||||
const HEADER_ROW_HEIGHT = 40;
|
const HEADER_ROW_HEIGHT = 40;
|
||||||
|
const EMPTY_RESULTS_ROW_HEIGHT = 48 + 20 + 48;
|
||||||
|
|
||||||
export function ConversationList({
|
export function ConversationList({
|
||||||
dimensions,
|
dimensions,
|
||||||
|
@ -260,19 +284,34 @@ export function ConversationList({
|
||||||
assertDev(false, `Expected a row at index ${index}`);
|
assertDev(false, `Expected a row at index ${index}`);
|
||||||
return NORMAL_ROW_HEIGHT;
|
return NORMAL_ROW_HEIGHT;
|
||||||
}
|
}
|
||||||
switch (row.type) {
|
const { type } = row;
|
||||||
|
switch (type) {
|
||||||
case RowType.Header:
|
case RowType.Header:
|
||||||
case RowType.SearchResultsLoadingFakeHeader:
|
case RowType.SearchResultsLoadingFakeHeader:
|
||||||
return HEADER_ROW_HEIGHT;
|
return HEADER_ROW_HEIGHT;
|
||||||
case RowType.SelectSingleGroup:
|
case RowType.SelectSingleGroup:
|
||||||
case RowType.ContactCheckbox:
|
case RowType.ContactCheckbox:
|
||||||
|
case RowType.GenericCheckbox:
|
||||||
case RowType.Contact:
|
case RowType.Contact:
|
||||||
case RowType.CreateNewGroup:
|
case RowType.CreateNewGroup:
|
||||||
case RowType.FindByUsername:
|
case RowType.FindByUsername:
|
||||||
case RowType.FindByPhoneNumber:
|
case RowType.FindByPhoneNumber:
|
||||||
return SELECT_ROW_HEIGHT;
|
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;
|
return NORMAL_ROW_HEIGHT;
|
||||||
|
case RowType.EmptyResults:
|
||||||
|
return EMPTY_RESULTS_ROW_HEIGHT;
|
||||||
|
default:
|
||||||
|
throw missingCaseError(type);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[getRow]
|
[getRow]
|
||||||
|
@ -400,6 +439,21 @@ export function ConversationList({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
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: {
|
case RowType.Conversation: {
|
||||||
const itemProps = pick(row.conversation, [
|
const itemProps = pick(row.conversation, [
|
||||||
'avatarPlaceholderGradient',
|
'avatarPlaceholderGradient',
|
||||||
|
@ -539,6 +593,13 @@ export function ConversationList({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case RowType.EmptyResults:
|
||||||
|
result = (
|
||||||
|
<div className="module-conversation-list__empty-results">
|
||||||
|
{row.message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(row);
|
throw missingCaseError(row);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,10 +52,9 @@ export function ListView({
|
||||||
|
|
||||||
const style: React.CSSProperties = useMemo(() => {
|
const style: React.CSSProperties = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
// See `<Timeline>` for an explanation of this `any` cast.
|
overflowY: scrollable ? 'auto' : 'hidden',
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
overflowY: scrollable ? ('overlay' as any) : 'hidden',
|
|
||||||
direction: 'inherit',
|
direction: 'inherit',
|
||||||
|
scrollbarGutter: 'stable',
|
||||||
};
|
};
|
||||||
}, [scrollable]);
|
}, [scrollable]);
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { Meta, StoryFn } from '@storybook/react';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { shuffle } from 'lodash';
|
||||||
import { Page, Preferences } from './Preferences';
|
import { Page, Preferences } from './Preferences';
|
||||||
import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors';
|
import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors';
|
||||||
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
||||||
|
@ -14,7 +15,10 @@ import { DAY, DurationInSeconds, WEEK } from '../util/durations';
|
||||||
import { DialogUpdate } from './DialogUpdate';
|
import { DialogUpdate } from './DialogUpdate';
|
||||||
import { DialogType } from '../types/Dialogs';
|
import { DialogType } from '../types/Dialogs';
|
||||||
import { ThemeType } from '../types/Util';
|
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 { EditState, ProfileEditor } from './ProfileEditor';
|
||||||
import {
|
import {
|
||||||
UsernameEditState,
|
UsernameEditState,
|
||||||
|
@ -25,6 +29,7 @@ import type { PropsType } from './Preferences';
|
||||||
import type { WidthBreakpoint } from './_util';
|
import type { WidthBreakpoint } from './_util';
|
||||||
import type { MessageAttributesType } from '../model-types';
|
import type { MessageAttributesType } from '../model-types';
|
||||||
import { PreferencesDonations } from './PreferencesDonations';
|
import { PreferencesDonations } from './PreferencesDonations';
|
||||||
|
import { strictAssert } from '../util/assert';
|
||||||
|
|
||||||
const { i18n } = window.SignalContext;
|
const { i18n } = window.SignalContext;
|
||||||
|
|
||||||
|
@ -34,6 +39,20 @@ const me = {
|
||||||
username: 'someone.243',
|
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 = [
|
const availableMicrophones = [
|
||||||
{
|
{
|
||||||
name: 'DefAuLt (Headphones)',
|
name: 'DefAuLt (Headphones)',
|
||||||
|
@ -164,6 +183,9 @@ export default {
|
||||||
args: {
|
args: {
|
||||||
i18n,
|
i18n,
|
||||||
|
|
||||||
|
conversations,
|
||||||
|
conversationSelector,
|
||||||
|
|
||||||
accountEntropyPool:
|
accountEntropyPool:
|
||||||
'uy38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t',
|
'uy38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t',
|
||||||
autoDownloadAttachment: {
|
autoDownloadAttachment: {
|
||||||
|
@ -273,6 +295,7 @@ export default {
|
||||||
renderToastManager,
|
renderToastManager,
|
||||||
renderUpdateDialog,
|
renderUpdateDialog,
|
||||||
getConversationsWithCustomColor: () => [],
|
getConversationsWithCustomColor: () => [],
|
||||||
|
getPreferredBadge: () => undefined,
|
||||||
|
|
||||||
addCustomColor: action('addCustomColor'),
|
addCustomColor: action('addCustomColor'),
|
||||||
doDeleteAllData: action('doDeleteAllData'),
|
doDeleteAllData: action('doDeleteAllData'),
|
||||||
|
@ -379,6 +402,14 @@ export const Chats = Template.bind({});
|
||||||
Chats.args = {
|
Chats.args = {
|
||||||
page: Page.Chats,
|
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({});
|
export const Calls = Template.bind({});
|
||||||
Calls.args = {
|
Calls.args = {
|
||||||
page: Page.Calls,
|
page: Page.Calls,
|
||||||
|
|
|
@ -13,9 +13,7 @@ import React, {
|
||||||
import { isNumber, noop, partition } from 'lodash';
|
import { isNumber, noop, partition } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||||
|
import type { MutableRefObject, ReactNode } from 'react';
|
||||||
import type { MutableRefObject } from 'react';
|
|
||||||
|
|
||||||
import { Button, ButtonVariant } from './Button';
|
import { Button, ButtonVariant } from './Button';
|
||||||
import { ChatColorPicker } from './ChatColorPicker';
|
import { ChatColorPicker } from './ChatColorPicker';
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
|
@ -37,7 +35,7 @@ import { focusableSelector } from '../util/focusableSelectors';
|
||||||
import { Modal } from './Modal';
|
import { Modal } from './Modal';
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
import { removeDiacritics } from '../util/removeDiacritics';
|
import { removeDiacritics } from '../util/removeDiacritics';
|
||||||
import { assertDev } from '../util/assert';
|
import { assertDev, strictAssert } from '../util/assert';
|
||||||
import { I18n } from './I18n';
|
import { I18n } from './I18n';
|
||||||
import { FunSkinTonesList } from './fun/FunSkinTones';
|
import { FunSkinTonesList } from './fun/FunSkinTones';
|
||||||
import { emojiParentKeyConstant, type EmojiSkinTone } from './fun/data/emojis';
|
import { emojiParentKeyConstant, type EmojiSkinTone } from './fun/data/emojis';
|
||||||
|
@ -89,11 +87,27 @@ import type {
|
||||||
PromptOSAuthReasonType,
|
PromptOSAuthReasonType,
|
||||||
PromptOSAuthResultType,
|
PromptOSAuthResultType,
|
||||||
} from '../util/os/promptOSAuthMain';
|
} 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 CheckboxChangeHandlerType = (value: boolean) => unknown;
|
||||||
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
|
conversations: ReadonlyArray<ConversationType>;
|
||||||
|
conversationSelector: GetConversationByIdType;
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
accountEntropyPool: string | undefined;
|
accountEntropyPool: string | undefined;
|
||||||
autoDownloadAttachment: AutoDownloadAttachmentType;
|
autoDownloadAttachment: AutoDownloadAttachmentType;
|
||||||
|
@ -207,6 +221,7 @@ type PropsFunctionType = {
|
||||||
version: number
|
version: number
|
||||||
) => Promise<Array<MessageAttributesType>>;
|
) => Promise<Array<MessageAttributesType>>;
|
||||||
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
|
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
|
||||||
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
makeSyncRequest: () => unknown;
|
makeSyncRequest: () => unknown;
|
||||||
onStartUpdate: () => unknown;
|
onStartUpdate: () => unknown;
|
||||||
pickLocalBackupFolder: () => Promise<string | undefined>;
|
pickLocalBackupFolder: () => Promise<string | undefined>;
|
||||||
|
@ -296,6 +311,8 @@ export enum Page {
|
||||||
|
|
||||||
// Sub pages
|
// Sub pages
|
||||||
ChatColor = 'ChatColor',
|
ChatColor = 'ChatColor',
|
||||||
|
ChatFolders = 'ChatFolders',
|
||||||
|
EditChatFolder = 'EditChatFolder',
|
||||||
PNP = 'PNP',
|
PNP = 'PNP',
|
||||||
BackupsDetails = 'BackupsDetails',
|
BackupsDetails = 'BackupsDetails',
|
||||||
LocalBackups = 'LocalBackups',
|
LocalBackups = 'LocalBackups',
|
||||||
|
@ -333,6 +350,8 @@ const DEFAULT_ZOOM_FACTORS = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Preferences({
|
export function Preferences({
|
||||||
|
conversations,
|
||||||
|
conversationSelector,
|
||||||
accountEntropyPool,
|
accountEntropyPool,
|
||||||
addCustomColor,
|
addCustomColor,
|
||||||
autoDownloadAttachment,
|
autoDownloadAttachment,
|
||||||
|
@ -358,6 +377,7 @@ export function Preferences({
|
||||||
getConversationsWithCustomColor,
|
getConversationsWithCustomColor,
|
||||||
getMessageCountBySchemaVersion,
|
getMessageCountBySchemaVersion,
|
||||||
getMessageSampleForSchemaVersion,
|
getMessageSampleForSchemaVersion,
|
||||||
|
getPreferredBadge,
|
||||||
hasAudioNotifications,
|
hasAudioNotifications,
|
||||||
hasAutoConvertEmoji,
|
hasAutoConvertEmoji,
|
||||||
hasAutoDownloadUpdate,
|
hasAutoDownloadUpdate,
|
||||||
|
@ -497,6 +517,54 @@ export function Preferences({
|
||||||
const [confirmPnpNotDiscoverable, setConfirmPnpNoDiscoverable] =
|
const [confirmPnpNotDiscoverable, setConfirmPnpNoDiscoverable] =
|
||||||
useState(false);
|
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() {
|
function closeLanguageDialog() {
|
||||||
setLanguageDialog(null);
|
setLanguageDialog(null);
|
||||||
setSelectedLanguageLocale(localeOverride);
|
setSelectedLanguageLocale(localeOverride);
|
||||||
|
@ -1103,6 +1171,33 @@ export function Preferences({
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</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 && (
|
{isSyncSupported && (
|
||||||
<SettingsRow>
|
<SettingsRow>
|
||||||
<Control
|
<Control
|
||||||
|
@ -1828,6 +1923,44 @@ export function Preferences({
|
||||||
title={i18n('icu:ChatColorPicker__menu-title')}
|
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) {
|
} else if (page === Page.PNP) {
|
||||||
let sharingDescription: string;
|
let sharingDescription: string;
|
||||||
|
|
||||||
|
@ -2248,11 +2381,13 @@ export function PreferencesContent({
|
||||||
contents,
|
contents,
|
||||||
contentsRef,
|
contentsRef,
|
||||||
title,
|
title,
|
||||||
|
actions,
|
||||||
}: {
|
}: {
|
||||||
backButton?: JSX.Element | undefined;
|
backButton?: JSX.Element | undefined;
|
||||||
contents: JSX.Element | undefined;
|
contents: JSX.Element | undefined;
|
||||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
title: string | undefined;
|
title: string | undefined;
|
||||||
|
actions?: ReactNode;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="Preferences__content">
|
<div className="Preferences__content">
|
||||||
|
@ -2267,6 +2402,7 @@ export function PreferencesContent({
|
||||||
</div>
|
</div>
|
||||||
<div className="Preferences__settings-pane-spacer" />
|
<div className="Preferences__settings-pane-spacer" />
|
||||||
</div>
|
</div>
|
||||||
|
{actions && <div className="Preferences__actions">{actions}</div>}
|
||||||
</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 { useItemsActions } from '../ducks/items';
|
||||||
import { useConversationsActions } from '../ducks/conversations';
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
import {
|
import {
|
||||||
|
getAllComposableConversations,
|
||||||
|
getConversationSelector,
|
||||||
getConversationsWithCustomColorSelector,
|
getConversationsWithCustomColorSelector,
|
||||||
getMe,
|
getMe,
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
|
@ -154,6 +156,8 @@ export function SmartPreferences(): JSX.Element | null {
|
||||||
getConversationsWithCustomColorSelector
|
getConversationsWithCustomColorSelector
|
||||||
);
|
);
|
||||||
const i18n = useSelector(getIntl);
|
const i18n = useSelector(getIntl);
|
||||||
|
const conversations = useSelector(getAllComposableConversations);
|
||||||
|
const conversationSelector = useSelector(getConversationSelector);
|
||||||
const items = useSelector(getItems);
|
const items = useSelector(getItems);
|
||||||
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
|
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
|
||||||
const dialogType = useSelector(getUpdateDialogType);
|
const dialogType = useSelector(getUpdateDialogType);
|
||||||
|
@ -163,8 +167,9 @@ export function SmartPreferences(): JSX.Element | null {
|
||||||
const preferredWidthFromStorage = useSelector(getPreferredLeftPaneWidth);
|
const preferredWidthFromStorage = useSelector(getPreferredLeftPaneWidth);
|
||||||
const theme = useSelector(getTheme);
|
const theme = useSelector(getTheme);
|
||||||
|
|
||||||
const badge = useSelector(getPreferredBadgeSelector)(me.badges);
|
|
||||||
const shouldShowUpdateDialog = dialogType !== DialogType.None;
|
const shouldShowUpdateDialog = dialogType !== DialogType.None;
|
||||||
|
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||||
|
const badge = getPreferredBadge(me.badges);
|
||||||
|
|
||||||
// The weird ones
|
// The weird ones
|
||||||
|
|
||||||
|
@ -676,6 +681,8 @@ export function SmartPreferences(): JSX.Element | null {
|
||||||
return (
|
return (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Preferences
|
<Preferences
|
||||||
|
conversations={conversations}
|
||||||
|
conversationSelector={conversationSelector}
|
||||||
accountEntropyPool={accountEntropyPool}
|
accountEntropyPool={accountEntropyPool}
|
||||||
addCustomColor={addCustomColor}
|
addCustomColor={addCustomColor}
|
||||||
autoDownloadAttachment={autoDownloadAttachment}
|
autoDownloadAttachment={autoDownloadAttachment}
|
||||||
|
@ -706,6 +713,7 @@ export function SmartPreferences(): JSX.Element | null {
|
||||||
getMessageSampleForSchemaVersion={
|
getMessageSampleForSchemaVersion={
|
||||||
DataReader.getMessageSampleForSchemaVersion
|
DataReader.getMessageSampleForSchemaVersion
|
||||||
}
|
}
|
||||||
|
getPreferredBadge={getPreferredBadge}
|
||||||
hasAudioNotifications={hasAudioNotifications}
|
hasAudioNotifications={hasAudioNotifications}
|
||||||
hasAutoConvertEmoji={hasAutoConvertEmoji}
|
hasAutoConvertEmoji={hasAutoConvertEmoji}
|
||||||
hasAutoDownloadUpdate={hasAutoDownloadUpdate}
|
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> {
|
interface Set<T> {
|
||||||
// Needed until TS upgrade
|
// Needed until TS upgrade
|
||||||
difference<U>(other: ReadonlySet<U>): Set<T>;
|
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