From 6d6249487ef0927192922e1d559f36b1ea2a0a83 Mon Sep 17 00:00:00 2001 From: Shahaf <10106174+kotevcode@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:16:31 +0300 Subject: [PATCH] Chat: Implement folder badges (#444) --- src/api/gramjs/apiBuilders/chats.ts | 2 + src/api/gramjs/gramjsBuilders/index.ts | 3 + src/api/gramjs/methods/chats.ts | 9 +- src/api/types/chats.ts | 1 + src/assets/localization/fallback.strings | 4 + .../common/ChatForumLastMessage.tsx | 98 +++++++++++-------- src/components/left/main/Chat.scss | 12 +++ src/components/left/main/Chat.tsx | 41 +++++++- src/components/left/main/ChatFolders.tsx | 1 + src/components/left/main/ChatList.tsx | 3 + src/components/left/main/ChatTags.module.scss | 33 +++++++ src/components/left/main/ChatTags.tsx | 74 ++++++++++++++ .../left/main/hooks/useChatListEntry.tsx | 3 + .../settings/folders/SettingsFolders.scss | 79 +++++++++++++++ .../settings/folders/SettingsFoldersEdit.tsx | 80 ++++++++++++++- .../settings/folders/SettingsFoldersMain.tsx | 42 +++++++- src/global/actions/api/chats.ts | 18 ++++ src/global/initialState.ts | 1 + src/global/types/actions.ts | 3 + src/global/types/globalState.ts | 1 + src/hooks/reducers/useFoldersReducer.ts | 11 ++- src/lib/gramjs/tl/api.d.ts | 5 + src/lib/gramjs/tl/apiTl.ts | 2 + src/lib/gramjs/tl/static/api.json | 1 + src/lib/gramjs/tl/static/api.tl | 1 + src/types/language.d.ts | 4 + src/util/fallbackLangPack.ts | 4 + src/util/folderManager.ts | 4 + 28 files changed, 491 insertions(+), 49 deletions(-) create mode 100644 src/components/left/main/ChatTags.module.scss create mode 100644 src/components/left/main/ChatTags.tsx diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 145cbfe19..45f4fd89d 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -425,6 +425,7 @@ export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFi hasMyInvites: filter.hasMyInvites, isChatList: true, noTitleAnimations: filter.titleNoanimate, + color: filter.color, title: buildApiFormattedText(filter.title), }; } @@ -438,6 +439,7 @@ export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFi pinnedChatIds: filter.pinnedPeers.map(getApiChatIdFromMtpPeer).filter(Boolean), includedChatIds: filter.includePeers.map(getApiChatIdFromMtpPeer).filter(Boolean), excludedChatIds: filter.excludePeers.map(getApiChatIdFromMtpPeer).filter(Boolean), + color: filter.color, title: buildApiFormattedText(filter.title), noTitleAnimations: filter.titleNoanimate, }; diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 0f2ca6f51..55a8b07a7 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -296,6 +296,7 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi groups, channels, bots, + color, excludeArchived, excludeMuted, excludeRead, @@ -321,6 +322,7 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi return new GramJs.DialogFilterChatlist({ id: folder.id, title: buildInputTextWithEntities(folder.title), + color, emoticon: emoticon || undefined, pinnedPeers, includePeers, @@ -337,6 +339,7 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi nonContacts: nonContacts || undefined, groups: groups || undefined, bots: bots || undefined, + color, excludeArchived: excludeArchived || undefined, excludeMuted: excludeMuted || undefined, excludeRead: excludeRead || undefined, diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 9ac1e2833..6da6ded2a 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -1076,7 +1076,7 @@ export async function fetchChatFolders() { if (!result) { return undefined; } - const { filters } = result; + const { filters, tagsEnabled: areTagsEnabled } = result; const defaultFolderPosition = filters.findIndex((folder) => folder instanceof GramJs.DialogFilterDefault); const dialogFilters = filters.filter(isChatFolder); @@ -1090,6 +1090,7 @@ export async function fetchChatFolders() { .map(buildApiChatFolder), 'id', ) as Record, orderedIds, + areTagsEnabled, }; } @@ -1182,6 +1183,12 @@ export function sortChatFolders(ids: number[]) { })); } +export function toggleDialogFilterTags(isEnabled: boolean) { + return invokeRequest(new GramJs.messages.ToggleDialogFilterTags({ + enabled: isEnabled, + })); +} + export async function toggleDialogUnread({ chat, hasUnreadMark, }: { diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 1cd47fc0c..915e78388 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -223,6 +223,7 @@ export interface ApiChatFolder { groups?: true; channels?: true; bots?: true; + color?: number; excludeMuted?: true; excludeRead?: true; excludeArchived?: true; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index ae658ca12..f054cee41 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -364,6 +364,10 @@ "BlockedUsersBlockUser" = "Block User..."; "FilterChatTypes" = "Chat types"; "FilterChats" = "Chats"; +"FilterColorTitle" = "Folder Color"; +"FilterColorHint" = "This color will be used for the folder's tag in the chat list"; +"ShowFolderTags" = "Show Folder Tags"; +"ShowFolderTagsHint" = "Display folder names for each chat in the chat list."; "FilterIncludeInfo" = "Choose chats or types of chats that will appear in this folder."; "FilterNameHint" = "Folder name"; "FilterInclude" = "Included Chats"; diff --git a/src/components/common/ChatForumLastMessage.tsx b/src/components/common/ChatForumLastMessage.tsx index 16f0857e4..dff6036f7 100644 --- a/src/components/common/ChatForumLastMessage.tsx +++ b/src/components/common/ChatForumLastMessage.tsx @@ -30,6 +30,7 @@ type OwnProps = { topics?: Record; renderLastMessage: () => React.ReactNode; observeIntersection?: ObserveFn; + noForumTitle?: boolean; }; const NO_CORNER_THRESHOLD = Number(REM); @@ -40,6 +41,7 @@ const ChatForumLastMessage: FC = ({ topics, renderLastMessage, observeIntersection, + noForumTitle, }) => { const { openThread } = getActions(); @@ -102,53 +104,63 @@ const ChatForumLastMessage: FC = ({ dir={lang.isRtl ? 'rtl' : undefined} style={overwrittenWidth ? `--overwritten-width: ${overwrittenWidth}px` : undefined} > - {lastActiveTopic && ( -
-
- -
{renderText(lastActiveTopic.title)}
- {!overwrittenWidth && isReversedCorner && ( -
-
+ { + !noForumTitle && ( + <> + {lastActiveTopic && ( +
+
+ +
{renderText(lastActiveTopic.title)}
+ {!overwrittenWidth && isReversedCorner && ( +
+
+
+ )} +
+ +
+ {otherTopics.map((topic) => ( +
+ + {renderText(topic.title)} +
+ ))} +
+ +
)} -
- -
- {otherTopics.map((topic) => ( -
- - {renderText(topic.title)} + {!lastActiveTopic && ( +
+ {lang('Loading')}
- ))} -
- -
-
- )} - {!lastActiveTopic &&
{lang('Loading')}
} + )} + + ) + }
void; + withTags?: boolean; }; type StateProps = { @@ -118,6 +122,11 @@ type StateProps = { currentUserId: string; isSynced?: boolean; isAccountFrozen?: boolean; + folderIds?: number[]; + orderedIds?: number[]; + chatFoldersById?: Record; + activeChatFolder?: number; + areTagsEnabled?: boolean; }; const Chat: FC = ({ @@ -157,6 +166,12 @@ const Chat: FC = ({ isSynced, onDragEnter, isAccountFrozen, + folderIds, + orderedIds, + chatFoldersById, + activeChatFolder, + areTagsEnabled, + withTags, }) => { const { openChat, @@ -184,6 +199,8 @@ const Chat: FC = ({ useEnsureMessage(isSavedDialog ? currentUserId : chatId, lastMessageId, lastMessage); + const shouldRenderTags = areTagsEnabled && withTags && folderIds && folderIds.length > 1; + const { renderSubtitle, ref } = useChatListEntry({ chat, chatId, @@ -200,6 +217,7 @@ const Chat: FC = ({ isSavedDialog, isPreview, topics, + noForumTitle: shouldRenderTags, }); const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed); @@ -388,7 +406,7 @@ const Chat: FC = ({ )}
-
+
= ({ /> )}
+ {shouldRenderTags && ( + + )}
{shouldRenderDeleteModal && ( ( }; } + const folderIds = getChatFolderIds(chatId); + const { areTagsEnabled } = global.chatFolders; + const isPremium = selectIsCurrentUserPremium(global); + const lastMessageId = previewMessageId || selectChatLastMessageId(global, chatId, isSavedDialog ? 'saved' : 'all'); const lastMessage = previewMessageId ? selectChatMessage(global, chatId, previewMessageId) @@ -497,6 +527,8 @@ export default memo(withGlobal( const monoforumChannel = selectMonoforumChannel(global, chatId); + const activeChatFolder = selectTabState(global).activeChatFolder; + return { chat, isMuted: getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)), @@ -524,6 +556,11 @@ export default memo(withGlobal( lastMessageStory, isAccountFrozen, monoforumChannel, + folderIds, + orderedIds: global.chatFolders.orderedIds, + activeChatFolder, + chatFoldersById: global.chatFolders.byId, + areTagsEnabled: areTagsEnabled && isPremium, }; }, )(Chat)); diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 3c5f3b8c7..40e3f68c3 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -368,6 +368,7 @@ const ChatFolders: FC = ({ archiveSettings={archiveSettings} sessions={sessions} isAccountFrozen={isAccountFrozen} + withTags /> ); } diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index c83ef4620..9cee48277 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -54,6 +54,7 @@ type OwnProps = { isAccountFrozen?: boolean; isMainList?: boolean; foldersDispatch?: FolderEditDispatch; + withTags?: boolean; }; const INTERSECTION_THROTTLE = 200; @@ -72,6 +73,7 @@ const ChatList: FC = ({ isAccountFrozen, isMainList, foldersDispatch, + withTags, }) => { const { openChat, @@ -238,6 +240,7 @@ const ChatList: FC = ({ offsetTop={offsetTop} observeIntersection={observe} onDragEnter={handleDragEnter} + withTags={withTags} /> ); }); diff --git a/src/components/left/main/ChatTags.module.scss b/src/components/left/main/ChatTags.module.scss new file mode 100644 index 000000000..8c6b21938 --- /dev/null +++ b/src/components/left/main/ChatTags.module.scss @@ -0,0 +1,33 @@ +.wrapper { + display: flex; + gap: 0.25rem; +} + +.tag { + position: relative; + + flex-shrink: 0; + + min-width: 1.5rem; + padding: 0 0.25rem; + border-radius: 0.25rem; + + font-size: 0.5625rem; + font-weight: var(--font-weight-medium); + line-height: 0.75rem; + color: var(--accent-color); + text-align: center; + text-transform: uppercase; + + background-color: var(--accent-background-color); + + :global(.emoji-small) { + width: 0.5625rem; + height: 0.5625rem; + margin-top: -0.0625rem; + } + + &ColorMore { + color: var(--color-text-secondary); + } +} diff --git a/src/components/left/main/ChatTags.tsx b/src/components/left/main/ChatTags.tsx new file mode 100644 index 000000000..28f0a333c --- /dev/null +++ b/src/components/left/main/ChatTags.tsx @@ -0,0 +1,74 @@ +import { memo, useMemo } from '../../../lib/teact/teact'; + +import type { ApiChatFolder } from '../../../api/types'; + +import { ALL_FOLDER_ID } from '../../../config'; +import buildClassName from '../../../util/buildClassName'; +import { getApiPeerColorClass } from '../../common/helpers/peerColor'; +import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; + +import styles from './ChatTags.module.scss'; + +const MAX_VISIBLE_TAGS = 3; + +type OwnProps = { + folderIds?: number[]; + orderedIds?: number[]; + chatFoldersById?: Record; + activeChatFolder?: number; +}; + +const ChatTags = ({ + folderIds, + orderedIds, + chatFoldersById, + activeChatFolder, +}: OwnProps) => { + const activeFolderId = activeChatFolder !== undefined && orderedIds ? orderedIds[activeChatFolder] : undefined; + + const orderedFolderIds = useMemo(() => orderedIds?.filter((id) => { + const isFolder = folderIds?.includes(id); + const isActive = id === activeFolderId; + const isAll = id === ALL_FOLDER_ID; + + const folder = chatFoldersById?.[id]; + const hasColor = folder?.color !== undefined && folder.color !== -1; + + return isFolder && !isActive && !isAll && hasColor; + }) || [], [orderedIds, folderIds, activeFolderId, chatFoldersById]); + + const visibleFolderIds = orderedFolderIds.slice(0, MAX_VISIBLE_TAGS); + const remainingCount = orderedFolderIds.length - visibleFolderIds.length; + + return ( +
+ {visibleFolderIds.map((folderId) => { + const folder = chatFoldersById?.[folderId]; + return folder && ( +
+ {renderTextWithEntities({ + text: folder.title.text, + entities: folder.title.entities, + noCustomEmojiPlayback: folder.noTitleAnimations, + })} +
+ ); + })} + {remainingCount > 0 && ( +
+ + + {remainingCount} +
+ )} +
+ ); +}; + +export default memo(ChatTags); diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index bc28a8c7d..82f4479c6 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -52,6 +52,7 @@ export default function useChatListEntry({ isTopic, isSavedDialog, isPreview, + noForumTitle, }: { chat?: ApiChat; topics?: Record; @@ -70,6 +71,7 @@ export default function useChatListEntry({ animationType: ChatAnimationTypes; orderDiff: number; withInterfaceAnimations?: boolean; + noForumTitle?: boolean; }) { const oldLang = useOldLang(); const ref = useRef(); @@ -153,6 +155,7 @@ export default function useChatListEntry({ renderLastMessage={renderLastMessageOrTyping} observeIntersection={observeIntersection} topics={topics} + noForumTitle={noForumTitle} /> ); } diff --git a/src/components/left/settings/folders/SettingsFolders.scss b/src/components/left/settings/folders/SettingsFolders.scss index 21236d849..a39a2166d 100644 --- a/src/components/left/settings/folders/SettingsFolders.scss +++ b/src/components/left/settings/folders/SettingsFolders.scss @@ -116,3 +116,82 @@ flex-direction: column; height: 100%; } + +.color-picker { + display: flex; + gap: 1rem; + align-items: center; + + min-height: 3rem; + padding: 0 1rem 1rem; +} + +.color-picker-title { + display: flex; + align-items: center; + justify-content: space-between; +} + +.color-picker-item { + cursor: pointer; + + width: 1.5rem; + height: 1.5rem; + border: 0.125rem solid transparent; + border-radius: 50%; + + color: var(--accent-color); + + background-color: currentColor; + outline: none; + + transition: border 100ms ease-in-out, outline 100ms ease-in-out; +} + +.color-picker-item-disabled { + --accent-color: var(--color-topic-grey); +} + +.color-picker-selected-color { + color: var(--accent-color); +} + +.color-picker-item-none { + display: flex; + align-items: center; + justify-content: center; + color: var(--color-topic-grey); + + .color-picker-item-none-icon { + color: var(--color-background); + } +} + +.color-picker-item:not(.color-picker-item-hover-disabled) { + &.color-picker-item-active, + &:focus, + &:hover, + &:active { + border: 0.125rem solid var(--color-background); + outline: 0.125rem solid currentColor; + } +} + +.settings-item-relative { + position: relative; +} + +.settings-folders-lock-icon { + position: absolute; + top: 50%; + transform: translateY(-50%); + + padding: 1.3125rem; + + font-size: 0.875rem; + color: var(--color-gray); +} + +.settings-folders-title { + color: var(--accent-color); +} diff --git a/src/components/left/settings/folders/SettingsFoldersEdit.tsx b/src/components/left/settings/folders/SettingsFoldersEdit.tsx index 1817c4c0c..cbdcf981c 100644 --- a/src/components/left/settings/folders/SettingsFoldersEdit.tsx +++ b/src/components/left/settings/folders/SettingsFoldersEdit.tsx @@ -12,13 +12,16 @@ import type { } from '../../../../hooks/reducers/useFoldersReducer'; import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config'; -import { selectCanShareFolder } from '../../../../global/selectors'; +import { selectCanShareFolder, selectIsCurrentUserPremium } from '../../../../global/selectors'; import { selectCurrentLimit } from '../../../../global/selectors/limits'; +import buildClassName from '../../../../util/buildClassName'; import { isUserId } from '../../../../util/entities/ids'; import { findIntersectionWithSet } from '../../../../util/iteratees'; import { MEMO_EMPTY_ARRAY } from '../../../../util/memo'; import { CUSTOM_PEER_EXCLUDED_CHAT_TYPES, CUSTOM_PEER_INCLUDED_CHAT_TYPES } from '../../../../util/objects/customPeer'; import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets'; +import { getApiPeerColorClass } from '../../../common/helpers/peerColor'; +import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import { selectChatFilters } from '../../../../hooks/reducers/useFoldersReducer'; import useHistoryBack from '../../../../hooks/useHistoryBack'; @@ -55,12 +58,15 @@ type StateProps = { maxInviteLinks: number; maxChatLists: number; chatListCount: number; + isCurrentUserPremium: boolean; }; const SUBMIT_TIMEOUT = 500; const INITIAL_CHATS_LIMIT = 5; +const FOLDER_COLORS = [0, 1, 2, 3, 4, 5, 6]; + export const ERROR_NO_TITLE = 'Please provide a title for this folder.'; export const ERROR_NO_CHATS = 'ChatList.Filter.Error.Empty'; @@ -83,11 +89,13 @@ const SettingsFoldersEdit: FC = ({ maxChatLists, chatListCount, onSaveFolder, + isCurrentUserPremium, }) => { const { loadChatlistInvites, openLimitReachedModal, showNotification, + openPremiumModal, } = getActions(); const isCreating = state.mode === 'create'; @@ -346,6 +354,73 @@ const SettingsFoldersEdit: FC = ({
)} +
+

+ {lang('FilterColorTitle')} +
+ {renderTextWithEntities({ + text: state.folder.title.text, + entities: state.folder.title.entities, + noCustomEmojiPlayback: state.folder.noTitleAnimations, + })} +
+

+
+ {FOLDER_COLORS.map((color) => ( + +
+

+ {lang('FilterColorHint')} +

+
+

{lang('FolderLinkScreen.Title')} @@ -401,6 +476,8 @@ export default memo(withGlobal( const { byId, invites } = global.chatFolders; const chatListCount = Object.values(byId).reduce((acc, el) => acc + (el.isChatList ? 1 : 0), 0); + const isCurrentUserPremium = selectIsCurrentUserPremium(global); + return { loadedActiveChatIds: listIds.active, loadedArchivedChatIds: listIds.archived, @@ -409,6 +486,7 @@ export default memo(withGlobal( maxInviteLinks: selectCurrentLimit(global, 'chatlistInvites'), maxChatLists: selectCurrentLimit(global, 'chatlistJoined'), chatListCount, + isCurrentUserPremium, }; }, )(SettingsFoldersEdit)); diff --git a/src/components/left/settings/folders/SettingsFoldersMain.tsx b/src/components/left/settings/folders/SettingsFoldersMain.tsx index 1c75b6785..3a2e67049 100644 --- a/src/components/left/settings/folders/SettingsFoldersMain.tsx +++ b/src/components/left/settings/folders/SettingsFoldersMain.tsx @@ -10,10 +10,12 @@ import { ALL_FOLDER_ID, STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config' import { getFolderDescriptionText } from '../../../../global/helpers'; import { selectIsCurrentUserPremium } from '../../../../global/selectors'; import { selectCurrentLimit } from '../../../../global/selectors/limits'; +import buildClassName from '../../../../util/buildClassName'; import { isBetween } from '../../../../util/math'; import { MEMO_EMPTY_ARRAY } from '../../../../util/memo'; import { throttle } from '../../../../util/schedulers'; import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets'; +import { getApiPeerColorClass } from '../../../common/helpers/peerColor'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import { useFolderManagerForChatsCount } from '../../../../hooks/useFolderManager'; @@ -24,6 +26,7 @@ import usePreviousDeprecated from '../../../../hooks/usePreviousDeprecated'; import AnimatedIconWithPreview from '../../../common/AnimatedIconWithPreview'; import Icon from '../../../common/icons/Icon'; import Button from '../../../ui/Button'; +import Checkbox from '../../../ui/Checkbox'; import Draggable from '../../../ui/Draggable'; import ListItem from '../../../ui/ListItem'; import Loading from '../../../ui/Loading'; @@ -41,6 +44,7 @@ type StateProps = { recommendedChatFolders?: ApiChatFolder[]; maxFolders: number; isPremium?: boolean; + areTagsEnabled?: boolean; }; type SortState = { @@ -62,6 +66,7 @@ const SettingsFoldersMain: FC = ({ isPremium, recommendedChatFolders, maxFolders, + areTagsEnabled, }) => { const { loadRecommendedChatFolders, @@ -69,6 +74,8 @@ const SettingsFoldersMain: FC = ({ openLimitReachedModal, openDeleteChatFolderModal, sortChatFolders, + toggleDialogFilterTags, + openPremiumModal, } = getActions(); const [state, setState] = useState({ @@ -145,6 +152,7 @@ const SettingsFoldersMain: FC = ({ title: folder.title, subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]), isChatList: folder.isChatList, + color: folder.color, noTitleAnimations: folder.noTitleAnimations, }; }); @@ -162,6 +170,14 @@ const SettingsFoldersMain: FC = ({ addChatFolder({ folder }); }, [foldersById, maxFolders, addChatFolder, openLimitReachedModal]); + const handleToggleTags = useCallback(() => { + if (!isPremium) { + return; + } + + toggleDialogFilterTags({ isEnabled: !areTagsEnabled }); + }, [areTagsEnabled, isPremium, toggleDialogFilterTags]); + const handleDrag = useCallback((translation: { x: number; y: number }, id: string | number) => { const delta = Math.round(translation.y / FOLDER_HEIGHT_PX); const index = state.orderedFolderIds?.indexOf(id as number) || 0; @@ -303,7 +319,12 @@ const SettingsFoldersMain: FC = ({ } }} > - + {renderTextWithEntities({ text: folder.title.text, entities: folder.title.entities, @@ -365,6 +386,23 @@ const SettingsFoldersMain: FC = ({ ))}

)} +
+
+ { + if (!isPremium) { + event.preventDefault(); + openPremiumModal(); + } + }} + /> + {!isPremium && } +
+
); }; @@ -375,6 +413,7 @@ export default memo(withGlobal( orderedIds: folderIds, byId: foldersById, recommended: recommendedChatFolders, + areTagsEnabled, } = global.chatFolders; return { @@ -383,6 +422,7 @@ export default memo(withGlobal( isPremium: selectIsCurrentUserPremium(global), recommendedChatFolders, maxFolders: selectCurrentLimit(global, 'dialogFilters'), + areTagsEnabled, }; }, )(SettingsFoldersMain)); diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 8ca750240..3bf033a29 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -1115,6 +1115,24 @@ addActionHandler('loadRecommendedChatFolders', async (global): Promise => } }); +addActionHandler('toggleDialogFilterTags', async (global, actions, payload): Promise => { + const { isEnabled } = payload; + + const result = await callApi('toggleDialogFilterTags', isEnabled); + + if (result) { + global = getGlobal(); + global = { + ...global, + chatFolders: { + ...global.chatFolders, + areTagsEnabled: isEnabled, + }, + }; + setGlobal(global); + } +}); + addActionHandler('editChatFolders', (global, actions, payload): ActionReturnType => { const { chatId, idsToRemove, idsToAdd, tabId = getCurrentTabId(), diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 8ed3d30ec..bd68008dd 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -198,6 +198,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { chatFolders: { byId: {}, invites: {}, + areTagsEnabled: false, }, fileUploads: { diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index d2797462c..6891b3339 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -396,6 +396,9 @@ export interface ActionPayloads { deleteChatFolder: { id: number; }; + toggleDialogFilterTags: { + isEnabled: boolean; + }; openSupportChat: WithTabId | undefined; openChatByPhoneNumber: { phoneNumber: string; diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index 6c7de0361..96996028c 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -276,6 +276,7 @@ export type GlobalState = { byId: Record; invites: Record; recommended?: ApiChatFolder[]; + areTagsEnabled?: boolean; }; phoneCall?: ApiPhoneCall; diff --git a/src/hooks/reducers/useFoldersReducer.ts b/src/hooks/reducers/useFoldersReducer.ts index 1ac494336..ef79fe649 100644 --- a/src/hooks/reducers/useFoldersReducer.ts +++ b/src/hooks/reducers/useFoldersReducer.ts @@ -116,7 +116,7 @@ export type FoldersState = { export type FoldersActions = ( 'setTitle' | 'saveFilters' | 'editFolder' | 'reset' | 'setChatFilter' | 'setIsLoading' | 'setError' | 'editIncludeFilters' | 'editExcludeFilters' | 'setIncludeFilters' | 'setExcludeFilters' | 'setIsTouched' | - 'setFolderId' | 'setIsChatlist' + 'setFolderId' | 'setIsChatlist' | 'setColor' ); export type FolderEditDispatch = Dispatch; @@ -248,6 +248,15 @@ const foldersReducer: StateReducer = ( isChatList: action.payload, }, }; + case 'setColor': + return { + ...state, + folder: { + ...state.folder, + color: action.payload, + }, + isTouched: true, + }; case 'reset': return INITIAL_STATE; default: diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index cd2d7821b..28d6aca06 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -24829,6 +24829,11 @@ namespace Api { }, Bool> { order: int[]; } + export class ToggleDialogFilterTags extends Request<{ + enabled: Bool; + }, Bool> { + enabled: Bool; + } export class GetOldFeaturedStickers extends Request<{ offset: int; limit: int; diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index 4a3061953..d1c4ac725 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -1616,6 +1616,7 @@ messages.getDialogFilters#efd48c89 = messages.DialogFilters; messages.getSuggestedDialogFilters#a29cd42c = Vector; messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool; messages.updateDialogFiltersOrder#c563c1e4 order:Vector = Bool; +messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool; messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage; messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool; @@ -1671,6 +1672,7 @@ messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate; messages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies; messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector hash:long = messages.Messages; messages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector random_id:Vector = Updates; +messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool; messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects; messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 9fb883c73..8d1604614 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -163,6 +163,7 @@ "messages.getSuggestedDialogFilters", "messages.updateDialogFilter", "messages.updateDialogFiltersOrder", + "messages.toggleDialogFilterTags", "messages.getReplies", "messages.getDiscussionMessage", "messages.readDiscussion", diff --git a/src/lib/gramjs/tl/static/api.tl b/src/lib/gramjs/tl/static/api.tl index cf284a780..f1b358f66 100644 --- a/src/lib/gramjs/tl/static/api.tl +++ b/src/lib/gramjs/tl/static/api.tl @@ -2311,6 +2311,7 @@ messages.getDialogFilters#efd48c89 = messages.DialogFilters; messages.getSuggestedDialogFilters#a29cd42c = Vector; messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool; messages.updateDialogFiltersOrder#c563c1e4 order:Vector = Bool; +messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool; messages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messages.FeaturedStickers; messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 547602f9d..c82dcea22 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -320,6 +320,10 @@ export interface LangPair { 'BlockedUsersBlockUser': undefined; 'FilterChatTypes': undefined; 'FilterChats': undefined; + 'FilterColorTitle': undefined; + 'FilterColorHint': undefined; + 'ShowFolderTags': undefined; + 'ShowFolderTagsHint': undefined; 'FilterIncludeInfo': undefined; 'FilterNameHint': undefined; 'FilterInclude': undefined; diff --git a/src/util/fallbackLangPack.ts b/src/util/fallbackLangPack.ts index b8d0e8598..547e2ad50 100644 --- a/src/util/fallbackLangPack.ts +++ b/src/util/fallbackLangPack.ts @@ -175,6 +175,10 @@ export default { FilterChannels: 'Channels', FilterBots: 'Bots', FilterChats: 'Chats', + FilterColorTitle: 'Folder Color', + FilterColorHint: 'This color will be used for the folder\'s tag in the chat list', + ShowFolderTags: 'Show Folder Tags', + ShowFolderTagsHint: 'Display folder names for each chat in the chat list.', AccDescrChannel: 'Channel', AccDescrGroup: 'Group', Bot: 'bot', diff --git a/src/util/folderManager.ts b/src/util/folderManager.ts index 8185d47a6..220b7aac0 100644 --- a/src/util/folderManager.ts +++ b/src/util/folderManager.ts @@ -177,6 +177,10 @@ export function getOrderKey(chatId: string, isForSaved?: boolean) { return isForSaved ? summary.orderInSaved : summary.orderInAll; } +export function getChatFolderIds(chatId: string) { + return prepared.folderIdsByChatId[chatId]; +} + /* Callback managers */ export function addOrderedIdsCallback(folderId: number, callback: (orderedIds: string[]) => void) {