+ {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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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 && (
+
+ )}
+
+ );
+};
+
+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);
+};