Chat Folders: Add "Read all messages" and "Edit" for the "All chats" folder (#5793)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2025-04-23 18:59:04 +02:00
parent 7d84defd09
commit 3167a36d84
9 changed files with 164 additions and 48 deletions

View File

@ -11,7 +11,7 @@
"Month11" = "November";
"Month12" = "December";
"GroupLeaveGroup" = "Leave Group";
"DeleteChatUser" = "Delete chat";
"DeleteChat" = "Delete Chat";
"AccDescrGroup" = "Group";
"AccDescrChannel" = "Channel";
"Nothing" = "Nothing";
@ -85,6 +85,9 @@
"ProfileCopyPhone" = "Copy Phone Number";
"ContextCopySelected" = "Copy Selected Text";
"ContextCopyText" = "Copy Text";
"ContextArchiveCollapse" = "Collapse";
"ContextArchiveExpand" = "Collapse";
"ContextArchiveToMenu" = "Move to Main Menu";
"CallMessageVideoIncomingDeclined" = "Declined Video Call";
"CallMessageVideoOutgoingMissed" = "Canceled Video Call";
"CallMessageVideoIncomingMissed" = "Missed Video Call";
@ -478,8 +481,9 @@
"PasscodeControllerDisableTitle" = "Disable Passcode";
"PasscodeControllerChangeTitle" = "Change Passcode";
"FilterNew" = "New Folder";
"FilterEdit" = "Edit folder";
"FilterDelete" = "Delete folder";
"EditFolder" = "Edit Folder";
"FilterEditFolders" = "Edit Folders";
"FilterMenuDelete" = "Delete Folder";
"FilterShare" = "Share";
"AutoDeleteConfirm" = "Confirm";
"LogOutTitle" = "Log Out";
@ -827,7 +831,7 @@
"EmptyGroupInfoLine3" = "• Public links such as t.me/title";
"EmptyGroupInfoLine4" = "• Admins with different rights";
"Reactions" = "Reactions";
"MarkAllAsRead" = "Mark all as read";
"ChatListMarkAllAsRead" = "Mark All as Read";
"PaymentCardNumber" = "Card Number";
"PaymentCheckoutAcceptRecurrent" = "I accept the *Terms of Service* of {bot}.";
"CheckoutTotalAmount" = "Total";
@ -881,7 +885,7 @@
"ChannelSubscribers" = "Subscribers";
"ChannelBlockedUsers" = "Removed users";
"ChannelDelete" = "Delete Channel";
"LeaveChannel" = "Leave channel";
"ChannelLeave" = "Leave Channel";
"ChannelDeleteAlert" = "Deleting this channel will remove all subscribers and all posts will be lost. Delete the channel anyway?";
"ChannelLeaveAlert" = "Are you sure you want to leave this channel?";
"ChannelCreator" = "Owner";
@ -1178,10 +1182,13 @@
"ConversationViewChannel" = "VIEW CHANNEL";
"Telegram" = "Telegram";
"ChatListFilterAddToFolder" = "Add to folder...";
"UnpinFromTop" = "Unpin from top";
"PinToTop" = "Pin to top";
"MarkAsRead" = "Mark as read";
"MarkAsUnread" = "Mark as unread";
"ChatListUnpinFromTop" = "Unpin from Top";
"ChatListPinToTop" = "Pin to Top";
"ChatListOpenInNewWindow" = "Open in New Window";
"ChatListOpenInNewTab" = "Open in New Tab";
"ChatListContextMaskAsRead" = "Mark as Read";
"ChatListContextMaskAsUnread" = "Mark as Unread";
"ChatListContextAddToFolder" = "Add to Folder";
"Unarchive" = "Unarchive";
"Archive" = "Archive";
"WaitingForNetwork" = "Waiting for network...";

View File

@ -109,7 +109,7 @@ const GiftMenuItems = ({
<>
{hasPinOptions && (
<MenuItem icon={savedGift.isPinned ? 'unpin' : 'pin'} onClick={handleTogglePin}>
{lang(savedGift.isPinned ? 'UnpinFromTop' : 'PinToTop')}
{lang(savedGift.isPinned ? 'ChatListUnpinFromTop' : 'ChatListPinToTop')}
</MenuItem>
)}
<MenuItem icon="link-badge" onClick={handleCopyLink}>

View File

@ -13,7 +13,7 @@ import { formatIntegerCompact } from '../../../util/textFormat';
import renderText from '../../common/helpers/renderText';
import { useFolderManagerForOrderedIds, useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
import useOldLang from '../../../hooks/useOldLang';
import useLang from '../../../hooks/useLang';
import Avatar from '../../common/Avatar';
import Icon from '../../common/icons/Icon';
@ -42,7 +42,7 @@ const Archive: FC<OwnProps> = ({
onClick,
}) => {
const { updateArchiveSettings } = getActions();
const lang = useOldLang();
const lang = useLang();
const orderedChatIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID);
const unreadCounters = useFolderManagerForUnreadCounters();
@ -75,7 +75,7 @@ const Archive: FC<OwnProps> = ({
const contextActions = useMemo(() => {
const actionMinimize = !archiveSettings.isMinimized && {
title: lang('lng_context_archive_collapse'),
title: lang('ContextArchiveCollapse'),
icon: 'collapse',
handler: () => {
updateArchiveSettings({ isMinimized: true });
@ -83,7 +83,7 @@ const Archive: FC<OwnProps> = ({
} satisfies MenuItemContextAction;
const actionExpand = archiveSettings.isMinimized && {
title: lang('lng_context_archive_expand'),
title: lang('ContextArchiveExpand'),
icon: 'expand',
handler: () => {
updateArchiveSettings({ isMinimized: false });
@ -91,7 +91,7 @@ const Archive: FC<OwnProps> = ({
} satisfies MenuItemContextAction;
const actionHide = {
title: lang('lng_context_archive_to_menu'),
title: lang('ContextArchiveToMenu'),
icon: 'archive-to-main',
handler: () => {
updateArchiveSettings({ isHidden: true });

View File

@ -7,9 +7,10 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChatFolder, ApiChatlistExportedInvite, ApiSession } from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
import type { LeftColumnContent, SettingsScreens } from '../../../types';
import type { LeftColumnContent } from '../../../types';
import type { MenuItemContextAction } from '../../ui/ListItem';
import type { TabWithProperties } from '../../ui/TabList';
import { SettingsScreens } from '../../../types';
import { ALL_FOLDER_ID } from '../../../config';
import { selectCanShareFolder, selectTabState } from '../../../global/selectors';
@ -22,7 +23,10 @@ import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import useDerivedState from '../../../hooks/useDerivedState';
import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
import {
useFolderManagerForUnreadChatsByFolder,
useFolderManagerForUnreadCounters,
} from '../../../hooks/useFolderManager';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
@ -90,6 +94,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
openDeleteChatFolderModal,
openEditChatFolder,
openLimitReachedModal,
markChatMessagesRead,
} = getActions();
// eslint-disable-next-line no-null/no-null
@ -112,6 +117,14 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
});
const isStoryRibbonClosing = useDerivedState(getIsStoryRibbonClosing);
const scrollToTop = useLastCallback(() => {
const activeList = ref.current?.querySelector<HTMLElement>('.chat-list.Transition_slide-active');
activeList?.scrollTo({
top: 0,
behavior: 'smooth',
});
});
const allChatsFolder: ApiChatFolder = useMemo(() => {
return {
id: ALL_FOLDER_ID,
@ -137,6 +150,16 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
const isInAllChatsFolder = allChatsFolderIndex === activeChatFolder;
const isInFirstFolder = FIRST_FOLDER_INDEX === activeChatFolder;
const folderUnreadChatsCountersById = useFolderManagerForUnreadChatsByFolder();
const handleReadAllChats = useLastCallback((folderId: number) => {
const unreadChatIds = folderUnreadChatsCountersById[folderId];
if (!unreadChatIds?.length) return;
unreadChatIds.forEach((chatId) => {
markChatMessagesRead({ id: chatId });
});
});
const folderCountersById = useFolderManagerForUnreadCounters();
const folderTabs = useMemo(() => {
if (!displayedFolders || !displayedFolders.length) {
@ -177,17 +200,41 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
});
}
if (id !== ALL_FOLDER_ID) {
if (id === ALL_FOLDER_ID) {
contextActions.push({
title: lang('FilterEdit'),
title: lang('FilterEditFolders'),
icon: 'edit',
handler: () => {
onSettingsScreenSelect(SettingsScreens.Folders);
},
});
if (folderUnreadChatsCountersById[id]?.length) {
contextActions.push({
title: lang('ChatListMarkAllAsRead'),
icon: 'readchats',
handler: () => handleReadAllChats(folder.id),
});
}
} else {
contextActions.push({
title: lang('EditFolder'),
icon: 'edit',
handler: () => {
openEditChatFolder({ folderId: id });
},
});
if (folderUnreadChatsCountersById[id]?.length) {
contextActions.push({
title: lang('ChatListMarkAllAsRead'),
icon: 'readchats',
handler: () => handleReadAllChats(folder.id),
});
}
contextActions.push({
title: lang('FilterDelete'),
title: lang('FilterMenuDelete'),
icon: 'delete',
destructive: true,
handler: () => {
@ -211,11 +258,14 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
});
}, [
displayedFolders, maxFolders, folderCountersById, lang, chatFoldersById, maxChatLists, folderInvitesById,
maxFolderInvites,
maxFolderInvites, folderUnreadChatsCountersById, onSettingsScreenSelect,
]);
const handleSwitchTab = useLastCallback((index: number) => {
setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true });
if (activeChatFolder === index) {
scrollToTop();
}
});
// Prevent `activeTab` pointing at non-existing folder after update

View File

@ -55,7 +55,7 @@
.badge {
min-width: 1.25rem;
height: 1.25rem;
margin-inline-start: 0.5rem;
margin-inline-start: 0.3125rem;
background: var(--color-gray);
border-radius: 0.75rem;
padding: 0 0.3125rem;

View File

@ -11,7 +11,7 @@ import {
} from '../global/helpers';
import { IS_ELECTRON, IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../util/browser/windowEnvironment';
import { compact } from '../util/iteratees';
import useOldLang from './useOldLang';
import useLang from './useLang';
const useChatContextActions = ({
chat,
@ -44,7 +44,7 @@ const useChatContextActions = ({
handleChatFolderChange: NoneToVoidFunction;
handleReport?: NoneToVoidFunction;
}, isInSearch = false) => {
const lang = useOldLang();
const lang = useLang();
const { isSelf } = user || {};
const isServiceNotifications = user?.id === SERVICE_NOTIFICATIONS_USER_ID;
@ -57,7 +57,7 @@ const useChatContextActions = ({
}
if (isUserId(chat.id)) {
return lang('DeleteChatUser');
return lang('DeleteChat');
}
if (getCanDeleteChat(chat)) {
@ -65,10 +65,10 @@ const useChatContextActions = ({
}
if (isChatChannel(chat)) {
return lang('LeaveChannel');
return lang('ChannelLeave');
}
return lang('Group.LeaveGroup');
return lang('GroupLeaveGroup');
}, [chat, isSavedDialog, lang]);
return useMemo(() => {
@ -87,7 +87,7 @@ const useChatContextActions = ({
} = getActions();
const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && {
title: IS_ELECTRON ? 'Open in new window' : 'Open in new tab',
title: IS_ELECTRON ? lang('ChatListOpenInNewWindow') : lang('ChatListOpenInNewTab'),
icon: 'open-in-new-tab',
handler: () => {
if (isSavedDialog) {
@ -108,12 +108,12 @@ const useChatContextActions = ({
const actionPin = isPinned
? {
title: lang('UnpinFromTop'),
title: lang('ChatListUnpinFromTop'),
icon: 'unpin',
handler: togglePinned,
}
: {
title: lang('PinToTop'),
title: lang('ChatListPinToTop'),
icon: 'pin',
handler: togglePinned,
};
@ -130,19 +130,19 @@ const useChatContextActions = ({
}
const actionAddToFolder = canChangeFolder ? {
title: lang('ChatList.Filter.AddToFolder'),
title: lang('ChatListContextAddToFolder'),
icon: 'folder',
handler: handleChatFolderChange,
} : undefined;
const actionMute = isMuted
? {
title: lang('ChatList.Unmute'),
title: lang('ChatsUnmute'),
icon: 'unmute',
handler: () => updateChatMutedState({ chatId: chat.id, isMuted: false }),
}
: {
title: `${lang('ChatList.Mute')}...`,
title: `${lang('ChatsMute')}...`,
icon: 'mute',
handler: handleMute,
};
@ -153,10 +153,13 @@ const useChatContextActions = ({
const actionMaskAsRead = (
chat.unreadCount || chat.hasUnreadMark || Object.values(topics || {}).some(({ unreadCount }) => unreadCount)
) ? { title: lang('MarkAsRead'), icon: 'readchats', handler: () => markChatMessagesRead({ id: chat.id }) }
: undefined;
) ? {
title: lang('ChatListContextMaskAsRead'),
icon: 'readchats',
handler: () => markChatMessagesRead({ id: chat.id }),
} : undefined;
const actionMarkAsUnread = !(chat.unreadCount || chat.hasUnreadMark) && !chat.isForum
? { title: lang('MarkAsUnread'), icon: 'unread', handler: () => markChatUnread({ id: chat.id }) }
? { title: lang('ChatListContextMaskAsUnread'), icon: 'unread', handler: () => markChatUnread({ id: chat.id }) }
: undefined;
const actionArchive = isChatArchived(chat)
@ -165,7 +168,7 @@ const useChatContextActions = ({
const canReport = handleReport && !user && (isChatChannel(chat) || isChatGroup(chat));
const actionReport = canReport
? { title: lang('ReportPeer.Report'), icon: 'flag', handler: handleReport }
? { title: lang('ReportPeerReport'), icon: 'flag', handler: handleReport }
: undefined;
const isInFolder = folderId !== undefined;

View File

@ -2,10 +2,10 @@ import { useEffect } from '../lib/teact/teact';
import {
addChatsCountCallback,
addOrderedIdsCallback,
addOrderedIdsCallback, addUnreadChatsByFolderIdCallback,
addUnreadCountersCallback,
getChatsCount,
getOrderedIds,
getOrderedIds, getUnreadChatsByFolderId,
getUnreadCounters,
} from '../util/folderManager';
import useForceUpdate from './useForceUpdate';
@ -33,3 +33,11 @@ export function useFolderManagerForChatsCount() {
return getChatsCount();
}
export function useFolderManagerForUnreadChatsByFolder() {
const forceUpdate = useForceUpdate();
useEffect(() => addUnreadChatsByFolderIdCallback(forceUpdate), [forceUpdate]);
return getUnreadChatsByFolderId();
}

View File

@ -14,7 +14,7 @@ export interface LangPair {
'Month11': undefined;
'Month12': undefined;
'GroupLeaveGroup': undefined;
'DeleteChatUser': undefined;
'DeleteChat': undefined;
'AccDescrGroup': undefined;
'AccDescrChannel': undefined;
'Nothing': undefined;
@ -67,6 +67,9 @@ export interface LangPair {
'ProfileCopyPhone': undefined;
'ContextCopySelected': undefined;
'ContextCopyText': undefined;
'ContextArchiveCollapse': undefined;
'ContextArchiveExpand': undefined;
'ContextArchiveToMenu': undefined;
'CallMessageVideoIncomingDeclined': undefined;
'CallMessageVideoOutgoingMissed': undefined;
'CallMessageVideoIncomingMissed': undefined;
@ -428,8 +431,9 @@ export interface LangPair {
'PasscodeControllerDisableTitle': undefined;
'PasscodeControllerChangeTitle': undefined;
'FilterNew': undefined;
'FilterEdit': undefined;
'FilterDelete': undefined;
'EditFolder': undefined;
'FilterEditFolders': undefined;
'FilterMenuDelete': undefined;
'FilterShare': undefined;
'AutoDeleteConfirm': undefined;
'LogOutTitle': undefined;
@ -710,7 +714,7 @@ export interface LangPair {
'EmptyGroupInfoLine3': undefined;
'EmptyGroupInfoLine4': undefined;
'Reactions': undefined;
'MarkAllAsRead': undefined;
'ChatListMarkAllAsRead': undefined;
'PaymentCardNumber': undefined;
'CheckoutTotalAmount': undefined;
'PaymentCheckoutMethod': undefined;
@ -761,7 +765,7 @@ export interface LangPair {
'ChannelSubscribers': undefined;
'ChannelBlockedUsers': undefined;
'ChannelDelete': undefined;
'LeaveChannel': undefined;
'ChannelLeave': undefined;
'ChannelDeleteAlert': undefined;
'ChannelLeaveAlert': undefined;
'ChannelCreator': undefined;
@ -976,6 +980,7 @@ export interface LangPair {
'ChatListFilterErrorEmpty': undefined;
'ChatListFilterErrorTitleEmpty': undefined;
'FilterMuted': undefined;
'ReadFolder': undefined;
'FilterRead': undefined;
'FilterArchived': undefined;
'GroupsAndChannelsLimitTitle': undefined;
@ -1008,10 +1013,13 @@ export interface LangPair {
'ConversationViewChannel': undefined;
'Telegram': undefined;
'ChatListFilterAddToFolder': undefined;
'UnpinFromTop': undefined;
'PinToTop': undefined;
'MarkAsRead': undefined;
'MarkAsUnread': undefined;
'ChatListUnpinFromTop': undefined;
'ChatListPinToTop': undefined;
'ChatListOpenInNewWindow': undefined;
'ChatListOpenInNewTab': undefined;
'ChatListContextMaskAsRead': undefined;
'ChatListContextMaskAsUnread': undefined;
'ChatListContextAddToFolder': undefined;
'Unarchive': undefined;
'Archive': undefined;
'WaitingForNetwork': undefined;

View File

@ -97,12 +97,14 @@ let results: {
chatsCount: number;
notificationsCount: number;
} | undefined>;
unreadChatIdsByFolderId: Record<string, string[] | undefined>;
} = initials.results;
let callbacks: {
orderedIdsByFolderId: Record<number, CallbackManager>;
chatsCountByFolderId: CallbackManager;
unreadCountersByFolderId: CallbackManager;
unreadChatIdsByFolderId: CallbackManager;
} = initials.callbacks;
if (DEBUG) {
@ -159,6 +161,12 @@ export function getUnreadCounters() {
return results.unreadCountersByFolderId;
}
export function getUnreadChatsByFolderId() {
if (!inited) init();
return results.unreadChatIdsByFolderId;
}
export function getAllNotificationsCount() {
return getUnreadCounters()[ALL_FOLDER_ID]?.notificationsCount || 0;
}
@ -182,6 +190,12 @@ export function addChatsCountCallback(callback: (chatsCount: typeof results.chat
return callbacks.chatsCountByFolderId.addCallback(callback);
}
export function addUnreadChatsByFolderIdCallback(
callback: (unreadChats: typeof results.unreadChatIdsByFolderId) => void,
) {
return callbacks.unreadChatIdsByFolderId.addCallback(callback);
}
export function addUnreadCountersCallback(callback: (unreadCounters: typeof results.unreadCountersByFolderId) => void) {
return callbacks.unreadCountersByFolderId.addCallback(callback);
}
@ -677,6 +691,7 @@ function updateListsForChat(chatId: string, currentFolderIds: number[], newFolde
function updateResults(affectedFolderIds: number[]) {
let wasUnreadCountersChanged = false;
let wasChatsCountChanged = false;
let wasUnreadChatsChanged = false;
Array.from(affectedFolderIds).forEach((folderId) => {
const { pinnedCount: newPinnedCount, orderedIds: newOrderedIds } = buildFolderOrderedIds(folderId);
@ -715,6 +730,15 @@ function updateResults(affectedFolderIds: number[]) {
);
}
results.unreadCountersByFolderId[folderId] = newUnreadCounters;
const currentUnreadChats = results.unreadChatIdsByFolderId[folderId];
const newUnreadChats = buildFolderByUnreadChats(folderId);
if (!wasUnreadChatsChanged) {
wasUnreadChatsChanged = (
!currentUnreadChats || !areSortedArraysEqual(newUnreadChats, currentUnreadChats)
);
}
results.unreadChatIdsByFolderId[folderId] = newUnreadChats;
});
if (wasChatsCountChanged) {
@ -730,6 +754,10 @@ function updateResults(affectedFolderIds: number[]) {
results.unreadCountersByFolderId = newValue;
callbacks.unreadCountersByFolderId.runCallbacks(newValue);
}
if (wasUnreadChatsChanged) {
callbacks.unreadChatIdsByFolderId.runCallbacks(results.unreadChatIdsByFolderId);
}
}
function buildFolderOrderedIds(folderId: number) {
@ -801,6 +829,16 @@ function buildFolderUnreadCounters(folderId: number) {
});
}
function buildFolderByUnreadChats(folderId: number) {
const { chatSummariesById } = prepared;
const { orderedIdsByFolderId: { [folderId]: orderedIds } } = results;
return orderedIds!.filter((chatId) => {
const chatSummary = chatSummariesById.get(chatId);
return chatSummary?.isUnread;
});
}
function buildInitials() {
return {
prevGlobal: {
@ -823,12 +861,14 @@ function buildInitials() {
pinnedCountByFolderId: {},
chatsCountByFolderId: {},
unreadCountersByFolderId: {},
unreadChatIdsByFolderId: {},
},
callbacks: {
orderedIdsByFolderId: {},
chatsCountByFolderId: createCallbackManager(),
unreadCountersByFolderId: createCallbackManager(),
unreadChatIdsByFolderId: createCallbackManager(),
},
};
}