diff --git a/src/assets/font-icons/folder-tabs/bot.svg b/src/assets/font-icons/folder-tabs/bot.svg new file mode 100644 index 000000000..a1770c135 --- /dev/null +++ b/src/assets/font-icons/folder-tabs/bot.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/font-icons/folder-tabs/channel.svg b/src/assets/font-icons/folder-tabs/channel.svg new file mode 100644 index 000000000..bd995d637 --- /dev/null +++ b/src/assets/font-icons/folder-tabs/channel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/font-icons/folder-tabs/chat.svg b/src/assets/font-icons/folder-tabs/chat.svg new file mode 100644 index 000000000..606f200af --- /dev/null +++ b/src/assets/font-icons/folder-tabs/chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/font-icons/folder-tabs/chats.svg b/src/assets/font-icons/folder-tabs/chats.svg new file mode 100644 index 000000000..d12c647f6 --- /dev/null +++ b/src/assets/font-icons/folder-tabs/chats.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/font-icons/folder-tabs/folder.svg b/src/assets/font-icons/folder-tabs/folder.svg new file mode 100644 index 000000000..719aca401 --- /dev/null +++ b/src/assets/font-icons/folder-tabs/folder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/font-icons/folder-tabs/group.svg b/src/assets/font-icons/folder-tabs/group.svg new file mode 100644 index 000000000..a04bb3801 --- /dev/null +++ b/src/assets/font-icons/folder-tabs/group.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/font-icons/folder-tabs/star.svg b/src/assets/font-icons/folder-tabs/star.svg new file mode 100644 index 000000000..773271a07 --- /dev/null +++ b/src/assets/font-icons/folder-tabs/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/font-icons/folder-tabs/user.svg b/src/assets/font-icons/folder-tabs/user.svg new file mode 100644 index 000000000..8ea08c29f --- /dev/null +++ b/src/assets/font-icons/folder-tabs/user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/font-icons/menu.svg b/src/assets/font-icons/menu.svg new file mode 100644 index 000000000..f1e0f08c2 --- /dev/null +++ b/src/assets/font-icons/menu.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/font-icons/tools.svg b/src/assets/font-icons/tools.svg new file mode 100644 index 000000000..ff6cd0a98 --- /dev/null +++ b/src/assets/font-icons/tools.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 8a336fe4c..fa029c0d3 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -330,7 +330,8 @@ "ChatEmptyChat" = "No messages here yet"; "ChatListEmptyChatListEditFilter" = "Edit Folder"; "UpdateTelegram" = "Update Telegram"; -"AccDescrOpenMenu2" = "Open menu"; +"AriaLabelOpenMenu" = "Open menu"; +"AriaLabelBackChatList" = "Return to chat list"; "SettingsTipsUsername" = "TelegramTips"; "SearchFriends" = "Search contacts"; "Search" = "Search"; @@ -370,6 +371,9 @@ "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."; +"TabsPosition" = "Tabs View"; +"TabsPositionLeft" = "Tabs on the left"; +"TabsPositionTop" = "Tabs at the top"; "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/FolderIcon.module.scss b/src/components/common/FolderIcon.module.scss new file mode 100644 index 000000000..4c4d92d2a --- /dev/null +++ b/src/components/common/FolderIcon.module.scss @@ -0,0 +1,6 @@ +.emoji { + display: grid; + place-content: center; + width: 2rem; + height: 2rem; +} diff --git a/src/components/common/FolderIcon.tsx b/src/components/common/FolderIcon.tsx new file mode 100644 index 000000000..014ba7883 --- /dev/null +++ b/src/components/common/FolderIcon.tsx @@ -0,0 +1,41 @@ +import { memo } from '../../lib/teact/teact'; + +import { emojiToFolderIcon } from '../../util/folderIconMap'; +import { REM } from './helpers/mediaDimensions'; +import renderText from './helpers/renderText'; + +import CustomEmoji from './CustomEmoji'; +import Icon from './icons/Icon'; + +import styles from './FolderIcon.module.scss'; + +const ICON_SIZE = 2.25 * REM; + +const FolderIcon = ( + { + emoji, + customEmojiId, + shouldAnimate, + }: { + emoji?: string; + customEmojiId?: string; + shouldAnimate?: boolean; + }, +) => { + if (customEmojiId) { + return ; + } + + if (!emoji) { + return ; + } + + const iconName = emojiToFolderIcon(emoji); + if (iconName) { + return ; + } + + return
{renderText(emoji)}
; +}; + +export default memo(FolderIcon); diff --git a/src/components/common/LinkField.tsx b/src/components/common/LinkField.tsx index 49ce1e714..fb7598e3b 100644 --- a/src/components/common/LinkField.tsx +++ b/src/components/common/LinkField.tsx @@ -6,6 +6,7 @@ import buildClassName from '../../util/buildClassName'; import { copyTextToClipboard } from '../../util/clipboard'; import useAppLayout from '../../hooks/useAppLayout'; +import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; @@ -33,7 +34,8 @@ const InviteLink: FC = ({ withShare, onRevoke, }) => { - const lang = useOldLang(); + const lang = useLang(); + const oldLang = useOldLang(); const { showNotification, openChatWithDraft } = getActions(); const { isMobile } = useAppLayout(); @@ -67,7 +69,7 @@ const InviteLink: FC = ({ color="translucent" className={isOpen ? 'active' : ''} onClick={onTrigger} - ariaLabel={lang('AccDescrOpenMenu2')} + ariaLabel={lang('AriaLabelOpenMenu')} > @@ -77,7 +79,7 @@ const InviteLink: FC = ({ return (

- {lang(title || 'InviteLink.InviteLink')} + {oldLang(title || 'InviteLink.InviteLink')}

= ({ trigger={PrimaryLinkMenuButton} positionX="right" > - {lang('Copy')} + {oldLang('Copy')} {onRevoke && ( - {lang('RevokeButton')} + {oldLang('RevokeButton')} )} )} @@ -116,7 +118,7 @@ const InviteLink: FC = ({ onClick={handleShare} className={styles.share} > - {lang('FolderLinkScreen.LinkActionShare')} + {oldLang('FolderLinkScreen.LinkActionShare')} )}
diff --git a/src/components/common/MainMenuDropdown.tsx b/src/components/common/MainMenuDropdown.tsx new file mode 100644 index 000000000..68a4fc35d --- /dev/null +++ b/src/components/common/MainMenuDropdown.tsx @@ -0,0 +1,85 @@ +import { type FC, memo } from '@teact'; +import { getActions } from '../../global'; + +import { LeftColumnContent, SettingsScreens } from '../../types'; + +import { + APP_NAME, + DEBUG, + IS_BETA, +} from '../../config'; +import buildClassName from '../../util/buildClassName'; + +import useFlag from '../../hooks/useFlag'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; +import useLeftHeaderButtonRtlForumTransition from '../left/main/hooks/useLeftHeaderButtonRtlForumTransition'; + +import LeftSideMenuItems from '../left/main/LeftSideMenuItems'; +import DropdownMenu from '../ui/DropdownMenu'; + +type OwnProps = { + trigger?: FC<{ onTrigger: () => void; isOpen?: boolean }>; + shouldHideSearch?: boolean; + className?: string; +}; + +const LeftSideMenuDropdown = ({ + trigger, + shouldHideSearch, + className, +}: OwnProps) => { + const { openLeftColumnContent, closeForumPanel, openSettingsScreen } = getActions(); + const [isBotMenuOpen, markBotMenuOpen, unmarkBotMenuOpen] = useFlag(); + const lang = useLang(); + + const versionString = IS_BETA ? `${APP_VERSION} Beta (${APP_REVISION})` : (DEBUG ? APP_REVISION : APP_VERSION); + + // Disable dropdown menu RTL animation for resize + const { + shouldDisableDropdownMenuTransitionRef, + handleDropdownMenuTransitionEnd, + } = useLeftHeaderButtonRtlForumTransition(shouldHideSearch); + + const handleSelectSettings = useLastCallback(() => { + openSettingsScreen({ screen: SettingsScreens.Main }); + }); + + const handleSelectContacts = useLastCallback(() => { + openLeftColumnContent({ contentKey: LeftColumnContent.Contacts }); + }); + + const handleSelectArchived = useLastCallback(() => { + openLeftColumnContent({ contentKey: LeftColumnContent.Archived }); + closeForumPanel(); + }); + + return ( + + + + ); +}; + +export default memo(LeftSideMenuDropdown); diff --git a/src/components/common/StickerButton.tsx b/src/components/common/StickerButton.tsx index 1bfe79398..77c1b6809 100644 --- a/src/components/common/StickerButton.tsx +++ b/src/components/common/StickerButton.tsx @@ -107,7 +107,7 @@ const StickerButton = & { isRightColumnShown?: boolean; leftColumnWidth?: number; + isFoldersSidebarShown?: boolean; }; const MAX_PRELOAD_DELAY = 700; @@ -104,6 +107,7 @@ const UiLoader: FC = ({ isRightColumnShown, shouldSkipHistoryAnimations, leftColumnWidth, + isFoldersSidebarShown, }) => { const { setIsUiReady } = getActions(); @@ -151,7 +155,8 @@ const UiLoader: FC = ({ {shouldRenderMask && !shouldSkipHistoryAnimations && Boolean(page) && (
{page === 'main' ? ( -
+
+ {isFoldersSidebarShown &&
}
( (global, { isMobile }): Complete => { const tabState = selectTabState(global); + const { tabsPosition } = selectSharedSettings(global); + return { shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations, uiReadyState: tabState.uiReadyState, isRightColumnShown: selectIsRightColumnShown(global, isMobile), leftColumnWidth: global.leftColumnWidth, + isFoldersSidebarShown: tabsPosition === TABS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global), }; }, )(UiLoader); diff --git a/src/components/left/LeftColumn.scss b/src/components/left/LeftColumn.scss index c469e4c7b..a0ccd7c9a 100644 --- a/src/components/left/LeftColumn.scss +++ b/src/components/left/LeftColumn.scss @@ -28,12 +28,7 @@ } .SearchInput { - max-width: calc(100% - 3.25rem); margin-left: 0.625rem; - - @media (max-width: 600px) { - max-width: calc(100% - 3rem); - } } .Button.smaller { @@ -45,19 +40,8 @@ } } - body.is-tauri.is-macos #Main:not(.is-fullscreen) &:not(#TopicListHeader) { - justify-content: space-between; - padding: 0.5rem 0.5rem 0.5rem var(--window-controls-width); - - .SearchInput { - max-width: calc(100% - 2.75rem); - margin-left: 0.5rem; - } - - .Menu.main-menu .bubble { - --offset-y: 100%; - --offset-x: -4.125rem; - } + body.is-tauri.is-macos #Main:not(.is-fullscreen):not(.tabs-sidebar-visible) &:not(#TopicListHeader) { + padding-left: var(--window-controls-width); } @media (max-width: 600px) { diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index 837bbf42b..db7950ce2 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -38,6 +38,7 @@ import './LeftColumn.scss'; interface OwnProps { ref: ElementRef; + isFoldersSidebarShown: boolean; } type StateProps = { @@ -96,6 +97,7 @@ function LeftColumn({ archiveSettings, isArchivedStoryRibbonShown, isAccountFrozen, + isFoldersSidebarShown, }: OwnProps & StateProps) { const { setGlobalSearchQuery, @@ -541,6 +543,7 @@ function LeftColumn({ isForumPanelOpen={isForumPanelOpen} onTopicSearch={handleTopicSearch} isAccountFrozen={isAccountFrozen} + isFoldersSidebarShown={isFoldersSidebarShown} /> ); } diff --git a/src/components/left/main/Archive.module.scss b/src/components/left/main/Archive.module.scss index 39d430e60..7c93da824 100644 --- a/src/components/left/main/Archive.module.scss +++ b/src/components/left/main/Archive.module.scss @@ -14,6 +14,10 @@ display: none; } + &.no-margin-top { + margin-top: 0 !important; + } + &:hover { opacity: 0.85; } diff --git a/src/components/left/main/Archive.tsx b/src/components/left/main/Archive.tsx index 24ab54145..f29f372ff 100644 --- a/src/components/left/main/Archive.tsx +++ b/src/components/left/main/Archive.tsx @@ -26,6 +26,7 @@ type OwnProps = { archiveSettings: GlobalState['archiveSettings']; onDragEnter?: NoneToVoidFunction; onClick?: NoneToVoidFunction; + isFoldersSidebarShown?: boolean; }; const PREVIEW_SLICE = 5; @@ -40,6 +41,7 @@ const Archive: FC = ({ archiveSettings, onDragEnter, onClick, + isFoldersSidebarShown, }) => { const { updateArchiveSettings } = getActions(); const lang = useLang(); @@ -158,6 +160,7 @@ const Archive: FC = ({ className={buildClassName( styles.root, archiveSettings.isMinimized && styles.minimized, + isFoldersSidebarShown && archiveSettings.isMinimized && styles.noMarginTop, 'chat-item-clickable', 'chat-item-archive', )} diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index e1ce015ce..d1ba75ea7 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -99,6 +99,7 @@ type OwnProps = { onDragEnter?: (chatId: string) => void; onDragLeave?: NoneToVoidFunction; onReorderAnimationEnd?: NoneToVoidFunction; + isFoldersSidebarShown?: boolean; }; type StateProps = { @@ -176,6 +177,7 @@ const Chat: FC = ({ areTagsEnabled, withTags, onReorderAnimationEnd, + isFoldersSidebarShown, }) => { const { openChat, @@ -491,6 +493,7 @@ const Chat: FC = ({ itemClassName="chat-tag" orderedFolderIds={tagFolderIds} chatFoldersById={chatFoldersById} + isFoldersSidebarShown={isFoldersSidebarShown} /> )}
diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 50337e72d..df1a266d3 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -1,32 +1,24 @@ import type { FC } from '@teact'; -import { memo, useEffect, useMemo, useRef } from '@teact'; -import { getActions, getGlobal, withGlobal } from '../../../global'; +import { memo, useEffect, useRef } from '@teact'; +import { getActions, 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 { AnimationLevel } 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, selectIsCurrentUserFrozen, selectTabState } from '../../../global/selectors'; +import { selectIsCurrentUserFrozen, selectTabState } from '../../../global/selectors'; import { selectCurrentLimit } from '../../../global/selectors/limits'; import { selectSharedSettings } from '../../../global/selectors/sharedState'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; import { captureEvents, SwipeDirection } from '../../../util/captureEvents'; -import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { resolveTransitionName } from '../../../util/resolveTransitionName'; -import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; import useDerivedState from '../../../hooks/useDerivedState'; -import { - useFolderManagerForUnreadChatsByFolder, - useFolderManagerForUnreadCounters, -} from '../../../hooks/useFolderManager'; +import useFolderTabs from '../../../hooks/useFolderTabs'; import useHistoryBack from '../../../hooks/useHistoryBack'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -41,6 +33,7 @@ type OwnProps = { foldersDispatch: FolderEditDispatch; shouldHideFolderTabs?: boolean; isForumPanelOpen?: boolean; + isFoldersSidebarShown?: boolean; }; type StateProps = { @@ -85,17 +78,12 @@ const ChatFolders: FC = ({ isStoryRibbonShown, sessions, isAccountFrozen, + isFoldersSidebarShown, }) => { const { loadChatFolders, setActiveChatFolder, openChat, - openShareChatFolderModal, - openDeleteChatFolderModal, - openEditChatFolder, - openLimitReachedModal, - markChatMessagesRead, - openSettingsScreen, } = getActions(); const transitionRef = useRef(); @@ -118,149 +106,27 @@ const ChatFolders: FC = ({ const isStoryRibbonClosing = useDerivedState(getIsStoryRibbonClosing); const scrollToTop = useLastCallback(() => { - const activeList = ref.current?.querySelector('.chat-list.Transition_slide-active'); + const activeList = ref.current?.querySelector('#LeftColumn .chat-list.Transition_slide-active'); activeList?.scrollTo({ top: 0, behavior: 'smooth', }); }); - const allChatsFolder: ApiChatFolder = useMemo(() => { - return { - id: ALL_FOLDER_ID, - title: { text: orderedFolderIds?.[0] === ALL_FOLDER_ID ? lang('FilterAllChatsShort') : lang('FilterAllChats') }, - includedChatIds: MEMO_EMPTY_ARRAY, - excludedChatIds: MEMO_EMPTY_ARRAY, - } satisfies ApiChatFolder; - }, [orderedFolderIds, lang]); - - const displayedFolders = useMemo(() => { - return orderedFolderIds - ? orderedFolderIds.map((id) => { - if (id === ALL_FOLDER_ID) { - return allChatsFolder; - } - - return chatFoldersById[id] || {}; - }).filter(Boolean) - : undefined; - }, [chatFoldersById, allChatsFolder, orderedFolderIds]); + const { displayedFolders, folderTabs } = useFolderTabs({ + sidebarMode: false, + orderedFolderIds, + chatFoldersById, + maxFolders, + maxChatLists, + folderInvitesById, + maxFolderInvites, + }); const allChatsFolderIndex = displayedFolders?.findIndex((folder) => folder.id === ALL_FOLDER_ID); 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) { - return undefined; - } - - return displayedFolders.map((folder, i) => { - const { id, title } = folder; - const isBlocked = id !== ALL_FOLDER_ID && i > maxFolders - 1; - const canShareFolder = selectCanShareFolder(getGlobal(), id); - const contextActions: MenuItemContextAction[] = []; - - if (canShareFolder) { - contextActions.push({ - title: lang('FilterShare'), - icon: 'link', - handler: () => { - const chatListCount = Object.values(chatFoldersById).reduce((acc, el) => acc + (el.isChatList ? 1 : 0), 0); - if (chatListCount >= maxChatLists && !folder.isChatList) { - openLimitReachedModal({ - limit: 'chatlistJoined', - }); - return; - } - - // Greater amount can be after premium downgrade - if (folderInvitesById[id]?.length >= maxFolderInvites) { - openLimitReachedModal({ - limit: 'chatlistInvites', - }); - return; - } - - openShareChatFolderModal({ - folderId: id, - }); - }, - }); - } - - if (id === ALL_FOLDER_ID) { - contextActions.push({ - title: lang('FilterEditFolders'), - icon: 'edit', - handler: () => { - openSettingsScreen({ screen: 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('FilterMenuDelete'), - icon: 'delete', - destructive: true, - handler: () => { - openDeleteChatFolderModal({ folderId: id }); - }, - }); - } - - return { - id, - title: renderTextWithEntities({ - text: title.text, - entities: title.entities, - noCustomEmojiPlayback: folder.noTitleAnimations, - }), - badgeCount: folderCountersById[id]?.chatsCount, - isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount), - isBlocked, - contextActions: contextActions?.length ? contextActions : undefined, - } satisfies TabWithProperties; - }); - }, [ - displayedFolders, maxFolders, folderCountersById, lang, chatFoldersById, maxChatLists, folderInvitesById, - maxFolderInvites, folderUnreadChatsCountersById, openSettingsScreen, - ]); - const handleSwitchTab = useLastCallback((index: number) => { setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true }); if (activeChatFolder === index) { @@ -368,6 +234,7 @@ const ChatFolders: FC = ({ archiveSettings={archiveSettings} sessions={sessions} isAccountFrozen={isAccountFrozen} + isFoldersSidebarShown={isFoldersSidebarShown} isStoryRibbonShown={isStoryRibbonShown} withTags /> @@ -381,12 +248,13 @@ const ChatFolders: FC = ({ ref={ref} className={buildClassName( 'ChatFolders', - shouldRenderFolders && shouldHideFolderTabs && 'ChatFolders--tabs-hidden', + shouldRenderFolders && shouldHideFolderTabs && !isFoldersSidebarShown && 'ChatFolders--tabs-hidden', shouldRenderStoryRibbon && 'with-story-ribbon', + isFoldersSidebarShown && 'ChatFolders--tabs-sidebar-shown', )} > {shouldRenderStoryRibbon && } - {shouldRenderFolders ? ( + {shouldRenderFolders && !isFoldersSidebarShown ? ( = ({ isAccountFrozen, isMainList, withTags, + isFoldersSidebarShown, isStoryRibbonShown, foldersDispatch, }) => { @@ -234,6 +236,7 @@ const ChatList: FC = ({ onDragEnter={handleChatDragEnter} onDragLeave={onDragLeave} withTags={withTags} + isFoldersSidebarShown={isFoldersSidebarShown} /> ); }); @@ -269,6 +272,7 @@ const ChatList: FC = ({ archiveSettings={archiveSettings} onClick={handleArchivedClick} onDragEnter={handleArchivedDragEnter} + isFoldersSidebarShown={isFoldersSidebarShown} /> )} {viewportIds?.length ? ( diff --git a/src/components/left/main/ChatTags.tsx b/src/components/left/main/ChatTags.tsx index 883d3dc72..17689afa8 100644 --- a/src/components/left/main/ChatTags.tsx +++ b/src/components/left/main/ChatTags.tsx @@ -1,6 +1,6 @@ -import { memo } from '../../../lib/teact/teact'; +import { memo, useCallback } from '@teact'; -import type { ApiChatFolder } from '../../../api/types'; +import { type ApiChatFolder, ApiMessageEntityTypes } from '../../../api/types'; import buildClassName from '../../../util/buildClassName'; import { REM } from '../../common/helpers/mediaDimensions'; @@ -16,12 +16,14 @@ const CUSTOM_EMOJI_SIZE = 0.875 * REM; type OwnProps = { orderedFolderIds?: number[]; chatFoldersById?: Record; + isFoldersSidebarShown?: boolean; itemClassName?: string; }; const ChatTags = ({ orderedFolderIds, chatFoldersById, + isFoldersSidebarShown, itemClassName, }: OwnProps) => { if (!orderedFolderIds) { @@ -31,6 +33,31 @@ const ChatTags = ({ const visibleFolderIds = orderedFolderIds.slice(0, MAX_VISIBLE_TAGS); const remainingCount = orderedFolderIds.length - visibleFolderIds.length; + const getFolderTitle = useCallback((folder: ApiChatFolder) => { + let text = folder.title.text; + let entities = folder.title.entities; + + if (isFoldersSidebarShown) { + const currentCustomEmoji = folder.title.entities?.find( + (entity) => entity.type === ApiMessageEntityTypes.CustomEmoji && entity.offset === 0); + if (currentCustomEmoji) { + const { offset, length } = currentCustomEmoji; + + text = folder.title.text.replace(folder.title.text.substring(offset, offset + length), ''); + entities = folder.title.entities?.filter((entity) => entity.offset !== offset).map((entity) => ({ + ...entity, + offset: entity.offset - length, + })); + } + } + return renderTextWithEntities({ + text, + entities, + noCustomEmojiPlayback: folder.noTitleAnimations, + emojiSize: CUSTOM_EMOJI_SIZE, + }); + }, [isFoldersSidebarShown]); + return (
{visibleFolderIds.map((folderId) => { @@ -44,12 +71,7 @@ const ChatTags = ({ itemClassName, )} > - {renderTextWithEntities({ - text: folder.title.text, - entities: folder.title.entities, - noCustomEmojiPlayback: folder.noTitleAnimations, - emojiSize: CUSTOM_EMOJI_SIZE, - })} + {getFolderTitle(folder)}
); })} diff --git a/src/components/left/main/LeftMain.scss b/src/components/left/main/LeftMain.scss index 2351799f2..ef9477894 100644 --- a/src/components/left/main/LeftMain.scss +++ b/src/components/left/main/LeftMain.scss @@ -47,6 +47,10 @@ opacity: 0.25; } + &--tabs-sidebar-shown .chat-list { + padding-top: 0; + } + .Tab { flex: 0 0 auto; } diff --git a/src/components/left/main/LeftMain.tsx b/src/components/left/main/LeftMain.tsx index 84b0caef5..63c8699a9 100644 --- a/src/components/left/main/LeftMain.tsx +++ b/src/components/left/main/LeftMain.tsx @@ -44,6 +44,7 @@ type OwnProps = { onTopicSearch: NoneToVoidFunction; isAccountFrozen?: boolean; onReset: () => void; + isFoldersSidebarShown?: boolean; }; const TRANSITION_RENDER_COUNT = Object.keys(LeftColumnContent).length / 2; @@ -66,8 +67,9 @@ const LeftMain: FC = ({ onReset, onTopicSearch, isAccountFrozen, + isFoldersSidebarShown, }) => { - const { closeForumPanel, openLeftColumnContent } = getActions(); + const { openLeftColumnContent } = getActions(); const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV); const [tauriUpdate, setTauriUpdate] = useState(); const [isTauriUpdateDownloading, setIsTauriUpdateDownloading] = useState(false); @@ -109,19 +111,10 @@ const LeftMain: FC = ({ }, BUTTON_CLOSE_DELAY_MS); }); - const handleSelectSettings = useLastCallback(() => { - openLeftColumnContent({ contentKey: LeftColumnContent.Settings }); - }); - const handleSelectContacts = useLastCallback(() => { openLeftColumnContent({ contentKey: LeftColumnContent.Contacts }); }); - const handleSelectArchived = useLastCallback(() => { - openLeftColumnContent({ contentKey: LeftColumnContent.Archived }); - closeForumPanel(); - }); - const handleUpdateClick = useLastCallback(async () => { if (tauriUpdate) { try { @@ -198,12 +191,10 @@ const LeftMain: FC = ({ content={content} contactsFilter={contactsFilter} onSearchQuery={onSearchQuery} - onSelectSettings={handleSelectSettings} - onSelectContacts={handleSelectContacts} - onSelectArchived={handleSelectArchived} onReset={onReset} shouldSkipTransition={shouldSkipTransition} isClosingSearch={isClosingSearch} + isFoldersSidebarShown={isFoldersSidebarShown} /> = ({ shouldHideFolderTabs={isForumPanelVisible} foldersDispatch={foldersDispatch} isForumPanelOpen={isForumPanelVisible} + isFoldersSidebarShown={isFoldersSidebarShown} /> ); case LeftColumnContent.GlobalSearch: diff --git a/src/components/left/main/LeftMainHeader.scss b/src/components/left/main/LeftMainHeader.scss index c424b31d1..daadb1de9 100644 --- a/src/components/left/main/LeftMainHeader.scss +++ b/src/components/left/main/LeftMainHeader.scss @@ -3,6 +3,12 @@ #LeftMainHeader { position: relative; + .main-menu { + width: 2.5rem; + /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ + transition: width var(--layer-transition); + } + .DropdownMenuFiller { width: 2.5rem; height: 2.5rem; @@ -85,6 +91,29 @@ } } + .hide-menu-button { + overflow: hidden; + width: 0; + visibility: hidden; + .animated-menu-icon { + &::before { + transform: rotate(45deg) scaleX(0.75) translate(0.375rem, -0.1875rem); + } + + &::after { + transform: rotate(-45deg) scaleX(0.75) translate(0.375rem, 0.1875rem); + } + } + } + + .forum-search-button { + margin-left: 0.75rem; + } + + .SearchInput--no-left-margin { + margin-left: 0; + } + .MenuItem .Toggle { margin-inline-start: auto; } @@ -106,11 +135,7 @@ .extra-spacing { position: relative; - margin-left: 0.8125rem; - - body.is-tauri.is-macos #Main:not(.is-fullscreen) & { - margin-left: 0.5rem; - } + margin-left: 0.5rem; } .StatusButton { diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index f700a1f0d..908404c0d 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -8,15 +8,11 @@ import type { GlobalState } from '../../../global/types'; import type { ThemeKey } from '../../../types'; import { LeftColumnContent, SettingsScreens } from '../../../types'; -import { - APP_NAME, - DEBUG, - IS_BETA, -} from '../../../config'; import { selectCanSetPasscode, selectCurrentMessageList, selectIsCurrentUserPremium, + selectIsForumPanelOpen, selectTabState, selectTheme, } from '../../../global/selectors'; @@ -29,23 +25,19 @@ import { formatDateToString } from '../../../util/dates/dateFormat'; import useAppLayout from '../../../hooks/useAppLayout'; import useConnectionStatus from '../../../hooks/useConnectionStatus'; -import useFlag from '../../../hooks/useFlag'; import { useHotkeys } from '../../../hooks/useHotkeys'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; -import { useFullscreenStatus } from '../../../hooks/window/useFullscreen'; -import useLeftHeaderButtonRtlForumTransition from './hooks/useLeftHeaderButtonRtlForumTransition'; import Icon from '../../common/icons/Icon'; +import MainMenuDropdown from '../../common/MainMenuDropdown'; import PeerChip from '../../common/PeerChip'; import StoryToggler from '../../story/StoryToggler'; import Button from '../../ui/Button'; -import DropdownMenu from '../../ui/DropdownMenu'; import SearchInput from '../../ui/SearchInput'; import ShowTransition from '../../ui/ShowTransition'; import ConnectionStatusOverlay from '../ConnectionStatusOverlay'; -import LeftSideMenuItems from './LeftSideMenuItems'; import StatusButton from './StatusButton'; import './LeftMainHeader.scss'; @@ -56,33 +48,32 @@ type OwnProps = { contactsFilter: string; isClosingSearch?: boolean; shouldSkipTransition?: boolean; + isFoldersSidebarShown?: boolean; onSearchQuery: (query: string) => void; - onSelectSettings: NoneToVoidFunction; - onSelectContacts: NoneToVoidFunction; - onSelectArchived: NoneToVoidFunction; onReset: NoneToVoidFunction; }; -type StateProps = - { - searchQuery?: string; - isLoading: boolean; - globalSearchChatId?: string; - searchDate?: number; - theme: ThemeKey; - isMessageListOpen: boolean; - isCurrentUserPremium?: boolean; - isConnectionStatusMinimized?: boolean; - areChatsLoaded?: boolean; - hasPasscode?: boolean; - canSetPasscode?: boolean; - } - & Pick; +type StateProps = { + searchQuery?: string; + isLoading: boolean; + globalSearchChatId?: string; + searchDate?: number; + theme: ThemeKey; + isMessageListOpen: boolean; + isCurrentUserPremium?: boolean; + isConnectionStatusMinimized?: boolean; + areChatsLoaded?: boolean; + hasPasscode?: boolean; + canSetPasscode?: boolean; + isForumPanelOpen?: boolean; +} & Pick; const CLEAR_DATE_SEARCH_PARAM = { date: undefined }; const CLEAR_CHAT_SEARCH_PARAM = { id: undefined }; -const LeftMainHeader: FC = ({ +const IS_WITH_WINDOW_BUTTONS = IS_TAURI && IS_MAC_OS; + +const LeftMainHeader = ({ shouldHideSearch, content, contactsFilter, @@ -102,12 +93,11 @@ const LeftMainHeader: FC = ({ areChatsLoaded, hasPasscode, canSetPasscode, + isFoldersSidebarShown, + isForumPanelOpen, onSearchQuery, - onSelectSettings, - onSelectContacts, - onSelectArchived, onReset, -}) => { +}: OwnProps & StateProps) => { const { setGlobalSearchDate, setSharedSettingOption, @@ -115,17 +105,18 @@ const LeftMainHeader: FC = ({ lockScreen, openSettingsScreen, searchMessagesGlobal, + closeForumPanel, } = getActions(); const oldLang = useOldLang(); const lang = useLang(); const { isMobile } = useAppLayout(); - const [isBotMenuOpen, markBotMenuOpen, unmarkBotMenuOpen] = useFlag(); - const areContactsVisible = content === LeftColumnContent.Contacts; const hasMenu = content === LeftColumnContent.ChatList; + const isSearchButton = isForumPanelOpen && isFoldersSidebarShown && !IS_WITH_WINDOW_BUTTONS; + const selectedSearchDate = useMemo(() => { return searchDate ? formatDateToString(new Date(searchDate * 1000)) @@ -151,6 +142,10 @@ const LeftMainHeader: FC = ({ } }); + const handleForumSearchClick = useLastCallback(() => { + closeForumPanel(); + }); + useHotkeys(useMemo(() => (canSetPasscode ? { 'Ctrl+Shift+L': handleLockScreenHotkey, 'Alt+Shift+L': handleLockScreenHotkey, @@ -165,20 +160,24 @@ const LeftMainHeader: FC = ({ ripple={hasMenu && !isMobile} size="smaller" color="translucent" - className={isOpen ? 'active' : ''} + className={buildClassName(isOpen && 'active')} - onClick={hasMenu ? onTrigger : () => onReset()} - ariaLabel={hasMenu ? oldLang('AccDescrOpenMenu2') : 'Return to chat list'} + onClick={isSearchButton ? handleForumSearchClick : hasMenu ? onTrigger : () => onReset()} + ariaLabel={hasMenu ? lang('AriaLabelOpenMenu') : lang('AriaLabelBackChatList')} > -
+ ) : ( +
)} - /> ); - }, [hasMenu, isMobile, oldLang, onReset, shouldSkipTransition]); + }, [hasMenu, isSearchButton, isMobile, lang, onReset, shouldSkipTransition]); const handleSearchFocus = useLastCallback(() => { if (!searchQuery) { @@ -215,16 +214,6 @@ const LeftMainHeader: FC = ({ ? lang('SearchFriends') : lang('Search'); - const versionString = IS_BETA ? `${APP_VERSION} Beta (${APP_REVISION})` : (DEBUG ? APP_REVISION : APP_VERSION); - - const isFullscreen = useFullscreenStatus(); - - // Disable dropdown menu RTL animation for resize - const { - shouldDisableDropdownMenuTransitionRef, - handleDropdownMenuTransitionEnd, - } = useLeftHeaderButtonRtlForumTransition(shouldHideSearch); - const withStoryToggler = !isSearchFocused && !selectedSearchDate && !globalSearchChatId && !areContactsVisible; const searchContent = useMemo(() => { @@ -256,53 +245,28 @@ const LeftMainHeader: FC = ({ ); }, [globalSearchChatId, selectedSearchDate]); - const version = useMemo(() => { - let fullVersion = ''; - if (IS_TAURI && window.tauri.version) { - fullVersion = `Tauri ${window.tauri.version} | `; - } - - fullVersion += `${APP_NAME} ${versionString}`; - - return fullVersion; - }, [versionString]); - return (
{lang.isRtl &&
} - - - + /> ( connectionState, isSyncing, isFetchingDifference, } = global; const { isConnectionStatusMinimized } = selectSharedSettings(global); + const isForumPanelOpen = selectIsForumPanelOpen(global); return { searchQuery, @@ -380,6 +345,7 @@ export default memo(withGlobal( areChatsLoaded: Boolean(global.chats.listIds.active), hasPasscode: Boolean(global.passcode.hasPasscode), canSetPasscode: selectCanSetPasscode(global), + isForumPanelOpen, }; }, )(LeftMainHeader)); diff --git a/src/components/left/settings/folders/FolderIconPickerMenu.tsx b/src/components/left/settings/folders/FolderIconPickerMenu.tsx new file mode 100644 index 000000000..5a51df7a3 --- /dev/null +++ b/src/components/left/settings/folders/FolderIconPickerMenu.tsx @@ -0,0 +1,58 @@ +import { memo, useCallback } from '@teact'; + +import type { ApiSticker } from '../../../../api/types'; + +import { folderIconMap } from '../../../../util/folderIconMap'; + +import CustomEmojiPicker from '../../../common/CustomEmojiPicker'; +import Icon from '../../../common/icons/Icon'; +import Menu from '../../../ui/Menu'; + +export type OwnProps = { + isOpen: boolean; + onEmojiSelect: (emoji: string | ApiSticker) => void; + onClose: () => void; +}; + +const FolderIconPickerMenu = ({ + isOpen, + onEmojiSelect, + onClose, +}: OwnProps) => { + const handleEmojiSelect = useCallback((sticker: string | ApiSticker) => { + onEmojiSelect(sticker); + onClose(); + }, [onClose, onEmojiSelect]); + + return ( + +
+
+ { + Object.keys(folderIconMap).map((emoji) => ( +
handleEmojiSelect(emoji)}> + +
+ )) + } +
+ handleEmojiSelect(emoji)} + onContextMenuClick={onClose} + onContextMenuClose={onClose} + /> +
+
+ ); +}; + +export default memo(FolderIconPickerMenu); diff --git a/src/components/left/settings/folders/SettingsFolders.scss b/src/components/left/settings/folders/SettingsFolders.scss index 1d7b54c10..7865332a0 100644 --- a/src/components/left/settings/folders/SettingsFolders.scss +++ b/src/components/left/settings/folders/SettingsFolders.scss @@ -99,7 +99,7 @@ } .settings-sortable-item .Button { - margin-right: -1rem; + margin-inline-end: -1rem; &:hover, &:active { background-color: transparent !important; @@ -148,7 +148,7 @@ } .color-picker-item { - cursor: pointer; + cursor: var(--custom-cursor, pointer); flex-shrink: 0; @@ -214,7 +214,7 @@ .settings-folders-color-circle { position: absolute; top: 50%; - right: 2.5rem; + inset-inline-end: 2.5rem; transform: translateY(-50%); width: 1.25rem; @@ -223,3 +223,42 @@ background-color: var(--accent-color); } + +.settings-folders-input-container { + position: relative; + display: flex; + align-items: center; + align-self: stretch; +} + +.settings-folders-input-with-icon .form-control { + padding-inline-end: 3rem; +} + +.settings-folders-icon-picker { + --custom-emoji-size: 2rem; + + position: absolute; + inset-inline-end: 0.5rem; + font-size: 2rem; + color: var(--color-text-secondary); +} + +.settings-folders-icon-picker-button { + cursor: var(--custom-cursor, pointer); + display: flex; + align-items: center; + justify-content: center; +} + +.settings-folders-icon-picker-menu .bubble { + --offset-y: 16rem !important; + --offset-x: 6rem; +} + +.settings-folders-icon-picker-menu-folders { + display: flex; + justify-content: space-between; + border-bottom: 1px solid var(--color-borders); + color: var(--color-text-secondary); +} diff --git a/src/components/left/settings/folders/SettingsFolders.tsx b/src/components/left/settings/folders/SettingsFolders.tsx index 87b57620a..0e7ef2aed 100644 --- a/src/components/left/settings/folders/SettingsFolders.tsx +++ b/src/components/left/settings/folders/SettingsFolders.tsx @@ -7,6 +7,7 @@ import type { FolderEditDispatch, FoldersState } from '../../../../hooks/reducer import { SettingsScreens } from '../../../../types'; import { selectChatFilters } from '../../../../hooks/reducers/useFoldersReducer'; +import useAppLayout from '../../../../hooks/useAppLayout'; import SettingsFoldersChatFilters from './SettingsFoldersChatFilters'; import SettingsFoldersEdit, { ERROR_NO_CHATS, ERROR_NO_TITLE } from './SettingsFoldersEdit'; @@ -34,6 +35,8 @@ const SettingsFolders: FC = ({ isActive, onReset, }) => { + const { isMobile } = useAppLayout(); + const { openShareChatFolderModal, editChatFolder, @@ -164,6 +167,7 @@ const SettingsFolders: FC = ({ SettingsScreens.FoldersExcludedChats, ].includes(shownScreen)} onReset={onReset} + isMobile={isMobile} /> ); case SettingsScreens.FoldersCreateFolder: @@ -186,6 +190,7 @@ const SettingsFolders: FC = ({ isOnlyInvites={currentScreen === SettingsScreens.FoldersEditFolderInvites} onBack={onReset} onSaveFolder={handleSaveFolder} + isMobile={isMobile} /> ); case SettingsScreens.FoldersIncludedChats: diff --git a/src/components/left/settings/folders/SettingsFoldersEdit.tsx b/src/components/left/settings/folders/SettingsFoldersEdit.tsx index 06225119b..867b54e10 100644 --- a/src/components/left/settings/folders/SettingsFoldersEdit.tsx +++ b/src/components/left/settings/folders/SettingsFoldersEdit.tsx @@ -5,14 +5,20 @@ import { } from '../../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../../global'; -import type { ApiChatlistExportedInvite } from '../../../../api/types'; import type { FolderEditDispatch, FoldersState, } from '../../../../hooks/reducers/useFoldersReducer'; +import { + type ApiChatlistExportedInvite, + type ApiMessageEntity, + type ApiMessageEntityCustomEmoji, + ApiMessageEntityTypes, + type ApiSticker, +} from '../../../../api/types'; import { FOLDER_TITLE_MAX_LENGTH, STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config'; -import { selectCanShareFolder, selectIsCurrentUserPremium } from '../../../../global/selectors'; +import { selectCanShareFolder, selectCustomEmoji, selectIsCurrentUserPremium } from '../../../../global/selectors'; import { selectCurrentLimit } from '../../../../global/selectors/limits'; import buildClassName from '../../../../util/buildClassName'; import { isUserId } from '../../../../util/entities/ids'; @@ -24,16 +30,19 @@ import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEn import { selectChatFilters } from '../../../../hooks/reducers/useFoldersReducer'; import useHistoryBack from '../../../../hooks/useHistoryBack'; +import useLastCallback from '../../../../hooks/useLastCallback'; import useOldLang from '../../../../hooks/useOldLang'; import { getPeerColorClass } from '../../../../hooks/usePeerColor'; import AnimatedIconWithPreview from '../../../common/AnimatedIconWithPreview'; +import FolderIcon from '../../../common/FolderIcon'; import GroupChatInfo from '../../../common/GroupChatInfo'; import Icon from '../../../common/icons/Icon'; import PrivateChatInfo from '../../../common/PrivateChatInfo'; import FloatingActionButton from '../../../ui/FloatingActionButton'; import InputText from '../../../ui/InputText'; import ListItem from '../../../ui/ListItem'; +import FolderIconPickerMenu from './FolderIconPickerMenu'; type OwnProps = { state: FoldersState; @@ -47,6 +56,7 @@ type OwnProps = { onReset: () => void; onBack: () => void; onSaveFolder: (cb?: VoidFunction) => void; + isMobile?: boolean; }; type StateProps = { @@ -69,6 +79,8 @@ 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'; +const DEFAULT_FOLDER_ICON = '🗂'; + const SettingsFoldersEdit: FC = ({ state, dispatch, @@ -89,6 +101,7 @@ const SettingsFoldersEdit: FC = ({ chatListCount, onSaveFolder, isCurrentUserPremium, + isMobile, }) => { const { loadChatlistInvites, @@ -102,6 +115,7 @@ const SettingsFoldersEdit: FC = ({ const [isIncludedChatsListExpanded, setIsIncludedChatsListExpanded] = useState(false); const [isExcludedChatsListExpanded, setIsExcludedChatsListExpanded] = useState(false); + const [isIconPickerMenuOpen, setIsIconPickerMenuOpen] = useState(false); useEffect(() => { if (isRemoved) { @@ -159,10 +173,65 @@ const SettingsFoldersEdit: FC = ({ onBack, }); + const currentCustomEmoji = useMemo(() => state.folder.title.entities?.find( + (entity): entity is ApiMessageEntityCustomEmoji => + entity.type === ApiMessageEntityTypes.CustomEmoji && entity.offset === 0, + ), [state.folder.title]); + + const folderTitleMaxLength = useMemo(() => { + return FOLDER_TITLE_MAX_LENGTH - (currentCustomEmoji ? currentCustomEmoji.length : 0); + }, [currentCustomEmoji]); + + const setEmoticon = useCallback((_emoticon: string | ApiSticker) => { + let text = state.folder.title.text; + const entities: ApiMessageEntity[] = []; + let emoticon = DEFAULT_FOLDER_ICON; + if (currentCustomEmoji) { + const { offset, length } = currentCustomEmoji; + text = text.replace(text.substring(offset, offset + length), ''); + } + if (typeof _emoticon === 'string') { + emoticon = _emoticon; + } else { + const { id, emoji } = _emoticon; + + entities.push({ + type: ApiMessageEntityTypes.CustomEmoji, + documentId: id, + offset: 0, + length: emoji?.length || 2, + }); + if (emoji) { + text = `${emoji}${text}`; + emoticon = emoji; + if (text.length > folderTitleMaxLength) { + text = text.slice(0, folderTitleMaxLength); + } + } + } + + dispatch({ type: 'setEmoticon', payload: emoticon }); + dispatch({ type: 'setTitle', payload: { + text, + entities, + } }); + }, [dispatch, currentCustomEmoji, state.folder.title, folderTitleMaxLength]); + const handleChange = useCallback((event: React.ChangeEvent) => { const { currentTarget } = event; - dispatch({ type: 'setTitle', payload: currentTarget.value.trim() }); - }, [dispatch]); + + let title = currentTarget.value; + + if (currentCustomEmoji) { + const { emoji } = selectCustomEmoji(getGlobal(), currentCustomEmoji.documentId); + title = `${emoji}${title}`; + } + + dispatch({ type: 'setTitle', payload: { + text: title, + entities: currentCustomEmoji ? [currentCustomEmoji] : [], + } }); + }, [dispatch, currentCustomEmoji]); const handleSubmit = useCallback(() => { dispatch({ type: 'setIsLoading', payload: true }); @@ -287,6 +356,27 @@ const SettingsFoldersEdit: FC = ({ ); } + const handleEmojiSelect = useLastCallback((emoji: string | ApiSticker) => { + setEmoticon(emoji); + }); + + const handleIconPickerClose = useLastCallback(() => { + setIsIconPickerMenuOpen(false); + }); + + const handleIconPickerOpen = useLastCallback(() => { + setIsIconPickerMenuOpen(true); + }); + + const titleText = useMemo(() => { + let title = state.folder.title.text; + if (currentCustomEmoji) { + const { offset, length } = currentCustomEmoji; + title = title.substring(offset + length, title.length); + } + return title; + }, [state.folder.title.text, currentCustomEmoji]); + return (
@@ -303,15 +393,36 @@ const SettingsFoldersEdit: FC = ({ {lang('FilterIncludeInfo')}

)} +
+ - + {!isMobile && ( +
+
+ +
+ +
+ )} +
{!isOnlyInvites && ( diff --git a/src/components/left/settings/folders/SettingsFoldersMain.tsx b/src/components/left/settings/folders/SettingsFoldersMain.tsx index e90bb3484..39dbb04fa 100644 --- a/src/components/left/settings/folders/SettingsFoldersMain.tsx +++ b/src/components/left/settings/folders/SettingsFoldersMain.tsx @@ -5,6 +5,7 @@ import { import { getActions, withGlobal } from '../../../../global'; import type { ApiChatFolder } from '../../../../api/types'; +import type { TabsPosition } from '../../../../types'; import { ALL_FOLDER_ID, STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config'; import { getFolderDescriptionText } from '../../../../global/helpers'; @@ -20,6 +21,7 @@ import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEn import { useFolderManagerForChatsCount } from '../../../../hooks/useFolderManager'; import useHistoryBack from '../../../../hooks/useHistoryBack'; import useLang from '../../../../hooks/useLang'; +import useLastCallback from '../../../../hooks/useLastCallback'; import { getPeerColorClass } from '../../../../hooks/usePeerColor'; import usePreviousDeprecated from '../../../../hooks/usePreviousDeprecated'; @@ -30,12 +32,14 @@ import Checkbox from '../../../ui/Checkbox'; import Draggable from '../../../ui/Draggable'; import ListItem from '../../../ui/ListItem'; import Loading from '../../../ui/Loading'; +import RadioGroup from '../../../ui/RadioGroup'; type OwnProps = { isActive?: boolean; onCreateFolder: () => void; onEditFolder: (folder: ApiChatFolder) => void; onReset: () => void; + isMobile?: boolean; }; type StateProps = { @@ -45,6 +49,7 @@ type StateProps = { maxFolders: number; isPremium?: boolean; areTagsEnabled?: boolean; + tabsPosition: TabsPosition; }; type SortState = { @@ -67,6 +72,8 @@ const SettingsFoldersMain: FC = ({ recommendedChatFolders, maxFolders, areTagsEnabled, + tabsPosition, + isMobile, }) => { const { loadRecommendedChatFolders, @@ -76,6 +83,7 @@ const SettingsFoldersMain: FC = ({ sortChatFolders, toggleDialogFilterTags, openPremiumModal, + setSharedSettingOption, } = getActions(); const [state, setState] = useState({ @@ -207,6 +215,10 @@ const SettingsFoldersMain: FC = ({ }); }, [sortChatFolders]); + const handleTabsPositionChange = useLastCallback((value: string) => { + setSharedSettingOption({ tabsPosition: value as TabsPosition }); + }); + const canCreateNewFolder = useMemo(() => { return !isPremium || Object.keys(foldersById).length < maxFolders - 1; }, [foldersById, isPremium, maxFolders]); @@ -411,6 +423,24 @@ const SettingsFoldersMain: FC = ({ {!isPremium && }
+ {!isMobile && ( +
+

{lang('TabsPosition')}

+ + +
+ )}
); }; @@ -431,6 +461,7 @@ export default memo(withGlobal( recommendedChatFolders, maxFolders: selectCurrentLimit(global, 'dialogFilters'), areTagsEnabled, + tabsPosition: global.sharedState.settings.tabsPosition, }; }, )(SettingsFoldersMain)); diff --git a/src/components/main/FoldersSidebar.module.scss b/src/components/main/FoldersSidebar.module.scss new file mode 100644 index 000000000..7522d105a --- /dev/null +++ b/src/components/main/FoldersSidebar.module.scss @@ -0,0 +1,94 @@ +.root { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + + width: var(--tabs-sidebar-width); + height: 100%; + + background-color: var(--color-background-sidebar); + + :global { + .Menu .bubble { + --offset-y: 3.5rem; + --offset-x: 1rem; + + overflow-y: auto; + min-width: 17rem; + max-height: calc(100 * var(--vh) - 3.5rem); + } + .MenuItem .Toggle { + margin-inline-start: auto; + } + + .MenuItem.compact .Toggle { + transform: scale(0.75); + margin-inline-end: -0.125rem; + } + + .MenuItem.compact .Switcher { + transform: scale(0.75); + } + .account-menu-item { + --custom-emoji-size: 1rem; + + &-test { + position: absolute; + z-index: 1; + bottom: 0.0625rem; + left: 2.875rem; + + font-size: 0.5rem; + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + } + + .account-avatar { + margin-inline: 0.375rem 1.125rem; + } + + .fullName { + margin: 0; + padding-top: 0.1875rem; + font-size: 1em; + line-height: 1; + } + } + } +} + +.tabs { + overflow-y: auto; + display: flex; + flex-direction: column; + flex-grow: 1; + + padding-inline: 0; + + font-size: 0.625rem; + line-height: 0.75rem; + + background-color: var(--color-background-sidebar); +} + +.icon { + font-size: 1.5rem; + color: var(--color-text-secondary); +} + +.menuButton { + width: var(--tabs-sidebar-width); + height: 3.5rem; + border-radius: 0; +} + +.divider { + width: 100%; + height: 1px; + background-color: var(--color-interactive-buffered); +} + +.hideMenuButton { + visibility: hidden; +} diff --git a/src/components/main/FoldersSidebar.tsx b/src/components/main/FoldersSidebar.tsx new file mode 100644 index 000000000..128aa7ae2 --- /dev/null +++ b/src/components/main/FoldersSidebar.tsx @@ -0,0 +1,206 @@ +import { memo, useEffect, useMemo, useRef } from '@teact'; +import { getActions, withGlobal } from '../../global'; + +import type { ApiChatFolder, ApiChatlistExportedInvite, ApiMessageEntityCustomEmoji } from '../../api/types'; +import { LeftColumnContent, SettingsScreens } from '../../types'; + +import { selectTabState } from '../../global/selectors'; +import { selectCurrentLimit } from '../../global/selectors/limits'; +import { IS_TAURI } from '../../util/browser/globalEnvironment'; +import { IS_MAC_OS } from '../../util/browser/windowEnvironment'; +import buildClassName from '../../util/buildClassName'; + +import useFolderTabs from '../../hooks/useFolderTabs'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; +import useScrolledState from '../../hooks/useScrolledState'; + +import FolderIcon from '../common/FolderIcon'; +import Icon from '../common/icons/Icon'; +import MainMenuDropdown from '../common/MainMenuDropdown'; +import Button from '../ui/Button'; +import Folder from '../ui/Folder'; + +import styles from './FoldersSidebar.module.scss'; + +type StateProps = { + chatFoldersById: Record; + folderInvitesById: Record; + orderedFolderIds?: number[]; + activeChatFolder: number; + maxFolders: number; + maxChatLists: number; + maxFolderInvites: number; +}; + +type OwnProps = { + isActive: boolean; +}; + +const FIRST_FOLDER_INDEX = 0; + +const FoldersSidebar = ({ + chatFoldersById, + orderedFolderIds, + activeChatFolder, + maxFolders, + maxChatLists, + folderInvitesById, + maxFolderInvites, + isActive, +}: OwnProps & StateProps) => { + const { + loadChatFolders, + setActiveChatFolder, + openLeftColumnContent, + openSettingsScreen, + } = getActions(); + + const tabsRef = useRef(); + + useEffect(() => { + loadChatFolders(); + }, []); + + const scrollChatListToTop = useLastCallback(() => { + const activeList = document.querySelector('#LeftColumn .chat-list.Transition_slide-active'); + activeList?.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }); + + const { folderTabs } = useFolderTabs({ + sidebarMode: true, + orderedFolderIds, + chatFoldersById, + maxFolders, + maxChatLists, + folderInvitesById, + maxFolderInvites, + }); + + const { + handleScroll, + isAtBeginning, + isAtEnd, + } = useScrolledState(); + + const lang = useLang(); + + const handleSwitchTab = useLastCallback((index: number) => { + openLeftColumnContent({ contentKey: LeftColumnContent.ChatList }); + openSettingsScreen({ screen: undefined }); + setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true }); + if (activeChatFolder === index) { + scrollChatListToTop(); + } + + tabsRef.current?.children[index]?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }); + + const handleSettingsClick = useLastCallback(() => { + openLeftColumnContent({ contentKey: LeftColumnContent.Settings }); + openSettingsScreen({ screen: SettingsScreens.Folders }); + }); + + // Prevent `activeTab` pointing at non-existing folder after update + useEffect(() => { + if (!folderTabs?.length) { + return; + } + + if (activeChatFolder >= folderTabs.length) { + setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX }); + } + }, [activeChatFolder, folderTabs, setActiveChatFolder]); + + const MainButton = useMemo(() => { + return ({ onTrigger, isOpen }: { onTrigger: () => void; isOpen?: boolean }) => ( + + ); + }, [lang]); + + if (!isActive) { + return undefined; + } + + return ( +
+ + {!isAtBeginning &&
} +
+ {folderTabs?.map((tab, i) => ( + + )} + /> + ))} +
+ {!isAtEnd &&
} + +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { + chatFolders: { + byId: chatFoldersById, + orderedIds: orderedFolderIds, + invites: folderInvitesById, + }, + } = global; + const { activeChatFolder } = selectTabState(global); + + return { + chatFoldersById, + folderInvitesById, + orderedFolderIds, + activeChatFolder, + maxFolders: selectCurrentLimit(global, 'dialogFilters'), + maxFolderInvites: selectCurrentLimit(global, 'chatlistInvites'), + maxChatLists: selectCurrentLimit(global, 'chatlistJoined'), + }; + }, +)(FoldersSidebar)); diff --git a/src/components/main/Main.scss b/src/components/main/Main.scss index f01cdb497..8c873cb09 100644 --- a/src/components/main/Main.scss +++ b/src/components/main/Main.scss @@ -3,6 +3,10 @@ height: 100%; text-align: left; + &.tabs-sidebar-visible { + grid-template-columns: auto auto 1fr; + } + @media (min-width: 1276px) { position: relative; } @@ -212,3 +216,12 @@ } } } + +@media (max-width: 925px) { + .tabs-sidebar-visible { + #LeftColumn { + left: var(--tabs-sidebar-width) !important; + width: 21.5rem !important; + } + } +} diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 041d379a2..21d079e3f 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -11,9 +11,10 @@ import { getActions, getGlobal, withGlobal } from '../../global'; import type { ApiChatFolder, ApiLimitTypeWithModal, ApiUser } from '../../api/types'; import type { TabState } from '../../global/types'; -import { BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER } from '../../config'; +import { BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER, TABS_POSITION_LEFT } from '../../config'; import { requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { + selectAreFoldersPresent, selectCanAnimateInterface, selectChatFolder, selectChatMessage, @@ -79,6 +80,7 @@ import DeleteFolderDialog from './DeleteFolderDialog.async'; import Dialogs from './Dialogs.async'; import DownloadManager from './DownloadManager'; import DraftRecipientPicker from './DraftRecipientPicker.async'; +import FoldersSidebar from './FoldersSidebar'; import ForwardRecipientPicker from './ForwardRecipientPicker.async'; import GameModal from './GameModal'; import HistoryCalendar from './HistoryCalendar.async'; @@ -145,6 +147,7 @@ type StateProps = { isSynced?: boolean; isAccountFrozen?: boolean; isAppConfigLoaded?: boolean; + isFoldersSidebarShown: boolean; }; const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min @@ -199,6 +202,7 @@ const Main = ({ currentUserId, isAccountFrozen, isAppConfigLoaded, + isFoldersSidebarShown, }: OwnProps & StateProps) => { const { initMain, @@ -518,6 +522,7 @@ const Main = ({ isNarrowMessageList && 'narrow-message-list', shouldSkipHistoryAnimations && 'history-animation-disabled', isFullscreen && 'is-fullscreen', + isFoldersSidebarShown && 'tabs-sidebar-visible', ); const handleBlur = useLastCallback(() => { @@ -549,7 +554,8 @@ const Main = ({ return (
- + + @@ -637,7 +643,7 @@ export default memo(withGlobal( deleteFolderDialogModal, } = selectTabState(global); - const { wasTimeFormatSetManually } = selectSharedSettings(global); + const { wasTimeFormatSetManually, tabsPosition } = selectSharedSettings(global); const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId); const gameTitle = gameMessage?.content.game?.title; @@ -694,6 +700,7 @@ export default memo(withGlobal( isSynced: global.isSynced, isAccountFrozen, isAppConfigLoaded: global.isAppConfigLoaded, + isFoldersSidebarShown: tabsPosition === TABS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global), }; }, )(Main)); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 74ee4db7a..340fae4ef 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -491,6 +491,7 @@ const MessageList = ({ }); } else { clearTimeout(scrollSnapDisabledTimerRef.current); + scrollSnapDisabledTimerRef.current = undefined; requestMutation(() => { removeExtraClass(container, BOTTOM_SNAP_CLASS); }); @@ -512,7 +513,10 @@ const MessageList = ({ updateStickyDates(container); } - updateBottomSnapClass(); + // Check if scroll should be snapped, but only if there's no new message animation in progress + if (scrollSnapDisabledTimerRef.current === undefined) { + updateBottomSnapClass(); + } runDebouncedForScroll(() => { const global = getGlobal(); @@ -658,12 +662,14 @@ const MessageList = ({ if (wasMessageAdded) { clearTimeout(scrollSnapDisabledTimerRef.current); + scrollSnapDisabledTimerRef.current = undefined; removeExtraClass(container, BOTTOM_SNAP_CLASS); scrollSnapDisabledTimerRef.current = window.setTimeout(() => { requestMutation(() => { addExtraClass(container, BOTTOM_SNAP_CLASS); + scrollSnapDisabledTimerRef.current = undefined; }); }, MESSAGE_ANIMATION_DURATION); } diff --git a/src/components/modals/paidReaction/PaidReactionModal.tsx b/src/components/modals/paidReaction/PaidReactionModal.tsx index d000443bd..e290de7b0 100644 --- a/src/components/modals/paidReaction/PaidReactionModal.tsx +++ b/src/components/modals/paidReaction/PaidReactionModal.tsx @@ -208,7 +208,7 @@ const PaidReactionModal = ({ color="translucent" className={buildClassName(styles.sendAsPeerMenuButton, isOpen ? 'active' : '')} onClick={onTrigger} - ariaLabel={lang('AccDescrOpenMenu2')} + ariaLabel={lang('AriaLabelOpenMenu')} > = ({ color="translucent" className={isOpen ? 'active' : ''} onClick={onTrigger} - ariaLabel={lang('AccDescrOpenMenu2')} + ariaLabel={lang('AriaLabelOpenMenu')} > diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index f10d23da4..a7374af70 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -571,13 +571,13 @@ function Story({ color="translucent-white" onClick={onTrigger} className={buildClassName(styles.button, isOpen && 'active')} - ariaLabel={oldLang('AccDescrOpenMenu2')} + ariaLabel={lang('AriaLabelOpenMenu')} > ); }; - }, [isMobile, oldLang]); + }, [isMobile, lang]); function renderStoriesTabs() { return ( diff --git a/src/components/story/helpers/ribbonAnimation.ts b/src/components/story/helpers/ribbonAnimation.ts index 2155082bd..b07815e42 100644 --- a/src/components/story/helpers/ribbonAnimation.ts +++ b/src/components/story/helpers/ribbonAnimation.ts @@ -19,7 +19,7 @@ export function animateOpening(isArchived?: boolean) { cancelDelayedCallbacks(); const { - container, toggler, leftMainHeader, ribbonPeers, toggleAvatars, + container, toggler, leftMainHeader, ribbonPeers, toggleAvatars, sidebar, } = getHTMLElements(isArchived); if (!toggler || !toggleAvatars || !ribbonPeers || !container || !leftMainHeader) { @@ -28,6 +28,7 @@ export function animateOpening(isArchived?: boolean) { const { bottom: headerBottom, right: headerRight } = leftMainHeader.getBoundingClientRect(); const toTop = headerBottom + RIBBON_OFFSET; + const sidebarWidth = sidebar ? sidebar.getBoundingClientRect().width : 0; // Toggle avatars are in the reverse order const lastToggleAvatar = toggleAvatars[0]; @@ -57,10 +58,11 @@ export function animateOpening(isArchived?: boolean) { width: fromWidth, } = toggleAvatar.getBoundingClientRect(); - const { - left: toLeft, - width: toWidth, - } = peer.getBoundingClientRect(); + fromLeft -= sidebarWidth; + + const peerBounds = peer.getBoundingClientRect(); + const toLeft = peerBounds.left - sidebarWidth; + const toWidth = peerBounds.width; if (toLeft > headerRight) { return; @@ -164,13 +166,14 @@ export function animateClosing(isArchived?: boolean) { toggleAvatars, ribbonPeers, leftMainHeader, + sidebar, } = getHTMLElements(isArchived); if (!toggler || !toggleAvatars || !ribbonPeers || !container || !leftMainHeader) { return; } const { right: headerRight } = leftMainHeader.getBoundingClientRect(); - + const sidebarWidth = sidebar ? sidebar.getBoundingClientRect().width : 0; // Toggle avatars are in the reverse order const lastToggleAvatar = toggleAvatars[0]; const firstToggleAvatar = toggleAvatars[toggleAvatars.length - 1]; @@ -192,11 +195,11 @@ export function animateClosing(isArchived?: boolean) { if (!toggleAvatar) return; - const { - top: fromTop, - left: fromLeft, - width: fromWidth, - } = peer.getBoundingClientRect(); + const peerBounds = peer.getBoundingClientRect(); + + const fromTop = peerBounds.top; + const fromLeft = peerBounds.left - sidebarWidth; + const fromWidth = peerBounds.width; let { left: toLeft, @@ -204,6 +207,8 @@ export function animateClosing(isArchived?: boolean) { top: toTop, } = toggleAvatar.getBoundingClientRect(); + toLeft -= sidebarWidth; + if (fromLeft > headerRight) { return; } @@ -306,6 +311,7 @@ function getHTMLElements(isArchived?: boolean) { const leftMainHeader = container.querySelector('.left-header'); const ribbonPeers = ribbon?.querySelectorAll(`.${ribbonStyles.peer}`); const toggleAvatars = toggler?.querySelectorAll('.Avatar'); + const sidebar = document.getElementById('FoldersSidebar'); return { container, @@ -313,6 +319,7 @@ function getHTMLElements(isArchived?: boolean) { leftMainHeader, ribbonPeers, toggleAvatars, + sidebar, }; } diff --git a/src/components/ui/Folder.module.scss b/src/components/ui/Folder.module.scss new file mode 100644 index 000000000..a448412a4 --- /dev/null +++ b/src/components/ui/Folder.module.scss @@ -0,0 +1,115 @@ +.folder { + cursor: var(--custom-cursor, pointer); + + position: relative; + + display: flex; + flex-direction: column; + flex-shrink: 0; + gap: 0.375rem; + align-items: center; + justify-content: center; + + width: var(--tabs-sidebar-width); + min-height: 4.5rem; + padding-right: 0.25rem; + padding-left: 0.375rem; + border-radius: 0; + + &::before { + content: ''; + + position: absolute; + top: 0.625rem; + bottom: 0.625rem; + left: 0; + transform: translateX(-0.375rem) scaleY(0.5); + + width: 0.3125rem; + border-start-end-radius: var(--border-radius-default); + border-end-end-radius: var(--border-radius-default); + + background: var(--color-primary); + + transition: transform var(--layer-transition); + + body.no-page-transitions & { + transition: none; + } + } + + .inner { + --emoji-size: 0.75rem; + --custom-emoji-size: 0.75rem; + + font-size: 0.625rem; + } + + .title { + overflow: hidden; + + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + text-align: center; + overflow-wrap: anywhere; + } + + .icon { + --emoji-size: 2rem; + --custom-emoji-size: 2rem; + + position: relative; + font-size: 2.25rem; // Font icons are smaller than custom emojis + color: var(--color-text-secondary); + } + + .badge { + position: absolute; + z-index: 1; + top: -0.25rem; + left: 1.25rem; + + display: flex; + align-items: flex-start; + justify-content: center; + + padding: 0.125rem 0.375rem; + border: 2px solid var(--color-background-sidebar); + border-radius: 0.75rem; + + font-size: 0.75rem; + font-weight: var(--font-weight-semibold); + line-height: normal; + color: var(--color-white); + + background: var(--color-text-secondary); + + &-active { + color: var(--color-white); + background: var(--color-primary); + } + } + + .blocked { + vertical-align: middle; + } + + &:hover { + background: var(--color-interactive-element-hover); + } + + &.active { + &::before { + transform: translateX(0) scaleY(1); + } + + .icon, .title { + color: var(--color-primary); + } + + .badge { + color: var(--color-white); + background: var(--color-primary); + } + } +} diff --git a/src/components/ui/Folder.tsx b/src/components/ui/Folder.tsx new file mode 100644 index 000000000..fa9b5ad92 --- /dev/null +++ b/src/components/ui/Folder.tsx @@ -0,0 +1,131 @@ +import type { TeactNode } from '../../lib/teact/teact'; +import { useRef } from '../../lib/teact/teact'; + +import type { MenuItemContextAction } from './ListItem'; + +import { MouseButton } from '../../util/browser/windowEnvironment'; +import buildClassName from '../../util/buildClassName'; + +import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; +import { useFastClick } from '../../hooks/useFastClick'; +import useLastCallback from '../../hooks/useLastCallback'; + +import Icon from '../common/icons/Icon'; +import Menu from './Menu'; +import MenuItem from './MenuItem'; +import MenuSeparator from './MenuSeparator'; + +import styles from './Folder.module.scss'; + +type OwnProps = { + className?: string; + title: TeactNode; + isActive?: boolean; + isBlocked?: boolean; + badgeCount?: number; + isBadgeActive?: boolean; + contextActions?: MenuItemContextAction[]; + contextRootElementSelector?: string; + icon?: TeactNode; + clickArg?: number; + onClick?: (arg: number) => void; +}; + +const Folder = ({ + className, + title, + isActive, + isBlocked, + badgeCount, + isBadgeActive, + contextActions, + contextRootElementSelector, + icon, + clickArg, + onClick, +}: OwnProps) => { + const folderRef = useRef(); + + const { + contextMenuAnchor, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose, + handleContextMenuHide, isContextMenuOpen, + } = useContextMenuHandlers(folderRef, !contextActions); + + const { handleClick, handleMouseDown } = useFastClick((e: React.MouseEvent) => { + if (contextActions && (e.button === MouseButton.Secondary || !onClick)) { + handleBeforeContextMenu(e); + } + + if (e.type === 'mousedown' && e.button !== MouseButton.Main) { + return; + } + + onClick?.(clickArg!); + }); + + const getTriggerElement = useLastCallback(() => folderRef.current); + const getRootElement = useLastCallback( + () => (contextRootElementSelector ? folderRef.current!.closest(contextRootElementSelector) : document.body), + ); + const getMenuElement = useLastCallback( + () => document.querySelector(`.${styles.contextMenu} .bubble`), + ); + const getLayout = useLastCallback(() => ({ withPortal: true })); + + return ( +
+
+ {icon} + {Boolean(badgeCount) && ( + {badgeCount} + )} +
+ +
+ {isBlocked && } + {title} +
+
+ + {contextActions && contextMenuAnchor !== undefined && ( + + {contextActions.map((action) => ( + ('isSeparator' in action) ? ( + + ) : ( + + {action.title} + + ) + ))} + + )} +
+ ); +}; + +export default Folder; diff --git a/src/components/ui/Tab.tsx b/src/components/ui/Tab.tsx index 0079f2252..683f4f0da 100644 --- a/src/components/ui/Tab.tsx +++ b/src/components/ui/Tab.tsx @@ -32,6 +32,7 @@ type OwnProps = { clickArg?: number; contextActions?: MenuItemContextAction[]; contextRootElementSelector?: string; + icon?: TeactNode; }; const classNames = { @@ -49,6 +50,7 @@ const Tab = ({ previousActiveTab, contextActions, contextRootElementSelector, + icon, clickArg, onClick, }: OwnProps) => { @@ -138,6 +140,7 @@ const Tab = ({ onContextMenu={handleContextMenu} ref={tabRef} > + {icon} {typeof title === 'string' ? renderText(title) : title} {Boolean(badgeCount) && ( diff --git a/src/components/ui/TabList.tsx b/src/components/ui/TabList.tsx index bbde90a6b..0c6a15ecb 100644 --- a/src/components/ui/TabList.tsx +++ b/src/components/ui/TabList.tsx @@ -1,6 +1,7 @@ import type { TeactNode } from '../../lib/teact/teact'; import { memo, useEffect, useRef } from '../../lib/teact/teact'; +import type { ApiMessageEntityCustomEmoji } from '../../api/types'; import type { MenuItemContextAction } from './ListItem'; import animateHorizontalScroll from '../../util/animateHorizontalScroll'; @@ -22,6 +23,8 @@ export type TabWithProperties = { isBlocked?: boolean; isBadgeActive?: boolean; contextActions?: MenuItemContextAction[]; + emoticon?: string | ApiMessageEntityCustomEmoji; + noTitleAnimations?: boolean; }; type OwnProps = { diff --git a/src/config.ts b/src/config.ts index daab0f46a..9029c5059 100644 --- a/src/config.ts +++ b/src/config.ts @@ -141,6 +141,10 @@ export const DEFAULT_MESSAGE_TEXT_SIZE_PX = 16; export const IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX = 17; export const MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX = 15; +export const TABS_POSITION_TOP = 'top'; +export const TABS_POSITION_LEFT = 'left'; +export const TABS_POSITION_DEFAULT = TABS_POSITION_TOP; + export const PREVIEW_AVATAR_COUNT = 3; export const DRAFT_DEBOUNCE = 10000; // 10s diff --git a/src/global/actions/ui/settings.ts b/src/global/actions/ui/settings.ts index 47b6e9480..f65ef927b 100644 --- a/src/global/actions/ui/settings.ts +++ b/src/global/actions/ui/settings.ts @@ -161,14 +161,14 @@ addActionHandler('openLeftColumnContent', (global, actions, payload): ActionRetu }); addActionHandler('openSettingsScreen', (global, actions, payload): ActionReturnType => { - const { screen = SettingsScreens.Main, tabId = getCurrentTabId() } = payload; + const { screen, tabId = getCurrentTabId() } = payload; const tabState = selectTabState(global, tabId); // Force settings only if new screen is passed, do not on resets - if (payload.screen) actions.openLeftColumnContent({ contentKey: LeftColumnContent.Settings, tabId }); + if (payload.screen !== undefined) actions.openLeftColumnContent({ contentKey: LeftColumnContent.Settings, tabId }); return updateTabState(global, { leftColumn: { ...tabState.leftColumn, - settingsScreen: screen, + settingsScreen: screen || SettingsScreens.Main, }, }, tabId); }); diff --git a/src/global/cache.ts b/src/global/cache.ts index b3eafbbbd..e44c25272 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -21,6 +21,7 @@ import { IS_SCREEN_LOCKED_CACHE_KEY, SAVED_FOLDER_ID, SHARED_STATE_CACHE_KEY, + TABS_POSITION_DEFAULT, } from '../config'; import { MAIN_IDB_STORE } from '../util/browser/idb'; import { isUserId } from '../util/entities/ids'; @@ -313,6 +314,7 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { cached.sharedState.settings = { canDisplayChatInTitle: untypedCached.settings.byKey.canDisplayChatInTitle, animationLevel: untypedCached.settings.byKey.animationLevel, + tabsPosition: untypedCached.settings.byKey.tabsPosition, messageSendKeyCombo: untypedCached.settings.byKey.messageSendKeyCombo, messageTextSize: untypedCached.settings.byKey.messageTextSize, performance: untypedCached.settings.performance, @@ -348,6 +350,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { cachedSharedSettings.performance = INITIAL_PERFORMANCE_STATE_MED; } + if (!cachedSharedSettings.tabsPosition) { + cachedSharedSettings.tabsPosition = TABS_POSITION_DEFAULT; + } + if (!cached.appConfig) { cached.appConfig = initialState.appConfig; } diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 730e3e0e7..ede9582b8 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -13,6 +13,7 @@ import { DEFAULT_VOLUME, IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, + TABS_POSITION_DEFAULT, } from '../config'; import { IS_IOS, IS_MAC_OS } from '../util/browser/windowEnvironment'; import { DEFAULT_APP_CONFIG } from '../limits'; @@ -79,6 +80,7 @@ export const INITIAL_SHARED_STATE: SharedState = { ? IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX : (IS_MAC_OS ? MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX : DEFAULT_MESSAGE_TEXT_SIZE_PX), animationLevel: ANIMATION_LEVEL_DEFAULT, + tabsPosition: TABS_POSITION_DEFAULT, messageSendKeyCombo: 'enter', performance: INITIAL_PERFORMANCE_STATE_MAX, shouldSkipWebAppCloseConfirmation: false, diff --git a/src/global/selectors/chats.ts b/src/global/selectors/chats.ts index 7cbd0c9bf..2dec98d36 100644 --- a/src/global/selectors/chats.ts +++ b/src/global/selectors/chats.ts @@ -383,3 +383,8 @@ export function selectIsChatRestricted(global: T, chatId: const activeRestrictions = selectActiveRestrictionReasons(global, chat.restrictionReasons); return activeRestrictions.length > 0; } + +export function selectAreFoldersPresent(global: T) { + const ids = global.chatFolders.orderedIds; + return Boolean(ids && ids.length > 1); +} diff --git a/src/global/types/sharedState.ts b/src/global/types/sharedState.ts index 414736cea..018746633 100644 --- a/src/global/types/sharedState.ts +++ b/src/global/types/sharedState.ts @@ -1,5 +1,5 @@ import type { ApiLanguage } from '../../api/types'; -import type { AnimationLevel, PerformanceType, Point, Size, ThemeKey, TimeFormat } from '../../types'; +import type { AnimationLevel, PerformanceType, Point, Size, TabsPosition, ThemeKey, TimeFormat } from '../../types'; export interface SharedState { settings: SharedSettings; @@ -14,6 +14,7 @@ export interface SharedSettings { performance: PerformanceType; messageTextSize: number; animationLevel: AnimationLevel; + tabsPosition: TabsPosition; // This can be deleted after September 2025, along with the corresponding migration wasAnimationLevelSetManually?: boolean; messageSendKeyCombo: 'enter' | 'ctrl-enter'; diff --git a/src/hooks/reducers/useFoldersReducer.ts b/src/hooks/reducers/useFoldersReducer.ts index a066d3199..477568c1d 100644 --- a/src/hooks/reducers/useFoldersReducer.ts +++ b/src/hooks/reducers/useFoldersReducer.ts @@ -1,8 +1,8 @@ import { getGlobal } from '../../global'; -import type { ApiChatFolder } from '../../api/types'; import type { IconName } from '../../types/icons'; import type { Dispatch, StateReducer } from '../useReducer'; +import { type ApiChatFolder } from '../../api/types'; import { selectChat } from '../../global/selectors'; import { omit, pick } from '../../util/iteratees'; @@ -109,14 +109,14 @@ export type FoldersState = { error?: string; folderId?: number; chatFilter: string; - folder: Omit; + folder: Omit; includeFilters?: FolderIncludeFilters; excludeFilters?: FolderExcludeFilters; }; export type FoldersActions = ( 'setTitle' | 'saveFilters' | 'editFolder' | 'reset' | 'setChatFilter' | 'setIsLoading' | 'setError' | 'editIncludeFilters' | 'editExcludeFilters' | 'setIncludeFilters' | 'setExcludeFilters' | 'setIsTouched' | - 'setFolderId' | 'setIsChatlist' | 'setColor' + 'setFolderId' | 'setIsChatlist' | 'setColor' | 'setEmoticon' ); export type FolderEditDispatch = Dispatch; @@ -140,7 +140,9 @@ const foldersReducer: StateReducer = ( ...state, folder: { ...state.folder, - title: { text: action.payload }, + title: typeof action.payload === 'string' + ? { ...state.folder.title, text: action.payload } + : { ...state.folder.title, ...action.payload }, }, isTouched: true, }; @@ -257,6 +259,16 @@ const foldersReducer: StateReducer = ( }, isTouched: true, }; + case 'setEmoticon': { + return { + ...state, + folder: { + ...state.folder, + emoticon: action.payload, + }, + isTouched: true, + }; + } case 'reset': return INITIAL_STATE; default: diff --git a/src/hooks/useFolderTabs.ts b/src/hooks/useFolderTabs.ts new file mode 100644 index 000000000..de0d9a096 --- /dev/null +++ b/src/hooks/useFolderTabs.ts @@ -0,0 +1,235 @@ +import { type TeactNode, useMemo } from '../lib/teact/teact'; +import { getActions, getGlobal } from '../global'; + +import type { ApiMessageEntity, ApiMessageEntityCustomEmoji } from '../api/types'; +import type { MenuItemContextAction } from '../components/ui/ListItem'; +import type { TabWithProperties } from '../components/ui/TabList'; +import { type ApiChatFolder, type ApiChatlistExportedInvite, ApiMessageEntityTypes } from '../api/types'; +import { SettingsScreens } from '../types'; + +import { ALL_FOLDER_ID } from '../config'; +import { selectCanShareFolder } from '../global/selectors'; +import { MEMO_EMPTY_ARRAY } from '../util/memo'; +import { renderTextWithEntities } from '../components/common/helpers/renderTextWithEntities'; +import useAppLayout from './useAppLayout'; +import { useFolderManagerForUnreadChatsByFolder, useFolderManagerForUnreadCounters } from './useFolderManager'; +import useLang from './useLang'; +import useLastCallback from './useLastCallback'; + +type FolderNameOptions = { + text: string; + entities?: ApiMessageEntity[]; + noCustomEmojiPlayback?: boolean; + emojiSize?: number; +}; + +const useFolderTabs = ({ + sidebarMode, + orderedFolderIds, + chatFoldersById, + maxFolders, + maxChatLists, + folderInvitesById, + maxFolderInvites, +}: { + sidebarMode: boolean; + orderedFolderIds?: number[]; + chatFoldersById: Record; + maxFolders: number; + maxChatLists: number; + folderInvitesById: Record; + maxFolderInvites: number; +}) => { + const lang = useLang(); + const { isMobile } = useAppLayout(); + + const { + openShareChatFolderModal, + openDeleteChatFolderModal, + openEditChatFolder, + openLimitReachedModal, + markChatMessagesRead, + openSettingsScreen, + setSharedSettingOption, + } = getActions(); + + const allChatsFolder: ApiChatFolder = useMemo(() => { + return { + id: ALL_FOLDER_ID, + title: { text: orderedFolderIds?.[0] === ALL_FOLDER_ID ? lang('FilterAllChatsShort') : lang('FilterAllChats') }, + includedChatIds: MEMO_EMPTY_ARRAY, + excludedChatIds: MEMO_EMPTY_ARRAY, + emoticon: '💬', + } satisfies ApiChatFolder; + }, [orderedFolderIds, lang]); + + const displayedFolders = useMemo(() => { + return orderedFolderIds + ? orderedFolderIds.map((id) => { + if (id === ALL_FOLDER_ID) { + return allChatsFolder; + } + + return chatFoldersById[id] || {}; + }).filter(Boolean) + : undefined; + }, [chatFoldersById, allChatsFolder, orderedFolderIds]); + + 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) { + return undefined; + } + + return displayedFolders.map((folder, i) => { + const { id, title } = folder; + const isBlocked = id !== ALL_FOLDER_ID && i > maxFolders - 1; + const canShareFolder = selectCanShareFolder(getGlobal(), id); + const contextActions: MenuItemContextAction[] = []; + + if (canShareFolder) { + contextActions.push({ + title: lang('FilterShare'), + icon: 'link', + handler: () => { + const chatListCount = Object.values(chatFoldersById).reduce((acc, el) => acc + (el.isChatList ? 1 : 0), 0); + if (chatListCount >= maxChatLists && !folder.isChatList) { + openLimitReachedModal({ + limit: 'chatlistJoined', + }); + return; + } + + // Greater amount can be after premium downgrade + if (folderInvitesById[id]?.length >= maxFolderInvites) { + openLimitReachedModal({ + limit: 'chatlistInvites', + }); + return; + } + + openShareChatFolderModal({ + folderId: id, + }); + }, + }); + } + + if (id === ALL_FOLDER_ID) { + contextActions.push({ + title: lang('FilterEditFolders'), + icon: 'edit', + handler: () => { + openSettingsScreen({ screen: 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('FilterMenuDelete'), + icon: 'delete', + destructive: true, + handler: () => { + openDeleteChatFolderModal({ folderId: id }); + }, + }); + } + + if (!isMobile) { + contextActions.push({ + isSeparator: true, + }); + + contextActions.push({ + title: sidebarMode ? lang('TabsPositionTop') : lang('TabsPositionLeft'), + icon: 'forums', + handler: () => { + setSharedSettingOption({ tabsPosition: sidebarMode ? 'top' : 'left' }); + }, + }); + } + + const folderNameOptions: FolderNameOptions = { + text: title.text, + entities: title.entities, + noCustomEmojiPlayback: folder.noTitleAnimations, + }; + + let folderIcon: string | ApiMessageEntityCustomEmoji | undefined = folder.emoticon; + + if (sidebarMode) { + folderNameOptions.emojiSize = 10; + const currentCustomEmoji = title.entities?.find( + (entity): entity is ApiMessageEntityCustomEmoji => + entity.type === ApiMessageEntityTypes.CustomEmoji && entity.offset === 0); + if (currentCustomEmoji) { + folderIcon = currentCustomEmoji; + const { offset, length } = currentCustomEmoji; + + folderNameOptions.text = title.text.replace(title.text.substring(offset, offset + length), ''); + folderNameOptions.entities = title.entities?.filter((entity) => entity.offset !== offset).map((entity) => ({ + ...entity, + offset: entity.offset - length, + })); + } + } + + const folderName: TeactNode[] | string = renderTextWithEntities(folderNameOptions); + + return { + id, + title: folderName, + badgeCount: folderCountersById[id]?.chatsCount, + isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount), + isBlocked, + contextActions: contextActions?.length ? contextActions : undefined, + emoticon: folderIcon, + noTitleAnimations: folder.noTitleAnimations, + } satisfies TabWithProperties; + }); + }, [ + displayedFolders, maxFolders, folderCountersById, lang, chatFoldersById, maxChatLists, folderInvitesById, + maxFolderInvites, folderUnreadChatsCountersById, openSettingsScreen, sidebarMode, isMobile, + setSharedSettingOption, + ]); + + return { + displayedFolders, + folderTabs, + }; +}; + +export default useFolderTabs; diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index fd4cb87a0..b00076b85 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -74,6 +74,7 @@ $color-message-story-mention-to: #74bcff; --color-background-selected: #f4f4f5; --color-background-secondary: #f4f4f5; --color-background-secondary-accent: #e4e4e5; + --color-background-sidebar: #E4E4E5; --color-background-own: #{$color-light-green}; --color-background-own-selected: color.adjust($color-light-green, -10%); --color-text: #{$color-black}; @@ -220,6 +221,7 @@ $color-message-story-mention-to: #74bcff; --border-radius-forum-avatar: 33.3333%; --messages-container-width: 45.5rem; --right-column-width: 26.5rem; + --tabs-sidebar-width: 5rem; --window-controls-width: 0rem; --header-height: 3.5rem; --custom-emoji-size: 1.25rem; diff --git a/src/styles/icons.css b/src/styles/icons.css index d682c4f90..361d17744 100644 --- a/src/styles/icons.css +++ b/src/styles/icons.css @@ -3,8 +3,8 @@ font-weight: normal; font-style: normal; font-display: block; - src: url("./icons.woff2?33f6294c2f4a2ffb1e77473fb35bc539") format("woff2"), -url("./icons.woff?33f6294c2f4a2ffb1e77473fb35bc539") format("woff"); + src: url("./icons.woff2?cc3fadda4d577575d1b441ec5d6c8994") format("woff2"), +url("./icons.woff?cc3fadda4d577575d1b441ec5d6c8994") format("woff"); } .icon-char::before { @@ -294,621 +294,651 @@ url("./icons.woff?33f6294c2f4a2ffb1e77473fb35bc539") format("woff"); .icon-folder-badge::before { content: "\f15b"; } -.icon-folder::before { +.icon-folder-tabs-bot::before { content: "\f15c"; } -.icon-fontsize::before { +.icon-folder-tabs-channel::before { content: "\f15d"; } -.icon-forums::before { +.icon-folder-tabs-chat::before { content: "\f15e"; } -.icon-forward::before { +.icon-folder-tabs-chats::before { content: "\f15f"; } -.icon-fragment::before { +.icon-folder-tabs-folder::before { content: "\f160"; } -.icon-frozen-time::before { +.icon-folder-tabs-group::before { content: "\f161"; } -.icon-fullscreen::before { +.icon-folder-tabs-star::before { content: "\f162"; } -.icon-gifs::before { +.icon-folder-tabs-user::before { content: "\f163"; } -.icon-gift-transfer-inline::before { +.icon-folder::before { content: "\f164"; } -.icon-gift::before { +.icon-fontsize::before { content: "\f165"; } -.icon-group-filled::before { +.icon-forums::before { content: "\f166"; } -.icon-group::before { +.icon-forward::before { content: "\f167"; } -.icon-grouped-disable::before { +.icon-fragment::before { content: "\f168"; } -.icon-grouped::before { +.icon-frozen-time::before { content: "\f169"; } -.icon-hand-stop::before { +.icon-fullscreen::before { content: "\f16a"; } -.icon-hashtag::before { +.icon-gifs::before { content: "\f16b"; } -.icon-hd-photo::before { +.icon-gift-transfer-inline::before { content: "\f16c"; } -.icon-heart-outline::before { +.icon-gift::before { content: "\f16d"; } -.icon-heart::before { +.icon-group-filled::before { content: "\f16e"; } -.icon-help::before { +.icon-group::before { content: "\f16f"; } -.icon-info-filled::before { +.icon-grouped-disable::before { content: "\f170"; } -.icon-info::before { +.icon-grouped::before { content: "\f171"; } -.icon-install::before { +.icon-hand-stop::before { content: "\f172"; } -.icon-italic::before { +.icon-hashtag::before { content: "\f173"; } -.icon-key::before { +.icon-hd-photo::before { content: "\f174"; } -.icon-keyboard::before { +.icon-heart-outline::before { content: "\f175"; } -.icon-lamp::before { +.icon-heart::before { content: "\f176"; } -.icon-language::before { +.icon-help::before { content: "\f177"; } -.icon-large-pause::before { +.icon-info-filled::before { content: "\f178"; } -.icon-large-play::before { +.icon-info::before { content: "\f179"; } -.icon-link-badge::before { +.icon-install::before { content: "\f17a"; } -.icon-link-broken::before { +.icon-italic::before { content: "\f17b"; } -.icon-link::before { +.icon-key::before { content: "\f17c"; } -.icon-location::before { +.icon-keyboard::before { content: "\f17d"; } -.icon-lock-badge::before { +.icon-lamp::before { content: "\f17e"; } -.icon-lock::before { +.icon-language::before { content: "\f17f"; } -.icon-logout::before { +.icon-large-pause::before { content: "\f180"; } -.icon-loop::before { +.icon-large-play::before { content: "\f181"; } -.icon-mention::before { +.icon-link-badge::before { content: "\f182"; } -.icon-message-failed::before { +.icon-link-broken::before { content: "\f183"; } -.icon-message-pending::before { +.icon-link::before { content: "\f184"; } -.icon-message-read::before { +.icon-location::before { content: "\f185"; } -.icon-message-succeeded::before { +.icon-lock-badge::before { content: "\f186"; } -.icon-message::before { +.icon-lock::before { content: "\f187"; } -.icon-microphone-alt::before { +.icon-logout::before { content: "\f188"; } -.icon-microphone::before { +.icon-loop::before { content: "\f189"; } -.icon-monospace::before { +.icon-mention::before { content: "\f18a"; } -.icon-more-circle::before { +.icon-menu::before { content: "\f18b"; } -.icon-more::before { +.icon-message-failed::before { content: "\f18c"; } -.icon-move-caption-down::before { +.icon-message-pending::before { content: "\f18d"; } -.icon-move-caption-up::before { +.icon-message-read::before { content: "\f18e"; } -.icon-mute::before { +.icon-message-succeeded::before { content: "\f18f"; } -.icon-muted::before { +.icon-message::before { content: "\f190"; } -.icon-my-notes::before { +.icon-microphone-alt::before { content: "\f191"; } -.icon-new-chat-filled::before { +.icon-microphone::before { content: "\f192"; } -.icon-next::before { +.icon-monospace::before { content: "\f193"; } -.icon-nochannel::before { +.icon-more-circle::before { content: "\f194"; } -.icon-noise-suppression::before { +.icon-more::before { content: "\f195"; } -.icon-non-contacts::before { +.icon-move-caption-down::before { content: "\f196"; } -.icon-note::before { +.icon-move-caption-up::before { content: "\f197"; } -.icon-one-filled::before { +.icon-mute::before { content: "\f198"; } -.icon-open-in-new-tab::before { +.icon-muted::before { content: "\f199"; } -.icon-password-off::before { +.icon-my-notes::before { content: "\f19a"; } -.icon-pause::before { +.icon-new-chat-filled::before { content: "\f19b"; } -.icon-permissions::before { +.icon-next::before { content: "\f19c"; } -.icon-phone-discard-outline::before { +.icon-nochannel::before { content: "\f19d"; } -.icon-phone-discard::before { +.icon-noise-suppression::before { content: "\f19e"; } -.icon-phone::before { +.icon-non-contacts::before { content: "\f19f"; } -.icon-photo::before { +.icon-note::before { content: "\f1a0"; } -.icon-pin-badge::before { +.icon-one-filled::before { content: "\f1a1"; } -.icon-pin-list::before { +.icon-open-in-new-tab::before { content: "\f1a2"; } -.icon-pin::before { +.icon-password-off::before { content: "\f1a3"; } -.icon-pinned-chat::before { +.icon-pause::before { content: "\f1a4"; } -.icon-pinned-message::before { +.icon-permissions::before { content: "\f1a5"; } -.icon-pip::before { +.icon-phone-discard-outline::before { content: "\f1a6"; } -.icon-play-story::before { +.icon-phone-discard::before { content: "\f1a7"; } -.icon-play::before { +.icon-phone::before { content: "\f1a8"; } -.icon-poll::before { +.icon-photo::before { content: "\f1a9"; } -.icon-previous::before { +.icon-pin-badge::before { content: "\f1aa"; } -.icon-privacy-policy::before { +.icon-pin-list::before { content: "\f1ab"; } -.icon-proof-of-ownership::before { +.icon-pin::before { content: "\f1ac"; } -.icon-quote-text::before { +.icon-pinned-chat::before { content: "\f1ad"; } -.icon-quote::before { +.icon-pinned-message::before { content: "\f1ae"; } -.icon-radial-badge::before { +.icon-pip::before { content: "\f1af"; } -.icon-rating-icons-level1::before { +.icon-play-story::before { content: "\f1b0"; } -.icon-rating-icons-level10::before { +.icon-play::before { content: "\f1b1"; } -.icon-rating-icons-level2::before { +.icon-poll::before { content: "\f1b2"; } -.icon-rating-icons-level20::before { +.icon-previous::before { content: "\f1b3"; } -.icon-rating-icons-level3::before { +.icon-privacy-policy::before { content: "\f1b4"; } -.icon-rating-icons-level30::before { +.icon-proof-of-ownership::before { content: "\f1b5"; } -.icon-rating-icons-level4::before { +.icon-quote-text::before { content: "\f1b6"; } -.icon-rating-icons-level40::before { +.icon-quote::before { content: "\f1b7"; } -.icon-rating-icons-level5::before { +.icon-radial-badge::before { content: "\f1b8"; } -.icon-rating-icons-level50::before { +.icon-rating-icons-level1::before { content: "\f1b9"; } -.icon-rating-icons-level6::before { +.icon-rating-icons-level10::before { content: "\f1ba"; } -.icon-rating-icons-level60::before { +.icon-rating-icons-level2::before { content: "\f1bb"; } -.icon-rating-icons-level7::before { +.icon-rating-icons-level20::before { content: "\f1bc"; } -.icon-rating-icons-level70::before { +.icon-rating-icons-level3::before { content: "\f1bd"; } -.icon-rating-icons-level8::before { +.icon-rating-icons-level30::before { content: "\f1be"; } -.icon-rating-icons-level80::before { +.icon-rating-icons-level4::before { content: "\f1bf"; } -.icon-rating-icons-level9::before { +.icon-rating-icons-level40::before { content: "\f1c0"; } -.icon-rating-icons-level90::before { +.icon-rating-icons-level5::before { content: "\f1c1"; } -.icon-rating-icons-negative::before { +.icon-rating-icons-level50::before { content: "\f1c2"; } -.icon-readchats::before { +.icon-rating-icons-level6::before { content: "\f1c3"; } -.icon-recent::before { +.icon-rating-icons-level60::before { content: "\f1c4"; } -.icon-refund::before { +.icon-rating-icons-level7::before { content: "\f1c5"; } -.icon-reload::before { +.icon-rating-icons-level70::before { content: "\f1c6"; } -.icon-remove-quote::before { +.icon-rating-icons-level8::before { content: "\f1c7"; } -.icon-remove::before { +.icon-rating-icons-level80::before { content: "\f1c8"; } -.icon-reopen-topic::before { +.icon-rating-icons-level9::before { content: "\f1c9"; } -.icon-reorder-tabs::before { +.icon-rating-icons-level90::before { content: "\f1ca"; } -.icon-replace::before { +.icon-rating-icons-negative::before { content: "\f1cb"; } -.icon-replies::before { +.icon-readchats::before { content: "\f1cc"; } -.icon-reply-filled::before { +.icon-recent::before { content: "\f1cd"; } -.icon-reply::before { +.icon-refund::before { content: "\f1ce"; } -.icon-revenue-split::before { +.icon-reload::before { content: "\f1cf"; } -.icon-revote::before { +.icon-remove-quote::before { content: "\f1d0"; } -.icon-save-story::before { +.icon-remove::before { content: "\f1d1"; } -.icon-saved-messages::before { +.icon-reopen-topic::before { content: "\f1d2"; } -.icon-schedule::before { +.icon-reorder-tabs::before { content: "\f1d3"; } -.icon-scheduled::before { +.icon-replace::before { content: "\f1d4"; } -.icon-sd-photo::before { +.icon-replies::before { content: "\f1d5"; } -.icon-search::before { +.icon-reply-filled::before { content: "\f1d6"; } -.icon-select::before { +.icon-reply::before { content: "\f1d7"; } -.icon-sell-outline::before { +.icon-revenue-split::before { content: "\f1d8"; } -.icon-sell::before { +.icon-revote::before { content: "\f1d9"; } -.icon-send-outline::before { +.icon-save-story::before { content: "\f1da"; } -.icon-send::before { +.icon-saved-messages::before { content: "\f1db"; } -.icon-settings-filled::before { +.icon-schedule::before { content: "\f1dc"; } -.icon-settings::before { +.icon-scheduled::before { content: "\f1dd"; } -.icon-share-filled::before { +.icon-sd-photo::before { content: "\f1de"; } -.icon-share-screen-outlined::before { +.icon-search::before { content: "\f1df"; } -.icon-share-screen-stop::before { +.icon-select::before { content: "\f1e0"; } -.icon-share-screen::before { +.icon-sell-outline::before { content: "\f1e1"; } -.icon-show-message::before { +.icon-sell::before { content: "\f1e2"; } -.icon-sidebar::before { +.icon-send-outline::before { content: "\f1e3"; } -.icon-skip-next::before { +.icon-send::before { content: "\f1e4"; } -.icon-skip-previous::before { +.icon-settings-filled::before { content: "\f1e5"; } -.icon-smallscreen::before { +.icon-settings::before { content: "\f1e6"; } -.icon-smile::before { +.icon-share-filled::before { content: "\f1e7"; } -.icon-sort-by-date::before { +.icon-share-screen-outlined::before { content: "\f1e8"; } -.icon-sort-by-number::before { +.icon-share-screen-stop::before { content: "\f1e9"; } -.icon-sort-by-price::before { +.icon-share-screen::before { content: "\f1ea"; } -.icon-sort::before { +.icon-show-message::before { content: "\f1eb"; } -.icon-speaker-muted-story::before { +.icon-sidebar::before { content: "\f1ec"; } -.icon-speaker-outline::before { +.icon-skip-next::before { content: "\f1ed"; } -.icon-speaker-story::before { +.icon-skip-previous::before { content: "\f1ee"; } -.icon-speaker::before { +.icon-smallscreen::before { content: "\f1ef"; } -.icon-spoiler-disable::before { +.icon-smile::before { content: "\f1f0"; } -.icon-spoiler::before { +.icon-sort-by-date::before { content: "\f1f1"; } -.icon-sport::before { +.icon-sort-by-number::before { content: "\f1f2"; } -.icon-star::before { +.icon-sort-by-price::before { content: "\f1f3"; } -.icon-stars-lock::before { +.icon-sort::before { content: "\f1f4"; } -.icon-stars-refund::before { +.icon-speaker-muted-story::before { content: "\f1f5"; } -.icon-stats::before { +.icon-speaker-outline::before { content: "\f1f6"; } -.icon-stealth-future::before { +.icon-speaker-story::before { content: "\f1f7"; } -.icon-stealth-past::before { +.icon-speaker::before { content: "\f1f8"; } -.icon-stickers::before { +.icon-spoiler-disable::before { content: "\f1f9"; } -.icon-stop-raising-hand::before { +.icon-spoiler::before { content: "\f1fa"; } -.icon-stop::before { +.icon-sport::before { content: "\f1fb"; } -.icon-story-caption::before { +.icon-star::before { content: "\f1fc"; } -.icon-story-expired::before { +.icon-stars-lock::before { content: "\f1fd"; } -.icon-story-priority::before { +.icon-stars-refund::before { content: "\f1fe"; } -.icon-story-reply::before { +.icon-stats::before { content: "\f1ff"; } -.icon-strikethrough::before { +.icon-stealth-future::before { content: "\f200"; } -.icon-tag-add::before { +.icon-stealth-past::before { content: "\f201"; } -.icon-tag-crossed::before { +.icon-stickers::before { content: "\f202"; } -.icon-tag-filter::before { +.icon-stop-raising-hand::before { content: "\f203"; } -.icon-tag-name::before { +.icon-stop::before { content: "\f204"; } -.icon-tag::before { +.icon-story-caption::before { content: "\f205"; } -.icon-timer::before { +.icon-story-expired::before { content: "\f206"; } -.icon-toncoin::before { +.icon-story-priority::before { content: "\f207"; } -.icon-topic-new::before { +.icon-story-reply::before { content: "\f208"; } -.icon-trade::before { +.icon-strikethrough::before { content: "\f209"; } -.icon-transcribe::before { +.icon-tag-add::before { content: "\f20a"; } -.icon-truck::before { +.icon-tag-crossed::before { content: "\f20b"; } -.icon-unarchive::before { +.icon-tag-filter::before { content: "\f20c"; } -.icon-underlined::before { +.icon-tag-name::before { content: "\f20d"; } -.icon-understood::before { +.icon-tag::before { content: "\f20e"; } -.icon-unique-profile::before { +.icon-timer::before { content: "\f20f"; } -.icon-unlist-outline::before { +.icon-toncoin::before { content: "\f210"; } -.icon-unlist::before { +.icon-tools::before { content: "\f211"; } -.icon-unlock-badge::before { +.icon-topic-new::before { content: "\f212"; } -.icon-unlock::before { +.icon-trade::before { content: "\f213"; } -.icon-unmute::before { +.icon-transcribe::before { content: "\f214"; } -.icon-unpin::before { +.icon-truck::before { content: "\f215"; } -.icon-unread::before { +.icon-unarchive::before { content: "\f216"; } -.icon-up::before { +.icon-underlined::before { content: "\f217"; } -.icon-user-filled::before { +.icon-understood::before { content: "\f218"; } -.icon-user-online::before { +.icon-unique-profile::before { content: "\f219"; } -.icon-user-stars::before { +.icon-unlist-outline::before { content: "\f21a"; } -.icon-user::before { +.icon-unlist::before { content: "\f21b"; } -.icon-video-outlined::before { +.icon-unlock-badge::before { content: "\f21c"; } -.icon-video-stop::before { +.icon-unlock::before { content: "\f21d"; } -.icon-video::before { +.icon-unmute::before { content: "\f21e"; } -.icon-view-once::before { +.icon-unpin::before { content: "\f21f"; } -.icon-voice-chat::before { +.icon-unread::before { content: "\f220"; } -.icon-volume-1::before { +.icon-up::before { content: "\f221"; } -.icon-volume-2::before { +.icon-user-filled::before { content: "\f222"; } -.icon-volume-3::before { +.icon-user-online::before { content: "\f223"; } -.icon-warning::before { +.icon-user-stars::before { content: "\f224"; } -.icon-web::before { +.icon-user::before { content: "\f225"; } -.icon-webapp::before { +.icon-video-outlined::before { content: "\f226"; } -.icon-word-wrap::before { +.icon-video-stop::before { content: "\f227"; } -.icon-zoom-in::before { +.icon-video::before { content: "\f228"; } -.icon-zoom-out::before { +.icon-view-once::before { content: "\f229"; } +.icon-voice-chat::before { + content: "\f22a"; +} +.icon-volume-1::before { + content: "\f22b"; +} +.icon-volume-2::before { + content: "\f22c"; +} +.icon-volume-3::before { + content: "\f22d"; +} +.icon-warning::before { + content: "\f22e"; +} +.icon-web::before { + content: "\f22f"; +} +.icon-webapp::before { + content: "\f230"; +} +.icon-word-wrap::before { + content: "\f231"; +} +.icon-zoom-in::before { + content: "\f232"; +} +.icon-zoom-out::before { + content: "\f233"; +} diff --git a/src/styles/icons.scss b/src/styles/icons.scss index 8c6340e56..f779cff39 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -107,210 +107,220 @@ $icons-map: ( "file-badge": "\f159", "flag": "\f15a", "folder-badge": "\f15b", - "folder": "\f15c", - "fontsize": "\f15d", - "forums": "\f15e", - "forward": "\f15f", - "fragment": "\f160", - "frozen-time": "\f161", - "fullscreen": "\f162", - "gifs": "\f163", - "gift-transfer-inline": "\f164", - "gift": "\f165", - "group-filled": "\f166", - "group": "\f167", - "grouped-disable": "\f168", - "grouped": "\f169", - "hand-stop": "\f16a", - "hashtag": "\f16b", - "hd-photo": "\f16c", - "heart-outline": "\f16d", - "heart": "\f16e", - "help": "\f16f", - "info-filled": "\f170", - "info": "\f171", - "install": "\f172", - "italic": "\f173", - "key": "\f174", - "keyboard": "\f175", - "lamp": "\f176", - "language": "\f177", - "large-pause": "\f178", - "large-play": "\f179", - "link-badge": "\f17a", - "link-broken": "\f17b", - "link": "\f17c", - "location": "\f17d", - "lock-badge": "\f17e", - "lock": "\f17f", - "logout": "\f180", - "loop": "\f181", - "mention": "\f182", - "message-failed": "\f183", - "message-pending": "\f184", - "message-read": "\f185", - "message-succeeded": "\f186", - "message": "\f187", - "microphone-alt": "\f188", - "microphone": "\f189", - "monospace": "\f18a", - "more-circle": "\f18b", - "more": "\f18c", - "move-caption-down": "\f18d", - "move-caption-up": "\f18e", - "mute": "\f18f", - "muted": "\f190", - "my-notes": "\f191", - "new-chat-filled": "\f192", - "next": "\f193", - "nochannel": "\f194", - "noise-suppression": "\f195", - "non-contacts": "\f196", - "note": "\f197", - "one-filled": "\f198", - "open-in-new-tab": "\f199", - "password-off": "\f19a", - "pause": "\f19b", - "permissions": "\f19c", - "phone-discard-outline": "\f19d", - "phone-discard": "\f19e", - "phone": "\f19f", - "photo": "\f1a0", - "pin-badge": "\f1a1", - "pin-list": "\f1a2", - "pin": "\f1a3", - "pinned-chat": "\f1a4", - "pinned-message": "\f1a5", - "pip": "\f1a6", - "play-story": "\f1a7", - "play": "\f1a8", - "poll": "\f1a9", - "previous": "\f1aa", - "privacy-policy": "\f1ab", - "proof-of-ownership": "\f1ac", - "quote-text": "\f1ad", - "quote": "\f1ae", - "radial-badge": "\f1af", - "rating-icons-level1": "\f1b0", - "rating-icons-level10": "\f1b1", - "rating-icons-level2": "\f1b2", - "rating-icons-level20": "\f1b3", - "rating-icons-level3": "\f1b4", - "rating-icons-level30": "\f1b5", - "rating-icons-level4": "\f1b6", - "rating-icons-level40": "\f1b7", - "rating-icons-level5": "\f1b8", - "rating-icons-level50": "\f1b9", - "rating-icons-level6": "\f1ba", - "rating-icons-level60": "\f1bb", - "rating-icons-level7": "\f1bc", - "rating-icons-level70": "\f1bd", - "rating-icons-level8": "\f1be", - "rating-icons-level80": "\f1bf", - "rating-icons-level9": "\f1c0", - "rating-icons-level90": "\f1c1", - "rating-icons-negative": "\f1c2", - "readchats": "\f1c3", - "recent": "\f1c4", - "refund": "\f1c5", - "reload": "\f1c6", - "remove-quote": "\f1c7", - "remove": "\f1c8", - "reopen-topic": "\f1c9", - "reorder-tabs": "\f1ca", - "replace": "\f1cb", - "replies": "\f1cc", - "reply-filled": "\f1cd", - "reply": "\f1ce", - "revenue-split": "\f1cf", - "revote": "\f1d0", - "save-story": "\f1d1", - "saved-messages": "\f1d2", - "schedule": "\f1d3", - "scheduled": "\f1d4", - "sd-photo": "\f1d5", - "search": "\f1d6", - "select": "\f1d7", - "sell-outline": "\f1d8", - "sell": "\f1d9", - "send-outline": "\f1da", - "send": "\f1db", - "settings-filled": "\f1dc", - "settings": "\f1dd", - "share-filled": "\f1de", - "share-screen-outlined": "\f1df", - "share-screen-stop": "\f1e0", - "share-screen": "\f1e1", - "show-message": "\f1e2", - "sidebar": "\f1e3", - "skip-next": "\f1e4", - "skip-previous": "\f1e5", - "smallscreen": "\f1e6", - "smile": "\f1e7", - "sort-by-date": "\f1e8", - "sort-by-number": "\f1e9", - "sort-by-price": "\f1ea", - "sort": "\f1eb", - "speaker-muted-story": "\f1ec", - "speaker-outline": "\f1ed", - "speaker-story": "\f1ee", - "speaker": "\f1ef", - "spoiler-disable": "\f1f0", - "spoiler": "\f1f1", - "sport": "\f1f2", - "star": "\f1f3", - "stars-lock": "\f1f4", - "stars-refund": "\f1f5", - "stats": "\f1f6", - "stealth-future": "\f1f7", - "stealth-past": "\f1f8", - "stickers": "\f1f9", - "stop-raising-hand": "\f1fa", - "stop": "\f1fb", - "story-caption": "\f1fc", - "story-expired": "\f1fd", - "story-priority": "\f1fe", - "story-reply": "\f1ff", - "strikethrough": "\f200", - "tag-add": "\f201", - "tag-crossed": "\f202", - "tag-filter": "\f203", - "tag-name": "\f204", - "tag": "\f205", - "timer": "\f206", - "toncoin": "\f207", - "topic-new": "\f208", - "trade": "\f209", - "transcribe": "\f20a", - "truck": "\f20b", - "unarchive": "\f20c", - "underlined": "\f20d", - "understood": "\f20e", - "unique-profile": "\f20f", - "unlist-outline": "\f210", - "unlist": "\f211", - "unlock-badge": "\f212", - "unlock": "\f213", - "unmute": "\f214", - "unpin": "\f215", - "unread": "\f216", - "up": "\f217", - "user-filled": "\f218", - "user-online": "\f219", - "user-stars": "\f21a", - "user": "\f21b", - "video-outlined": "\f21c", - "video-stop": "\f21d", - "video": "\f21e", - "view-once": "\f21f", - "voice-chat": "\f220", - "volume-1": "\f221", - "volume-2": "\f222", - "volume-3": "\f223", - "warning": "\f224", - "web": "\f225", - "webapp": "\f226", - "word-wrap": "\f227", - "zoom-in": "\f228", - "zoom-out": "\f229", + "folder-tabs-bot": "\f15c", + "folder-tabs-channel": "\f15d", + "folder-tabs-chat": "\f15e", + "folder-tabs-chats": "\f15f", + "folder-tabs-folder": "\f160", + "folder-tabs-group": "\f161", + "folder-tabs-star": "\f162", + "folder-tabs-user": "\f163", + "folder": "\f164", + "fontsize": "\f165", + "forums": "\f166", + "forward": "\f167", + "fragment": "\f168", + "frozen-time": "\f169", + "fullscreen": "\f16a", + "gifs": "\f16b", + "gift-transfer-inline": "\f16c", + "gift": "\f16d", + "group-filled": "\f16e", + "group": "\f16f", + "grouped-disable": "\f170", + "grouped": "\f171", + "hand-stop": "\f172", + "hashtag": "\f173", + "hd-photo": "\f174", + "heart-outline": "\f175", + "heart": "\f176", + "help": "\f177", + "info-filled": "\f178", + "info": "\f179", + "install": "\f17a", + "italic": "\f17b", + "key": "\f17c", + "keyboard": "\f17d", + "lamp": "\f17e", + "language": "\f17f", + "large-pause": "\f180", + "large-play": "\f181", + "link-badge": "\f182", + "link-broken": "\f183", + "link": "\f184", + "location": "\f185", + "lock-badge": "\f186", + "lock": "\f187", + "logout": "\f188", + "loop": "\f189", + "mention": "\f18a", + "menu": "\f18b", + "message-failed": "\f18c", + "message-pending": "\f18d", + "message-read": "\f18e", + "message-succeeded": "\f18f", + "message": "\f190", + "microphone-alt": "\f191", + "microphone": "\f192", + "monospace": "\f193", + "more-circle": "\f194", + "more": "\f195", + "move-caption-down": "\f196", + "move-caption-up": "\f197", + "mute": "\f198", + "muted": "\f199", + "my-notes": "\f19a", + "new-chat-filled": "\f19b", + "next": "\f19c", + "nochannel": "\f19d", + "noise-suppression": "\f19e", + "non-contacts": "\f19f", + "note": "\f1a0", + "one-filled": "\f1a1", + "open-in-new-tab": "\f1a2", + "password-off": "\f1a3", + "pause": "\f1a4", + "permissions": "\f1a5", + "phone-discard-outline": "\f1a6", + "phone-discard": "\f1a7", + "phone": "\f1a8", + "photo": "\f1a9", + "pin-badge": "\f1aa", + "pin-list": "\f1ab", + "pin": "\f1ac", + "pinned-chat": "\f1ad", + "pinned-message": "\f1ae", + "pip": "\f1af", + "play-story": "\f1b0", + "play": "\f1b1", + "poll": "\f1b2", + "previous": "\f1b3", + "privacy-policy": "\f1b4", + "proof-of-ownership": "\f1b5", + "quote-text": "\f1b6", + "quote": "\f1b7", + "radial-badge": "\f1b8", + "rating-icons-level1": "\f1b9", + "rating-icons-level10": "\f1ba", + "rating-icons-level2": "\f1bb", + "rating-icons-level20": "\f1bc", + "rating-icons-level3": "\f1bd", + "rating-icons-level30": "\f1be", + "rating-icons-level4": "\f1bf", + "rating-icons-level40": "\f1c0", + "rating-icons-level5": "\f1c1", + "rating-icons-level50": "\f1c2", + "rating-icons-level6": "\f1c3", + "rating-icons-level60": "\f1c4", + "rating-icons-level7": "\f1c5", + "rating-icons-level70": "\f1c6", + "rating-icons-level8": "\f1c7", + "rating-icons-level80": "\f1c8", + "rating-icons-level9": "\f1c9", + "rating-icons-level90": "\f1ca", + "rating-icons-negative": "\f1cb", + "readchats": "\f1cc", + "recent": "\f1cd", + "refund": "\f1ce", + "reload": "\f1cf", + "remove-quote": "\f1d0", + "remove": "\f1d1", + "reopen-topic": "\f1d2", + "reorder-tabs": "\f1d3", + "replace": "\f1d4", + "replies": "\f1d5", + "reply-filled": "\f1d6", + "reply": "\f1d7", + "revenue-split": "\f1d8", + "revote": "\f1d9", + "save-story": "\f1da", + "saved-messages": "\f1db", + "schedule": "\f1dc", + "scheduled": "\f1dd", + "sd-photo": "\f1de", + "search": "\f1df", + "select": "\f1e0", + "sell-outline": "\f1e1", + "sell": "\f1e2", + "send-outline": "\f1e3", + "send": "\f1e4", + "settings-filled": "\f1e5", + "settings": "\f1e6", + "share-filled": "\f1e7", + "share-screen-outlined": "\f1e8", + "share-screen-stop": "\f1e9", + "share-screen": "\f1ea", + "show-message": "\f1eb", + "sidebar": "\f1ec", + "skip-next": "\f1ed", + "skip-previous": "\f1ee", + "smallscreen": "\f1ef", + "smile": "\f1f0", + "sort-by-date": "\f1f1", + "sort-by-number": "\f1f2", + "sort-by-price": "\f1f3", + "sort": "\f1f4", + "speaker-muted-story": "\f1f5", + "speaker-outline": "\f1f6", + "speaker-story": "\f1f7", + "speaker": "\f1f8", + "spoiler-disable": "\f1f9", + "spoiler": "\f1fa", + "sport": "\f1fb", + "star": "\f1fc", + "stars-lock": "\f1fd", + "stars-refund": "\f1fe", + "stats": "\f1ff", + "stealth-future": "\f200", + "stealth-past": "\f201", + "stickers": "\f202", + "stop-raising-hand": "\f203", + "stop": "\f204", + "story-caption": "\f205", + "story-expired": "\f206", + "story-priority": "\f207", + "story-reply": "\f208", + "strikethrough": "\f209", + "tag-add": "\f20a", + "tag-crossed": "\f20b", + "tag-filter": "\f20c", + "tag-name": "\f20d", + "tag": "\f20e", + "timer": "\f20f", + "toncoin": "\f210", + "tools": "\f211", + "topic-new": "\f212", + "trade": "\f213", + "transcribe": "\f214", + "truck": "\f215", + "unarchive": "\f216", + "underlined": "\f217", + "understood": "\f218", + "unique-profile": "\f219", + "unlist-outline": "\f21a", + "unlist": "\f21b", + "unlock-badge": "\f21c", + "unlock": "\f21d", + "unmute": "\f21e", + "unpin": "\f21f", + "unread": "\f220", + "up": "\f221", + "user-filled": "\f222", + "user-online": "\f223", + "user-stars": "\f224", + "user": "\f225", + "video-outlined": "\f226", + "video-stop": "\f227", + "video": "\f228", + "view-once": "\f229", + "voice-chat": "\f22a", + "volume-1": "\f22b", + "volume-2": "\f22c", + "volume-3": "\f22d", + "warning": "\f22e", + "web": "\f22f", + "webapp": "\f230", + "word-wrap": "\f231", + "zoom-in": "\f232", + "zoom-out": "\f233", ); diff --git a/src/styles/icons.woff b/src/styles/icons.woff index 2e9a73dcd..f0e528d00 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index 941663bf3..c01b062f8 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/styles/themes.json b/src/styles/themes.json index 08ba7f56d..7cd1a0cd1 100644 --- a/src/styles/themes.json +++ b/src/styles/themes.json @@ -9,8 +9,9 @@ "--color-web-app-browser": ["#FFFFFFBB", "#0303038F"], "--color-background-compact-menu-reactions": ["#FFFFFFEB", "#212121DD"], "--color-background-compact-menu-hover": ["#00000011", "#00000066"], - "--color-background-secondary": ["#f4f4f5", "#0F0F0F"], + "--color-background-secondary": ["#F4F4F5", "#0F0F0F"], "--color-background-secondary-accent": ["#E4E4E5", "#191919"], + "--color-background-sidebar": ["#E4E4E5", "#0F0F0F"], "--color-background-own": ["#EEFFDE", "#766AC8"], "--color-background-own-apple": ["#DCF8C5", "#766AC8"], "--color-background-selected": ["#F4F4F5", "#2C2C2C"], @@ -18,8 +19,8 @@ "--color-chat-hover": ["#F4F4F5", "#2C2C2C"], "--color-chat-active": ["#3390EC", "#766AC8"], "--color-chat-active-greyed": ["#60a7f0", "#9288d3"], - "--color-item-hover": ["#f4f4f5", "#2c2c2c"], - "--color-item-active": ["#ededed", "#292929"], + "--color-item-hover": ["#F4F4F5", "#2C2C2C"], + "--color-item-active": ["#EDEDED", "#292929"], "--color-text": ["#000000", "#FFFFFF"], "--color-text-secondary": ["#707579", "#AAAAAA"], "--color-icon-secondary": ["#707579", "#AAAAAA"], diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index d31adf0c3..f16e15bcd 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -90,6 +90,14 @@ export type FontIconName = | 'file-badge' | 'flag' | 'folder-badge' + | 'folder-tabs-bot' + | 'folder-tabs-channel' + | 'folder-tabs-chat' + | 'folder-tabs-chats' + | 'folder-tabs-folder' + | 'folder-tabs-group' + | 'folder-tabs-star' + | 'folder-tabs-user' | 'folder' | 'fontsize' | 'forums' @@ -129,6 +137,7 @@ export type FontIconName = | 'logout' | 'loop' | 'mention' + | 'menu' | 'message-failed' | 'message-pending' | 'message-read' @@ -262,6 +271,7 @@ export type FontIconName = | 'tag' | 'timer' | 'toncoin' + | 'tools' | 'topic-new' | 'trade' | 'transcribe' diff --git a/src/types/index.ts b/src/types/index.ts index 4a2b5cb0d..a27fe3be6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -100,6 +100,7 @@ export type ThreadId = string | number; export type ThemeKey = 'light' | 'dark'; export type AnimationLevel = 0 | 1 | 2; +export type TabsPosition = 'top' | 'left'; export type PerformanceTypeKey = ( 'pageTransitions' | 'messageSendingAnimations' | 'mediaViewerAnimations' | 'messageComposerAnimations' | 'contextMenuAnimations' | 'contextMenuBlur' | 'rightColumnAnimations' diff --git a/src/types/language.d.ts b/src/types/language.d.ts index b26800428..ba8dc4d2b 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -288,7 +288,8 @@ export interface LangPair { 'ChatEmptyChat': undefined; 'ChatListEmptyChatListEditFilter': undefined; 'UpdateTelegram': undefined; - 'AccDescrOpenMenu2': undefined; + 'AriaLabelOpenMenu': undefined; + 'AriaLabelBackChatList': undefined; 'SettingsTipsUsername': undefined; 'SearchFriends': undefined; 'Search': undefined; @@ -326,6 +327,9 @@ export interface LangPair { 'FilterColorHint': undefined; 'ShowFolderTags': undefined; 'ShowFolderTagsHint': undefined; + 'TabsPosition': undefined; + 'TabsPositionLeft': undefined; + 'TabsPositionTop': undefined; 'FilterIncludeInfo': undefined; 'FilterNameHint': undefined; 'FilterInclude': undefined; diff --git a/src/util/fallbackLangPack.ts b/src/util/fallbackLangPack.ts index 547e2ad50..06a781008 100644 --- a/src/util/fallbackLangPack.ts +++ b/src/util/fallbackLangPack.ts @@ -179,6 +179,9 @@ export default { 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.', + TabsPosition: 'Tabs View', + TabsPositionLeft: 'Tabs on the left', + TabsPositionTop: 'Tabs at the top', AccDescrChannel: 'Channel', AccDescrGroup: 'Group', Bot: 'bot', diff --git a/src/util/folderIconMap.ts b/src/util/folderIconMap.ts new file mode 100644 index 000000000..279585ac4 --- /dev/null +++ b/src/util/folderIconMap.ts @@ -0,0 +1,20 @@ +import type { IconName } from '../types/icons'; + +export const folderIconMap: Record = { + '🗂': 'folder-tabs-folder', + '⭐': 'folder-tabs-star', + '🤖': 'folder-tabs-bot', + '👥': 'folder-tabs-group', + '👤': 'folder-tabs-user', + '✅': 'folder-tabs-chat', + '📢': 'folder-tabs-channel', + '💬': 'folder-tabs-chats', +}; + +export const emojiToFolderIcon = (emoji: string): IconName | undefined => { + return folderIconMap[emoji]; +}; + +export const folderIconToEmoji = (icon: IconName): string | undefined => { + return Object.keys(folderIconMap).find((key) => folderIconMap[key] === icon); +};