mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-15 07:28:59 +00:00
Add shortcuts for add/remove chat to chat folder
This commit is contained in:
@@ -439,6 +439,18 @@
|
||||
"messageformat": "Edit folder",
|
||||
"description": "Left Pane > Inbox > Chat List > Chat Folders > Item > Right-Click Context Menu > Open settings for current chat folder"
|
||||
},
|
||||
"icu:LeftPane__ConversationListItem__ContextMenu__ChatFolderToggleChatsItem": {
|
||||
"messageformat": "Add to folder",
|
||||
"description": "Left Pane > Inbox > Chat List > Chat > Right-Click Context Menu > Add to folder (only when in 'All chats')"
|
||||
},
|
||||
"icu:LeftPane__ConversationListItem__ContextMenu__ChatFolderToggleChatsMenu__CreateFolderItem": {
|
||||
"messageformat": "Create folder",
|
||||
"description": "Left Pane > Inbox > Chat List > Chat > Right-Click Context Menu > Add to folder (only when in 'All chats') > Create folder"
|
||||
},
|
||||
"icu:LeftPane__ConversationListItem__ContextMenu__ChatFolderRemoveChatItem": {
|
||||
"messageformat": "Remove from folder",
|
||||
"description": "Left Pane > Inbox > Chat List > Chat > Right-Click Context Menu > Remove from folder (only when in custom chat folder)"
|
||||
},
|
||||
"icu:CountryCodeSelect__placeholder": {
|
||||
"messageformat": "Country code",
|
||||
"description": "Placeholder displayed as default value of country code select element"
|
||||
@@ -888,6 +900,14 @@
|
||||
"messageformat": "Error saving receipt. Please try again.",
|
||||
"description": "Toast message shown when a donation receipt fails to save to disk"
|
||||
},
|
||||
"icu:Toast--ChatFolderAddedChat": {
|
||||
"messageformat": "Added to “{chatFolderName}”",
|
||||
"description": "Toast message show when a chat folder has a new chat added to it (via chats list context menu)"
|
||||
},
|
||||
"icu:Toast--ChatFolderRemovedChat": {
|
||||
"messageformat": "Removed from “{chatFolderName}”",
|
||||
"description": "Toast message show when a chat folder has a chat removed from it (via chats list context menu)"
|
||||
},
|
||||
"icu:Toast--ChatFolderCreated": {
|
||||
"messageformat": "{chatFolderName} folder added",
|
||||
"description": "Toast message show when a chat folder is created (in some places like creating a preset)"
|
||||
|
||||
@@ -5069,7 +5069,8 @@ button.module-calling-participants-list__contact {
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(:disabled, &--disabled, &--is-selected) {
|
||||
&:hover:not(:disabled, &--disabled, &--is-selected),
|
||||
&[data-state='open'] {
|
||||
background-color: light-dark(
|
||||
variables.$color-gray-05,
|
||||
variables.$color-gray-75
|
||||
|
||||
@@ -338,6 +338,9 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||
<LeftPaneConversationListItemContextMenu
|
||||
i18n={i18n}
|
||||
conversation={getDefaultConversation()}
|
||||
selectedChatFolder={null}
|
||||
currentChatFolders={CurrentChatFolders.createEmpty()}
|
||||
isActivelySearching={false}
|
||||
onMarkUnread={action('onMarkUnread')}
|
||||
onMarkRead={action('onMarkRead')}
|
||||
onPin={action('onPin')}
|
||||
@@ -346,6 +349,8 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||
onArchive={action('onArchive')}
|
||||
onUnarchive={action('onUnarchive')}
|
||||
onDelete={action('onDelete')}
|
||||
onChatFolderOpenCreatePage={action('onChatFolderOpenCreatePage')}
|
||||
onChatFolderToggleChat={action('onChatFolderToggleChat')}
|
||||
localDeleteWarningShown={false}
|
||||
setLocalDeleteWarningShown={action('setLocalDeleteWarningShown')}
|
||||
>
|
||||
|
||||
@@ -674,6 +674,7 @@ EditChatFolder.args = {
|
||||
settingsLocation: {
|
||||
page: SettingsPage.EditChatFolder,
|
||||
chatFolderId: null,
|
||||
initChatFolderParams: null,
|
||||
previousLocation: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -561,6 +561,7 @@ export function Preferences({
|
||||
setSettingsLocation({
|
||||
page: SettingsPage.EditChatFolder,
|
||||
chatFolderId,
|
||||
initChatFolderParams: null,
|
||||
previousLocation: null,
|
||||
});
|
||||
},
|
||||
@@ -2017,6 +2018,7 @@ export function Preferences({
|
||||
previousLocation: settingsLocation.previousLocation,
|
||||
settingsPaneRef,
|
||||
existingChatFolderId: settingsLocation.chatFolderId,
|
||||
initChatFolderParams: settingsLocation.initChatFolderParams,
|
||||
});
|
||||
} else if (settingsLocation.page === SettingsPage.PNP) {
|
||||
let sharingDescription: string;
|
||||
|
||||
@@ -61,6 +61,16 @@ function getToast(toastType: ToastType): AnyToast {
|
||||
return { toastType: ToastType.CaptchaFailed };
|
||||
case ToastType.CaptchaSolved:
|
||||
return { toastType: ToastType.CaptchaSolved };
|
||||
case ToastType.ChatFolderAddedChat:
|
||||
return {
|
||||
toastType: ToastType.ChatFolderCreated,
|
||||
parameters: { chatFolderName: 'Friends' },
|
||||
};
|
||||
case ToastType.ChatFolderRemovedChat:
|
||||
return {
|
||||
toastType: ToastType.ChatFolderCreated,
|
||||
parameters: { chatFolderName: 'Friends' },
|
||||
};
|
||||
case ToastType.ChatFolderCreated:
|
||||
return {
|
||||
toastType: ToastType.ChatFolderCreated,
|
||||
|
||||
@@ -189,6 +189,34 @@ export function renderToast({
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ChatFolderAddedChat) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:Toast--ChatFolderAddedChat"
|
||||
components={{
|
||||
chatFolderName: <UserText text={toast.parameters.chatFolderName} />,
|
||||
}}
|
||||
/>
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ChatFolderRemovedChat) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:Toast--ChatFolderRemovedChat"
|
||||
components={{
|
||||
chatFolderName: <UserText text={toast.parameters.chatFolderName} />,
|
||||
}}
|
||||
/>
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ChatFolderCreated) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
|
||||
@@ -15,6 +15,20 @@ import { isAlpha } from '../../util/version.std.js';
|
||||
import { drop } from '../../util/drop.std.js';
|
||||
import { DeleteMessagesConfirmationDialog } from '../DeleteMessagesConfirmationDialog.dom.js';
|
||||
import { getMuteOptions } from '../../util/getMuteOptions.std.js';
|
||||
import {
|
||||
CHAT_FOLDER_DEFAULTS,
|
||||
ChatFolderType,
|
||||
isConversationInChatFolder,
|
||||
} from '../../types/ChatFolder.std.js';
|
||||
import type {
|
||||
ChatFolderParams,
|
||||
ChatFolder,
|
||||
ChatFolderId,
|
||||
} from '../../types/ChatFolder.std.js';
|
||||
import { CurrentChatFolders } from '../../types/CurrentChatFolders.std.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import { UserText } from '../UserText.dom.js';
|
||||
import { isConversationMuted } from '../../util/isConversationMuted.std.js';
|
||||
|
||||
function isEnabled() {
|
||||
const env = getEnvironment();
|
||||
@@ -38,9 +52,18 @@ function isEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export type ChatFolderToggleChat = (
|
||||
chatFolderId: ChatFolderId,
|
||||
conversationId: string,
|
||||
toggle: boolean
|
||||
) => void;
|
||||
|
||||
export type LeftPaneConversationListItemContextMenuProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
conversation: ConversationType;
|
||||
selectedChatFolder: ChatFolder | null;
|
||||
currentChatFolders: CurrentChatFolders;
|
||||
isActivelySearching: boolean;
|
||||
onMarkUnread: (conversationId: string) => void;
|
||||
onMarkRead: (conversationId: string) => void;
|
||||
onPin: (conversationId: string) => void;
|
||||
@@ -49,6 +72,8 @@ export type LeftPaneConversationListItemContextMenuProps = Readonly<{
|
||||
onArchive: (conversationId: string) => void;
|
||||
onUnarchive: (conversationId: string) => void;
|
||||
onDelete: (conversationId: string) => void;
|
||||
onChatFolderOpenCreatePage: (initChatFolderParams: ChatFolderParams) => void;
|
||||
onChatFolderToggleChat: ChatFolderToggleChat;
|
||||
localDeleteWarningShown: boolean;
|
||||
setLocalDeleteWarningShown: () => void;
|
||||
children: ReactNode;
|
||||
@@ -59,6 +84,7 @@ export const LeftPaneConversationListItemContextMenu: FC<LeftPaneConversationLis
|
||||
const {
|
||||
i18n,
|
||||
conversation,
|
||||
selectedChatFolder,
|
||||
onMarkUnread,
|
||||
onMarkRead,
|
||||
onPin,
|
||||
@@ -67,9 +93,19 @@ export const LeftPaneConversationListItemContextMenu: FC<LeftPaneConversationLis
|
||||
onArchive,
|
||||
onUnarchive,
|
||||
onDelete,
|
||||
onChatFolderOpenCreatePage,
|
||||
onChatFolderToggleChat,
|
||||
} = props;
|
||||
|
||||
const { id: conversationId, muteExpiresAt } = conversation;
|
||||
const selectedChatFolderId = selectedChatFolder?.id ?? null;
|
||||
|
||||
const isSelectedChatFolderAllChats = useMemo(() => {
|
||||
return (
|
||||
selectedChatFolder == null ||
|
||||
selectedChatFolder.folderType === ChatFolderType.ALL
|
||||
);
|
||||
}, [selectedChatFolder]);
|
||||
|
||||
const muteOptions = useMemo(() => {
|
||||
return getMuteOptions(muteExpiresAt, i18n);
|
||||
@@ -125,6 +161,21 @@ export const LeftPaneConversationListItemContextMenu: FC<LeftPaneConversationLis
|
||||
onDelete(conversationId);
|
||||
}, [onDelete, conversationId]);
|
||||
|
||||
const handleChatFolderCreateNew = useCallback(() => {
|
||||
onChatFolderOpenCreatePage({
|
||||
...CHAT_FOLDER_DEFAULTS,
|
||||
includedConversationIds: [conversationId],
|
||||
});
|
||||
}, [onChatFolderOpenCreatePage, conversationId]);
|
||||
|
||||
const handleChatFolderRemoveChat = useCallback(() => {
|
||||
strictAssert(
|
||||
selectedChatFolderId != null,
|
||||
'Missing selectedChatFolderId'
|
||||
);
|
||||
onChatFolderToggleChat(selectedChatFolderId, conversationId, false);
|
||||
}, [onChatFolderToggleChat, selectedChatFolderId, conversationId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AxoContextMenu.Root>
|
||||
@@ -175,6 +226,42 @@ export const LeftPaneConversationListItemContextMenu: FC<LeftPaneConversationLis
|
||||
})}
|
||||
</AxoContextMenu.SubContent>
|
||||
</AxoContextMenu.Sub>
|
||||
{!props.isActivelySearching &&
|
||||
isSelectedChatFolderAllChats &&
|
||||
props.currentChatFolders.hasAnyCurrentCustomChatFolders && (
|
||||
<AxoContextMenu.Sub>
|
||||
<AxoContextMenu.SubTrigger symbol="folder">
|
||||
{i18n(
|
||||
'icu:LeftPane__ConversationListItem__ContextMenu__ChatFolderToggleChatsItem'
|
||||
)}
|
||||
</AxoContextMenu.SubTrigger>
|
||||
<AxoContextMenu.SubContent>
|
||||
<ContextMenuChatFolderToggleChatsSubMenu
|
||||
currentChatFolders={props.currentChatFolders}
|
||||
conversation={conversation}
|
||||
onChatFolderToggleChat={props.onChatFolderToggleChat}
|
||||
/>
|
||||
<AxoContextMenu.Item
|
||||
symbol="plus"
|
||||
onSelect={handleChatFolderCreateNew}
|
||||
>
|
||||
{i18n(
|
||||
'icu:LeftPane__ConversationListItem__ContextMenu__ChatFolderToggleChatsMenu__CreateFolderItem'
|
||||
)}
|
||||
</AxoContextMenu.Item>
|
||||
</AxoContextMenu.SubContent>
|
||||
</AxoContextMenu.Sub>
|
||||
)}
|
||||
{!props.isActivelySearching && !isSelectedChatFolderAllChats && (
|
||||
<AxoContextMenu.Item
|
||||
symbol="folder"
|
||||
onSelect={handleChatFolderRemoveChat}
|
||||
>
|
||||
{i18n(
|
||||
'icu:LeftPane__ConversationListItem__ContextMenu__ChatFolderRemoveChatItem'
|
||||
)}
|
||||
</AxoContextMenu.Item>
|
||||
)}
|
||||
{!conversation.isArchived && (
|
||||
<AxoContextMenu.Item symbol="archive" onSelect={handleArchive}>
|
||||
{i18n('icu:archiveConversation')}
|
||||
@@ -273,3 +360,73 @@ function ContextMenuCopyTextItem(props: {
|
||||
</AxoContextMenu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuChatFolderToggleChatsSubMenu(props: {
|
||||
currentChatFolders: CurrentChatFolders;
|
||||
conversation: ConversationType;
|
||||
onChatFolderToggleChat: ChatFolderToggleChat;
|
||||
}) {
|
||||
const { currentChatFolders } = props;
|
||||
|
||||
const sortedAndFilteredChatFolders = useMemo(() => {
|
||||
return CurrentChatFolders.toSortedArray(currentChatFolders).filter(
|
||||
chatFolder => {
|
||||
return chatFolder.folderType === ChatFolderType.CUSTOM;
|
||||
}
|
||||
);
|
||||
}, [currentChatFolders]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{sortedAndFilteredChatFolders.map(chatFolder => {
|
||||
return (
|
||||
<ContextMenuChatFolderToggleChatItem
|
||||
key={chatFolder.id}
|
||||
chatFolder={chatFolder}
|
||||
conversation={props.conversation}
|
||||
onChatFolderToggleChat={props.onChatFolderToggleChat}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuChatFolderToggleChatItem(props: {
|
||||
chatFolder: ChatFolder;
|
||||
conversation: ConversationType;
|
||||
onChatFolderToggleChat: ChatFolderToggleChat;
|
||||
}) {
|
||||
const { chatFolder, conversation, onChatFolderToggleChat } = props;
|
||||
const chatFolderId = chatFolder.id;
|
||||
const conversationId = conversation.id;
|
||||
|
||||
const checked = useMemo(() => {
|
||||
return isConversationInChatFolder(chatFolder, conversation, {
|
||||
ignoreShowOnlyUnread: true,
|
||||
ignoreShowMutedChats: true,
|
||||
});
|
||||
}, [chatFolder, conversation]);
|
||||
|
||||
const isExcludedByMute = useMemo(() => {
|
||||
return !chatFolder.showMutedChats && isConversationMuted(conversation);
|
||||
}, [chatFolder, conversation]);
|
||||
|
||||
const handleCheckedChange = useCallback(
|
||||
(value: boolean) => {
|
||||
onChatFolderToggleChat(chatFolderId, conversationId, value);
|
||||
},
|
||||
[onChatFolderToggleChat, chatFolderId, conversationId]
|
||||
);
|
||||
|
||||
return (
|
||||
<AxoContextMenu.CheckboxItem
|
||||
symbol="folder"
|
||||
checked={checked}
|
||||
disabled={checked || isExcludedByMute}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
>
|
||||
<UserText text={chatFolder.name} />
|
||||
</AxoContextMenu.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,6 +172,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
||||
details: {
|
||||
page: SettingsPage.EditChatFolder,
|
||||
chatFolderId: selectedChatFolder.id,
|
||||
initChatFolderParams: null,
|
||||
previousLocation: {
|
||||
tab: NavTab.Chats,
|
||||
},
|
||||
|
||||
@@ -1269,6 +1269,11 @@ type WritableInterface = {
|
||||
createAllChatsChatFolder: () => ChatFolder;
|
||||
upsertAllChatsChatFolderFromSync: (chatFolder: ChatFolder) => void;
|
||||
updateChatFolder: (chatFolder: ChatFolder) => void;
|
||||
updateChatFolderToggleChat: (
|
||||
chatFolderId: ChatFolderId,
|
||||
conversationId: string,
|
||||
toggle: boolean
|
||||
) => void;
|
||||
updateChatFolderPositions: (chatFolders: ReadonlyArray<ChatFolder>) => void;
|
||||
updateChatFolderDeletedAtTimestampMsFromSync: (
|
||||
chatFolderId: ChatFolderId,
|
||||
|
||||
@@ -246,6 +246,7 @@ import {
|
||||
createAllChatsChatFolder,
|
||||
upsertAllChatsChatFolderFromSync,
|
||||
updateChatFolder,
|
||||
updateChatFolderToggleChat,
|
||||
markChatFolderDeleted,
|
||||
getOldestDeletedChatFolder,
|
||||
updateChatFolderPositions,
|
||||
@@ -710,10 +711,11 @@ export const DataWriter: ServerWritableInterface = {
|
||||
createAllChatsChatFolder,
|
||||
upsertAllChatsChatFolderFromSync,
|
||||
updateChatFolder,
|
||||
markChatFolderDeleted,
|
||||
deleteExpiredChatFolders,
|
||||
updateChatFolderToggleChat,
|
||||
updateChatFolderPositions,
|
||||
updateChatFolderDeletedAtTimestampMsFromSync,
|
||||
markChatFolderDeleted,
|
||||
deleteExpiredChatFolders,
|
||||
|
||||
removeAll,
|
||||
removeAllConfiguration,
|
||||
@@ -8047,7 +8049,7 @@ function eraseStorageServiceState(db: WritableDB): void {
|
||||
storageID = null,
|
||||
storageVersion = null,
|
||||
storageUnknownFields = null,
|
||||
storageNeedsSync = 0;
|
||||
storageNeedsSync = 0;
|
||||
|
||||
-- Notification Profiles
|
||||
UPDATE notificationProfiles
|
||||
|
||||
@@ -96,19 +96,17 @@ export function getCurrentChatFolders(
|
||||
|
||||
export function getChatFolder(
|
||||
db: ReadableDB,
|
||||
id: ChatFolderId
|
||||
chatFolderId: ChatFolderId
|
||||
): ChatFolder | null {
|
||||
return db.transaction(() => {
|
||||
const [query, params] = sql`
|
||||
SELECT * FROM chatFolders
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
const row = db.prepare(query).get<ChatFolderRow>(params);
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
return rowToChatFolder(row);
|
||||
})();
|
||||
const [query, params] = sql`
|
||||
SELECT * FROM chatFolders
|
||||
WHERE id = ${chatFolderId};
|
||||
`;
|
||||
const row = db.prepare(query).get<ChatFolderRow>(params);
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
return rowToChatFolder(row);
|
||||
}
|
||||
|
||||
function _insertChatFolder(db: WritableDB, chatFolder: ChatFolder): void {
|
||||
@@ -224,30 +222,63 @@ export function upsertAllChatsChatFolderFromSync(
|
||||
}
|
||||
|
||||
export function updateChatFolder(db: WritableDB, chatFolder: ChatFolder): void {
|
||||
const chatFolderRow = chatFolderToRow(chatFolder);
|
||||
const [chatFolderQuery, chatFolderParams] = sql`
|
||||
UPDATE chatFolders
|
||||
SET
|
||||
id = ${chatFolderRow.id},
|
||||
folderType = ${chatFolderRow.folderType},
|
||||
name = ${chatFolderRow.name},
|
||||
position = ${chatFolderRow.position},
|
||||
showOnlyUnread = ${chatFolderRow.showOnlyUnread},
|
||||
showMutedChats = ${chatFolderRow.showMutedChats},
|
||||
includeAllIndividualChats = ${chatFolderRow.includeAllIndividualChats},
|
||||
includeAllGroupChats = ${chatFolderRow.includeAllGroupChats},
|
||||
includedConversationIds = ${chatFolderRow.includedConversationIds},
|
||||
excludedConversationIds = ${chatFolderRow.excludedConversationIds},
|
||||
deletedAtTimestampMs = ${chatFolderRow.deletedAtTimestampMs},
|
||||
storageID = ${chatFolderRow.storageID},
|
||||
storageVersion = ${chatFolderRow.storageVersion},
|
||||
storageUnknownFields = ${chatFolderRow.storageUnknownFields},
|
||||
storageNeedsSync = ${chatFolderRow.storageNeedsSync}
|
||||
WHERE
|
||||
id = ${chatFolderRow.id}
|
||||
`;
|
||||
db.prepare(chatFolderQuery).run(chatFolderParams);
|
||||
}
|
||||
|
||||
export function updateChatFolderToggleChat(
|
||||
db: WritableDB,
|
||||
chatFolderId: ChatFolderId,
|
||||
conversationId: string,
|
||||
toggle: boolean
|
||||
): void {
|
||||
return db.transaction(() => {
|
||||
const chatFolderRow = chatFolderToRow(chatFolder);
|
||||
const [chatFolderQuery, chatFolderParams] = sql`
|
||||
UPDATE chatFolders
|
||||
SET
|
||||
id = ${chatFolderRow.id},
|
||||
folderType = ${chatFolderRow.folderType},
|
||||
name = ${chatFolderRow.name},
|
||||
position = ${chatFolderRow.position},
|
||||
showOnlyUnread = ${chatFolderRow.showOnlyUnread},
|
||||
showMutedChats = ${chatFolderRow.showMutedChats},
|
||||
includeAllIndividualChats = ${chatFolderRow.includeAllIndividualChats},
|
||||
includeAllGroupChats = ${chatFolderRow.includeAllGroupChats},
|
||||
includedConversationIds = ${chatFolderRow.includedConversationIds},
|
||||
excludedConversationIds = ${chatFolderRow.excludedConversationIds},
|
||||
deletedAtTimestampMs = ${chatFolderRow.deletedAtTimestampMs},
|
||||
storageID = ${chatFolderRow.storageID},
|
||||
storageVersion = ${chatFolderRow.storageVersion},
|
||||
storageUnknownFields = ${chatFolderRow.storageUnknownFields},
|
||||
storageNeedsSync = ${chatFolderRow.storageNeedsSync}
|
||||
WHERE
|
||||
id = ${chatFolderRow.id}
|
||||
`;
|
||||
db.prepare(chatFolderQuery).run(chatFolderParams);
|
||||
const chatFolder = getChatFolder(db, chatFolderId);
|
||||
strictAssert(
|
||||
chatFolder != null,
|
||||
`Missing chat folder for id: ${chatFolderId}`
|
||||
);
|
||||
|
||||
const included = new Set<string>(chatFolder.includedConversationIds);
|
||||
const excluded = new Set<string>(chatFolder.excludedConversationIds);
|
||||
|
||||
if (toggle) {
|
||||
// add
|
||||
included.add(conversationId);
|
||||
excluded.delete(conversationId);
|
||||
} else {
|
||||
// remove
|
||||
included.delete(conversationId);
|
||||
excluded.add(conversationId);
|
||||
}
|
||||
|
||||
updateChatFolder(db, {
|
||||
...chatFolder,
|
||||
includedConversationIds: Array.from(included),
|
||||
excludedConversationIds: Array.from(excluded),
|
||||
storageNeedsSync: true,
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -207,6 +207,41 @@ function updateSelectedChangeFolderId(
|
||||
};
|
||||
}
|
||||
|
||||
function updateChatFolderToggleChat(
|
||||
chatFolderId: ChatFolderId,
|
||||
conversationId: string,
|
||||
toggle: boolean,
|
||||
showToastOnSuccess: boolean
|
||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const currentChatFolders = getCurrentChatFolders(getState());
|
||||
const chatFolder = CurrentChatFolders.expect(
|
||||
currentChatFolders,
|
||||
chatFolderId,
|
||||
'updateChatFolderToggleChat'
|
||||
);
|
||||
|
||||
await DataWriter.updateChatFolderToggleChat(
|
||||
chatFolderId,
|
||||
conversationId,
|
||||
toggle
|
||||
);
|
||||
storageServiceUploadJob({ reason: 'toggleChatFolderChat' });
|
||||
dispatch(_refetchChatFolders());
|
||||
|
||||
if (showToastOnSuccess) {
|
||||
dispatch(
|
||||
showToast({
|
||||
toastType: toggle
|
||||
? ToastType.ChatFolderAddedChat
|
||||
: ToastType.ChatFolderRemovedChat,
|
||||
parameters: { chatFolderName: chatFolder.name },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
refetchChatFolders,
|
||||
createChatFolder,
|
||||
@@ -215,6 +250,7 @@ export const actions = {
|
||||
deleteChatFolder,
|
||||
updateChatFoldersPositions,
|
||||
updateSelectedChangeFolderId,
|
||||
updateChatFolderToggleChat,
|
||||
};
|
||||
|
||||
export const useChatFolderActions = (): BoundActionCreatorsMapObject<
|
||||
|
||||
@@ -45,6 +45,7 @@ export const SmartLeftPaneChatFolders = memo(
|
||||
details: {
|
||||
page: SettingsPage.EditChatFolder,
|
||||
chatFolderId,
|
||||
initChatFolderParams: null,
|
||||
previousLocation: location,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,18 +6,34 @@ import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getIntl } from '../selectors/user.std.js';
|
||||
import { getConversationByIdSelector } from '../selectors/conversations.dom.js';
|
||||
import type { ChatFolderToggleChat } from '../../components/leftPane/LeftPaneConversationListItemContextMenu.dom.js';
|
||||
import { LeftPaneConversationListItemContextMenu } from '../../components/leftPane/LeftPaneConversationListItemContextMenu.dom.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import type { RenderConversationListItemContextMenuProps } from '../../components/conversationList/BaseConversationListItem.dom.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.js';
|
||||
import { getLocalDeleteWarningShown } from '../selectors/items.dom.js';
|
||||
import { useItemsActions } from '../ducks/items.preload.js';
|
||||
import {
|
||||
getCurrentChatFolders,
|
||||
getSelectedChatFolder,
|
||||
} from '../selectors/chatFolders.std.js';
|
||||
import { useChatFolderActions } from '../ducks/chatFolders.preload.js';
|
||||
import { useNavActions } from '../ducks/nav.std.js';
|
||||
import { NavTab, SettingsPage } from '../../types/Nav.std.js';
|
||||
import type { ChatFolderParams } from '../../types/ChatFolder.std.js';
|
||||
import { getSelectedLocation } from '../selectors/nav.preload.js';
|
||||
import { getIsActivelySearching } from '../selectors/search.dom.js';
|
||||
|
||||
export const SmartLeftPaneConversationListItemContextMenu: FC<RenderConversationListItemContextMenuProps> =
|
||||
memo(function SmartLeftPaneConversationListItemContextMenu(props) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const conversationByIdSelector = useSelector(getConversationByIdSelector);
|
||||
const localDeleteWarningShown = useSelector(getLocalDeleteWarningShown);
|
||||
const location = useSelector(getSelectedLocation);
|
||||
const isActivelySearching = useSelector(getIsActivelySearching);
|
||||
const selectedChatFolder = useSelector(getSelectedChatFolder);
|
||||
const currentChatFolders = useSelector(getCurrentChatFolders);
|
||||
|
||||
const {
|
||||
onMarkUnread,
|
||||
markConversationRead,
|
||||
@@ -27,7 +43,9 @@ export const SmartLeftPaneConversationListItemContextMenu: FC<RenderConversation
|
||||
deleteConversation,
|
||||
setMuteExpiration,
|
||||
} = useConversationsActions();
|
||||
const { updateChatFolderToggleChat } = useChatFolderActions();
|
||||
const { putItem } = useItemsActions();
|
||||
const { changeLocation } = useNavActions();
|
||||
|
||||
const setLocalDeleteWarningShown = useCallback(() => {
|
||||
putItem('localDeleteWarningShown', true);
|
||||
@@ -50,10 +68,35 @@ export const SmartLeftPaneConversationListItemContextMenu: FC<RenderConversation
|
||||
[setPinned]
|
||||
);
|
||||
|
||||
const handleChatFolderOpenCreatePage = useCallback(
|
||||
(initChatFolderParams: ChatFolderParams) => {
|
||||
changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: SettingsPage.EditChatFolder,
|
||||
chatFolderId: null,
|
||||
initChatFolderParams,
|
||||
previousLocation: location,
|
||||
},
|
||||
});
|
||||
},
|
||||
[changeLocation, location]
|
||||
);
|
||||
|
||||
const handleChatFolderToggleChat: ChatFolderToggleChat = useCallback(
|
||||
(chatFolderId, conversationId, toggle) => {
|
||||
updateChatFolderToggleChat(chatFolderId, conversationId, toggle, true);
|
||||
},
|
||||
[updateChatFolderToggleChat]
|
||||
);
|
||||
|
||||
return (
|
||||
<LeftPaneConversationListItemContextMenu
|
||||
i18n={i18n}
|
||||
conversation={conversation}
|
||||
selectedChatFolder={selectedChatFolder}
|
||||
currentChatFolders={currentChatFolders}
|
||||
isActivelySearching={isActivelySearching}
|
||||
onMarkUnread={onMarkUnread}
|
||||
onMarkRead={markConversationRead}
|
||||
onPin={handlePin}
|
||||
@@ -62,6 +105,8 @@ export const SmartLeftPaneConversationListItemContextMenu: FC<RenderConversation
|
||||
onArchive={onArchive}
|
||||
onUnarchive={onMoveToInbox}
|
||||
onDelete={deleteConversation}
|
||||
onChatFolderOpenCreatePage={handleChatFolderOpenCreatePage}
|
||||
onChatFolderToggleChat={handleChatFolderToggleChat}
|
||||
localDeleteWarningShown={localDeleteWarningShown}
|
||||
setLocalDeleteWarningShown={setLocalDeleteWarningShown}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useSelector } from 'react-redux';
|
||||
import type { PreferencesEditChatFolderPageProps } from '../../components/preferences/chatFolders/PreferencesEditChatFoldersPage.dom.js';
|
||||
import { PreferencesEditChatFolderPage } from '../../components/preferences/chatFolders/PreferencesEditChatFoldersPage.dom.js';
|
||||
import { getIntl, getTheme } from '../selectors/user.std.js';
|
||||
import type { ChatFolderParams } from '../../types/ChatFolder.std.js';
|
||||
import { CHAT_FOLDER_DEFAULTS } from '../../types/ChatFolder.std.js';
|
||||
import {
|
||||
getAllComposableConversations,
|
||||
@@ -20,13 +21,14 @@ import { CurrentChatFolders } from '../../types/CurrentChatFolders.std.js';
|
||||
export type SmartPreferencesEditChatFolderPageProps = Readonly<{
|
||||
previousLocation: Location | null;
|
||||
existingChatFolderId: PreferencesEditChatFolderPageProps['existingChatFolderId'];
|
||||
initChatFolderParams: ChatFolderParams | null;
|
||||
settingsPaneRef: PreferencesEditChatFolderPageProps['settingsPaneRef'];
|
||||
}>;
|
||||
|
||||
export function SmartPreferencesEditChatFolderPage(
|
||||
props: SmartPreferencesEditChatFolderPageProps
|
||||
): JSX.Element {
|
||||
const { existingChatFolderId } = props;
|
||||
const { existingChatFolderId, initChatFolderParams } = props;
|
||||
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
@@ -38,23 +40,23 @@ export function SmartPreferencesEditChatFolderPage(
|
||||
useChatFolderActions();
|
||||
const { changeLocation } = useNavActions();
|
||||
|
||||
const initChatFolderParams = useMemo(() => {
|
||||
const initChatFolderParamsOrCurrentChatFolder = useMemo(() => {
|
||||
if (existingChatFolderId == null) {
|
||||
return CHAT_FOLDER_DEFAULTS;
|
||||
return initChatFolderParams ?? CHAT_FOLDER_DEFAULTS;
|
||||
}
|
||||
return CurrentChatFolders.expect(
|
||||
currentChatFolders,
|
||||
existingChatFolderId,
|
||||
'initChatFolderParams'
|
||||
);
|
||||
}, [currentChatFolders, existingChatFolderId]);
|
||||
}, [currentChatFolders, existingChatFolderId, initChatFolderParams]);
|
||||
|
||||
return (
|
||||
<PreferencesEditChatFolderPage
|
||||
i18n={i18n}
|
||||
previousLocation={props.previousLocation}
|
||||
existingChatFolderId={props.existingChatFolderId}
|
||||
initChatFolderParams={initChatFolderParams}
|
||||
initChatFolderParams={initChatFolderParamsOrCurrentChatFolder}
|
||||
changeLocation={changeLocation}
|
||||
conversations={conversations}
|
||||
preferredBadgeSelector={preferredBadgeSelector}
|
||||
|
||||
@@ -153,6 +153,11 @@ type ConversationPropsForChatFolder = Pick<
|
||||
'type' | 'id' | 'unreadCount' | 'markedUnread' | 'muteExpiresAt'
|
||||
>;
|
||||
|
||||
export type ChatFolderConversationFilterOptions = Readonly<{
|
||||
ignoreShowOnlyUnread?: boolean;
|
||||
ignoreShowMutedChats?: boolean;
|
||||
}>;
|
||||
|
||||
function _isConversationIncludedInChatFolder(
|
||||
chatFolder: ChatFolder,
|
||||
conversation: ConversationPropsForChatFolder
|
||||
@@ -171,13 +176,18 @@ function _isConversationIncludedInChatFolder(
|
||||
|
||||
function _isConversationExcludedFromChatFolder(
|
||||
chatFolder: ChatFolder,
|
||||
conversation: ConversationPropsForChatFolder
|
||||
conversation: ConversationPropsForChatFolder,
|
||||
options: ChatFolderConversationFilterOptions
|
||||
): boolean {
|
||||
if (chatFolder.showOnlyUnread && !isConversationUnread(conversation)) {
|
||||
return true; // not unread, only showing unread
|
||||
if (!options.ignoreShowOnlyUnread) {
|
||||
if (chatFolder.showOnlyUnread && !isConversationUnread(conversation)) {
|
||||
return true; // not unread, only showing unread
|
||||
}
|
||||
}
|
||||
if (!chatFolder.showMutedChats && (conversation.muteExpiresAt ?? 0) > 0) {
|
||||
return true; // muted, not showing muted chats
|
||||
if (!options.ignoreShowMutedChats) {
|
||||
if (!chatFolder.showMutedChats && (conversation.muteExpiresAt ?? 0) > 0) {
|
||||
return true; // muted, not showing muted chats
|
||||
}
|
||||
}
|
||||
if (chatFolder.excludedConversationIds.includes(conversation.id)) {
|
||||
return true; // is excluded by id
|
||||
@@ -187,7 +197,8 @@ function _isConversationExcludedFromChatFolder(
|
||||
|
||||
export function isConversationInChatFolder(
|
||||
chatFolder: ChatFolder,
|
||||
conversation: ConversationPropsForChatFolder
|
||||
conversation: ConversationPropsForChatFolder,
|
||||
options: ChatFolderConversationFilterOptions = {}
|
||||
): boolean {
|
||||
if (chatFolder.folderType === ChatFolderType.ALL) {
|
||||
return true;
|
||||
@@ -195,6 +206,6 @@ export function isConversationInChatFolder(
|
||||
|
||||
return (
|
||||
_isConversationIncludedInChatFolder(chatFolder, conversation) &&
|
||||
!_isConversationExcludedFromChatFolder(chatFolder, conversation)
|
||||
!_isConversationExcludedFromChatFolder(chatFolder, conversation, options)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { ChatFolderId } from './ChatFolder.std.js';
|
||||
import type { ChatFolderId, ChatFolderParams } from './ChatFolder.std.js';
|
||||
|
||||
export type SettingsLocation = ReadonlyDeep<
|
||||
| {
|
||||
@@ -16,6 +16,7 @@ export type SettingsLocation = ReadonlyDeep<
|
||||
| {
|
||||
page: SettingsPage.EditChatFolder;
|
||||
chatFolderId: ChatFolderId | null;
|
||||
initChatFolderParams: ChatFolderParams | null;
|
||||
previousLocation: Location | null;
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -20,6 +20,8 @@ export enum ToastType {
|
||||
CannotOpenGiftBadgeOutgoing = 'CannotOpenGiftBadgeOutgoing',
|
||||
CannotStartGroupCall = 'CannotStartGroupCall',
|
||||
ChatFolderCreated = 'ChatFolderCreated',
|
||||
ChatFolderAddedChat = 'ChatFolderAddedChat',
|
||||
ChatFolderRemovedChat = 'ChatFolderRemovedChat',
|
||||
ConversationArchived = 'ConversationArchived',
|
||||
ConversationMarkedUnread = 'ConversationMarkedUnread',
|
||||
ConversationRemoved = 'ConversationRemoved',
|
||||
@@ -118,6 +120,14 @@ export type AnyToast =
|
||||
| { toastType: ToastType.CannotStartGroupCall }
|
||||
| { toastType: ToastType.CaptchaFailed }
|
||||
| { toastType: ToastType.CaptchaSolved }
|
||||
| {
|
||||
toastType: ToastType.ChatFolderAddedChat;
|
||||
parameters: { chatFolderName: string };
|
||||
}
|
||||
| {
|
||||
toastType: ToastType.ChatFolderRemovedChat;
|
||||
parameters: { chatFolderName: string };
|
||||
}
|
||||
| {
|
||||
toastType: ToastType.ChatFolderCreated;
|
||||
parameters: { chatFolderName: string };
|
||||
|
||||
Reference in New Issue
Block a user