Folders: Support vertical sidebar display mode (#6280)

By @kotevcode
This commit is contained in:
Alexander Zinchuk 2025-11-17 12:18:37 +04:00
parent aff179d666
commit 682ec32dd2
67 changed files with 2004 additions and 751 deletions

View File

@ -0,0 +1,7 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.116 16.8597C21.8886 16.8597 22.5149 16.0768 22.5149 15.1111C22.5149 14.1453 21.8886 13.3625 21.116 13.3625C20.3434 13.3625 19.7171 14.1453 19.7171 15.1111C19.7171 16.0768 20.3434 16.8597 21.116 16.8597Z" fill="black"/>
<path d="M14.8791 16.8597C15.6517 16.8597 16.278 16.0768 16.278 15.1111C16.278 14.1454 15.6517 13.3625 14.8791 13.3625C14.1065 13.3625 13.4802 14.1454 13.4802 15.1111C13.4802 16.0768 14.1065 16.8597 14.8791 16.8597Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.0395 21.2574V15.3174C27.0395 10.3252 22.9925 6.27826 18.0004 6.27826C13.0082 6.27826 8.96124 10.3252 8.96124 15.3174V21.2574C8.96124 24.1502 8.96124 25.5966 9.52422 26.7015C10.0194 27.6734 10.8096 28.4636 11.7815 28.9588C12.8864 29.5217 14.3328 29.5217 17.2256 29.5217H18.7752C21.6679 29.5217 23.1143 29.5217 24.2192 28.9588C25.1911 28.4636 25.9813 27.6734 26.4765 26.7015C27.0395 25.5966 27.0395 24.1502 27.0395 21.2574ZM11.5434 15.3174C11.5434 11.7516 14.4341 8.86088 17.9999 8.86088C21.5658 8.86088 24.4565 11.7516 24.4565 15.3174V16.7772C24.4565 18.2549 23.608 19.6077 22.1748 19.9675C21.0836 20.2414 19.6533 20.4826 17.9999 20.4826C16.3466 20.4826 14.9163 20.2414 13.8251 19.9675C12.3919 19.6077 11.5434 18.2549 11.5434 16.7772V15.3174Z" fill="black"/>
<path d="M30.2676 16.4C31.096 16.4 31.7676 17.0716 31.7676 17.9V24.3565C31.7676 25.1849 31.096 25.8565 30.2676 25.8565C29.4392 25.8565 28.7676 25.1849 28.7676 24.3565V17.9C28.7676 17.0716 29.4392 16.4 30.2676 16.4Z" fill="black"/>
<path d="M7.23291 17.9C7.23291 17.0716 6.56134 16.4 5.73291 16.4C4.90448 16.4 4.23291 17.0716 4.23291 17.9V24.3565C4.23291 25.1849 4.90448 25.8565 5.73291 25.8565C6.56134 25.8565 7.23291 25.1849 7.23291 24.3565V17.9Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,4 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.1765 22.708H15.5855C16.0157 22.708 16.2308 22.708 16.4395 22.7306C17.0143 22.7929 17.568 22.9829 18.0601 23.2866C18.2387 23.3969 18.4085 23.5289 18.7481 23.7931L22.3164 26.5684C24.4611 28.2365 25.5335 29.0706 26.4338 29.0629C27.217 29.0562 27.9551 28.6953 28.4412 28.0811C29 27.3751 29 26.0166 29 23.2996V11.7634C29 9.04635 29 7.68782 28.4412 6.98185C27.9551 6.3677 27.217 6.00672 26.4338 6.00005C25.5335 5.99239 24.4611 6.82644 22.3164 8.49455L18.7481 11.2699C18.4085 11.534 18.2387 11.6661 18.0601 11.7763C17.568 12.0801 17.0143 12.27 16.4395 12.3324C16.2308 12.355 16.0157 12.355 15.5855 12.355H12.1765C9.31759 12.355 7 14.6726 7 17.5315C7 20.3904 9.31758 22.708 12.1765 22.708Z" fill="black"/>
<path d="M12.2982 25.1742C12.1764 25.4348 12.1764 25.7709 12.1764 26.4431V27.8844C12.1764 29.3139 13.3352 30.4727 14.7646 30.4727C16.1941 30.4727 17.3529 29.3139 17.3529 27.8844V26.4431C17.3529 25.7709 17.3529 25.4348 17.231 25.1742C17.1026 24.8995 16.8817 24.6786 16.6069 24.5501C16.3464 24.4283 16.0103 24.4283 15.3381 24.4283H14.1912C13.519 24.4283 13.1829 24.4283 12.9224 24.5501C12.6476 24.6786 12.4267 24.8995 12.2982 25.1742Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30 17.2847C30 11.2598 24.6274 6.37561 18 6.37561C11.3726 6.37561 6 11.2598 6 17.2847C6 20.7209 7.60509 23.5375 10.3363 25.5371C10.6856 25.7929 11.0073 27.2137 10.2288 28.4072C9.45024 29.6006 8.47959 30.146 8.96637 30.3502C9.26647 30.4761 11.0397 30.5384 12.3196 29.8206C14.1496 28.7943 14.6613 27.7725 15.0551 27.8629C15.9973 28.079 16.9839 28.1938 18 28.1938C24.6274 28.1938 30 23.3096 30 17.2847Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 528 B

View File

@ -0,0 +1,4 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.3902 9.03227C20.3914 7.162 17.595 6 14.5 6C8.42487 6 3.5 10.4772 3.5 16C3.5 19.1499 4.97133 21.7318 7.47498 23.5647C7.79515 23.7991 8.09005 25.1016 7.37638 26.1956C7.08455 26.643 6.76327 26.991 6.51594 27.2589C6.15843 27.6462 5.95542 27.8661 6.21917 27.9767C6.49426 28.0921 8.11974 28.1492 9.29294 27.4912C10.2112 26.9763 10.7674 26.4625 11.1419 26.1166C11.4516 25.8305 11.6371 25.6592 11.8005 25.6967C12.3863 25.831 12.9908 25.9227 13.6098 25.9677C13.6182 25.9683 13.6267 25.9689 13.6351 25.9695C13.9205 25.9897 14.2089 26 14.5 26C20.5751 26 25.5 21.5228 25.5 16C25.5 13.2908 24.3149 10.8332 22.3902 9.03227Z" fill="black"/>
<path d="M32.5 19C32.5 15.0932 30.0356 11.7097 26.4429 10.0641C26.0059 9.86387 25.6121 10.3758 25.8411 10.7985C26.6828 12.3527 27.16 14.1145 27.16 16C27.16 21.6043 22.944 26.1149 17.4721 27.3341C16.9915 27.4412 16.8851 28.0914 17.3467 28.2627C18.6282 28.738 20.0305 29 21.5 29C22.4314 29 23.3358 28.8948 24.1995 28.6967C24.3629 28.6592 24.5484 28.8305 24.8581 29.1166C25.2326 29.4625 25.7888 29.9763 26.7071 30.4912C27.8803 31.1492 29.5057 31.0921 29.7808 30.9767C30.0446 30.8661 29.8416 30.6462 29.4841 30.2589C29.2367 29.991 28.9155 29.643 28.6236 29.1956C27.91 28.1016 28.2048 26.7991 28.525 26.5647C31.0287 24.7318 32.5 22.1499 32.5 19Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,4 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.21265 9.53789C6 10.1456 6 10.9016 6 12.4135C6 12.6655 6 12.7915 6.03544 12.8928C6.09892 13.0742 6.24156 13.2169 6.42298 13.2803C6.52427 13.3158 6.65026 13.3158 6.90226 13.3158H29.2421C29.3595 13.3158 29.4181 13.3158 29.4672 13.308C29.7375 13.2652 29.9494 13.0533 29.9922 12.783C30 12.7339 30 12.6752 30 12.5579C30 12.0885 30 11.8538 29.9689 11.6574C29.7977 10.5764 28.9499 9.72863 27.8689 9.55742C27.6725 9.52632 27.4378 9.52632 26.9684 9.52632H19.1548C18.9418 9.52632 18.8353 9.52632 18.7327 9.52075C17.8706 9.47396 17.0503 9.13419 16.4077 8.55768C16.3312 8.48907 16.2559 8.41377 16.1053 8.26319C15.9547 8.1126 15.8794 8.03725 15.8029 7.96863C15.1602 7.39213 14.3399 7.05236 13.4778 7.00557C13.3752 7 13.2687 7 13.0557 7H11.4135C9.90159 7 9.14562 7 8.53789 7.21265C7.44937 7.59354 6.59354 8.44937 6.21265 9.53789Z" fill="black"/>
<path d="M6.20651 16.8766C6 17.282 6 17.8125 6 18.8737V20.3895C6 23.2192 6 24.6341 6.5507 25.7149C7.03512 26.6656 7.80807 27.4386 8.75878 27.923C9.8396 28.4737 11.2545 28.4737 14.0842 28.4737H21.9158C24.7455 28.4737 26.1604 28.4737 27.2412 27.923C28.1919 27.4386 28.9649 26.6656 29.4493 25.7149C30 24.6341 30 23.2192 30 20.3895V18.8737C30 17.8125 30 17.282 29.7935 16.8766C29.6118 16.5201 29.322 16.2303 28.9655 16.0486C28.5602 15.8421 28.0296 15.8421 26.9684 15.8421H9.03158C7.97043 15.8421 7.43985 15.8421 7.03454 16.0486C6.67803 16.2303 6.38817 16.5201 6.20651 16.8766Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,6 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.3333 17.3333C16.9107 17.3333 19 15.244 19 12.6667C19 10.0893 16.9107 8 14.3333 8C11.756 8 9.66667 10.0893 9.66667 12.6667C9.66667 15.244 11.756 17.3333 14.3333 17.3333Z" fill="black"/>
<path d="M5 25.3654C5 26.8204 6.17955 28 7.63461 28H21.0376C22.4901 27.997 23.6667 26.8186 23.6667 25.3654C23.6667 24.5819 23.4961 23.799 23.0277 23.1928C22.9608 23.1062 22.8879 23.0232 22.8084 22.9443C22.4728 22.611 22.0286 22.2323 21.4585 21.8601C21.4277 21.8399 21.3965 21.8198 21.365 21.7998C19.9048 20.8708 17.6512 20 14.3333 20C9.6174 20 7.05162 21.7592 5.85822 22.9443C5.22282 23.5753 5 24.4699 5 25.3654Z" fill="black"/>
<path d="M25.1023 28H30.823C32.0253 28 33 27.0253 33 25.823C33 25.0652 32.806 24.3089 32.2604 23.7831C31.2358 22.7956 29.037 21.3333 25 21.3333C24.7745 21.3333 24.5548 21.3379 24.3406 21.3467L24.368 21.3738C25.5844 22.5817 25.88 24.1575 25.88 25.3654C25.88 26.3368 25.5943 27.2415 25.1023 28Z" fill="black"/>
<path d="M25 18.6667C27.2091 18.6667 29 16.8758 29 14.6667C29 12.4575 27.2091 10.6667 25 10.6667C22.7909 10.6667 21 12.4575 21 14.6667C21 16.8758 22.7909 18.6667 25 18.6667Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.8151 27.4314C17.4066 27.0754 18.1464 27.0754 18.7379 27.4314L23.2643 30.1558C24.6767 31.0059 26.4164 29.739 26.0409 28.1338L24.8434 23.0146C24.6854 22.3394 24.9152 21.6324 25.4399 21.1792L29.4253 17.7361C30.6744 16.6571 30.0084 14.6069 28.3637 14.4678L23.1107 14.0233C22.4222 13.9651 21.8224 13.5308 21.5521 12.8949L19.4922 8.04816C18.849 6.53473 16.7039 6.53473 16.0607 8.04816L14.0008 12.8949C13.7306 13.5308 13.1307 13.9651 12.4422 14.0233L7.18921 14.4678C5.54451 14.6069 4.8786 16.6571 6.12762 17.7361L10.1131 21.1792C10.6377 21.6324 10.8675 22.3394 10.7096 23.0146L9.51201 28.1338C9.1365 29.739 10.8762 31.0059 12.2887 30.1558L16.8151 27.4314Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 821 B

View File

@ -0,0 +1,4 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 16.8125C22.0376 16.8125 24.5 14.3501 24.5 11.3125C24.5 8.27493 22.0376 5.8125 19 5.8125C15.9624 5.8125 13.5 8.27493 13.5 11.3125C13.5 14.3501 15.9624 16.8125 19 16.8125Z" fill="black"/>
<path d="M8 25.5942C8 27.5787 9.60875 29.1875 11.5933 29.1875H26.4067C28.3912 29.1875 30 27.5787 30 25.5942C30 24.7942 29.8042 23.999 29.2698 23.4036C28.008 21.9978 24.9618 19.5625 19 19.5625C13.0382 19.5625 9.992 21.9978 8.7302 23.4036C8.19579 23.999 8 24.7942 8 25.5942Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
<path d="M3 7C3 6.44772 3.44772 6 4 6H20C20.5523 6 21 6.44772 21 7C21 7.55228 20.5523 8 20 8H4C3.44772 8 3 7.55228 3 7ZM3 12C3 11.4477 3.44772 11 4 11H20C20.5523 11 21 11.4477 21 12C21 12.5523 20.5523 13 20 13H4C3.44772 13 3 12.5523 3 12ZM4 16C3.44772 16 3 16.4477 3 17C3 17.5523 3.44772 18 4 18H20C20.5523 18 21 17.5523 21 17C21 16.4477 20.5523 16 20 16H4Z" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M16,13 C16.5128358,13 16.9355072,13.3860402 16.9932723,13.8833789 L17,14 L17,16 L20,16 C20.5522847,16 21,16.4477153 21,17 C21,17.5128358 20.6139598,17.9355072 20.1166211,17.9932723 L20,18 L17,18 L17,20 C17,20.5522847 16.5522847,21 16,21 C15.4871642,21 15.0644928,20.6139598 15.0067277,20.1166211 L15,20 L15,14 C15,13.4477153 15.4477153,13 16,13 Z M12,16 C12.5522847,16 13,16.4477153 13,17 C13,17.5128358 12.6139598,17.9355072 12.1166211,17.9932723 L12,18 L4,18 C3.44771525,18 3,17.5522847 3,17 C3,16.4871642 3.38604019,16.0644928 3.88337887,16.0067277 L4,16 L12,16 Z M8,3 C8.51283584,3 8.93550716,3.38604019 8.99327227,3.88337887 L9,4 L9,10 C9,10.5522847 8.55228475,11 8,11 C7.48716416,11 7.06449284,10.6139598 7.00672773,10.1166211 L7,10 L7,8 L4,8 C3.44771525,8 3,7.55228475 3,7 C3,6.48716416 3.38604019,6.06449284 3.88337887,6.00672773 L4,6 L7,6 L7,4 C7,3.44771525 7.44771525,3 8,3 Z M20,6 C20.5522847,6 21,6.44771525 21,7 C21,7.51283584 20.6139598,7.93550716 20.1166211,7.99327227 L20,8 L12,8 C11.4477153,8 11,7.55228475 11,7 C11,6.48716416 11.3860402,6.06449284 11.8833789,6.00672773 L12,6 L20,6 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -330,7 +330,8 @@
"ChatEmptyChat" = "No messages here yet";
"ChatListEmptyChatListEditFilter" = "Edit Folder";
"UpdateTelegram" = "Update Telegram";
"AccDescrOpenMenu2" = "Open menu";
"AriaLabelOpenMenu" = "Open menu";
"AriaLabelBackChatList" = "Return to chat list";
"SettingsTipsUsername" = "TelegramTips";
"SearchFriends" = "Search contacts";
"Search" = "Search";
@ -370,6 +371,9 @@
"FilterColorHint" = "This color will be used for the folder's tag in the chat list";
"ShowFolderTags" = "Show Folder Tags";
"ShowFolderTagsHint" = "Display folder names for each chat in the chat list.";
"TabsPosition" = "Tabs View";
"TabsPositionLeft" = "Tabs on the left";
"TabsPositionTop" = "Tabs at the top";
"FilterIncludeInfo" = "Choose chats or types of chats that will appear in this folder.";
"FilterNameHint" = "Folder name";
"FilterInclude" = "Included Chats";

View File

@ -0,0 +1,6 @@
.emoji {
display: grid;
place-content: center;
width: 2rem;
height: 2rem;
}

View File

@ -0,0 +1,41 @@
import { memo } from '../../lib/teact/teact';
import { emojiToFolderIcon } from '../../util/folderIconMap';
import { REM } from './helpers/mediaDimensions';
import renderText from './helpers/renderText';
import CustomEmoji from './CustomEmoji';
import Icon from './icons/Icon';
import styles from './FolderIcon.module.scss';
const ICON_SIZE = 2.25 * REM;
const FolderIcon = (
{
emoji,
customEmojiId,
shouldAnimate,
}: {
emoji?: string;
customEmojiId?: string;
shouldAnimate?: boolean;
},
) => {
if (customEmojiId) {
return <CustomEmoji documentId={customEmojiId} size={ICON_SIZE} noPlay={shouldAnimate} />;
}
if (!emoji) {
return <Icon name="folder-tabs-folder" />;
}
const iconName = emojiToFolderIcon(emoji);
if (iconName) {
return <Icon name={iconName} />;
}
return <div className={styles.emoji}>{renderText(emoji)}</div>;
};
export default memo(FolderIcon);

View File

@ -6,6 +6,7 @@ import buildClassName from '../../util/buildClassName';
import { copyTextToClipboard } from '../../util/clipboard';
import useAppLayout from '../../hooks/useAppLayout';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
@ -33,7 +34,8 @@ const InviteLink: FC<OwnProps> = ({
withShare,
onRevoke,
}) => {
const lang = useOldLang();
const lang = useLang();
const oldLang = useOldLang();
const { showNotification, openChatWithDraft } = getActions();
const { isMobile } = useAppLayout();
@ -67,7 +69,7 @@ const InviteLink: FC<OwnProps> = ({
color="translucent"
className={isOpen ? 'active' : ''}
onClick={onTrigger}
ariaLabel={lang('AccDescrOpenMenu2')}
ariaLabel={lang('AriaLabelOpenMenu')}
>
<Icon name="more" />
</Button>
@ -77,7 +79,7 @@ const InviteLink: FC<OwnProps> = ({
return (
<div className={className}>
<p className={styles.title}>
{lang(title || 'InviteLink.InviteLink')}
{oldLang(title || 'InviteLink.InviteLink')}
</p>
<div className={styles.primaryLink}>
<input
@ -103,9 +105,9 @@ const InviteLink: FC<OwnProps> = ({
trigger={PrimaryLinkMenuButton}
positionX="right"
>
<MenuItem icon="copy" onClick={handleCopyClick} disabled={isDisabled}>{lang('Copy')}</MenuItem>
<MenuItem icon="copy" onClick={handleCopyClick} disabled={isDisabled}>{oldLang('Copy')}</MenuItem>
{onRevoke && (
<MenuItem icon="delete" onClick={onRevoke} destructive>{lang('RevokeButton')}</MenuItem>
<MenuItem icon="delete" onClick={onRevoke} destructive>{oldLang('RevokeButton')}</MenuItem>
)}
</DropdownMenu>
)}
@ -116,7 +118,7 @@ const InviteLink: FC<OwnProps> = ({
onClick={handleShare}
className={styles.share}
>
{lang('FolderLinkScreen.LinkActionShare')}
{oldLang('FolderLinkScreen.LinkActionShare')}
</Button>
)}
</div>

View File

@ -0,0 +1,85 @@
import { type FC, memo } from '@teact';
import { getActions } from '../../global';
import { LeftColumnContent, SettingsScreens } from '../../types';
import {
APP_NAME,
DEBUG,
IS_BETA,
} from '../../config';
import buildClassName from '../../util/buildClassName';
import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useLeftHeaderButtonRtlForumTransition from '../left/main/hooks/useLeftHeaderButtonRtlForumTransition';
import LeftSideMenuItems from '../left/main/LeftSideMenuItems';
import DropdownMenu from '../ui/DropdownMenu';
type OwnProps = {
trigger?: FC<{ onTrigger: () => void; isOpen?: boolean }>;
shouldHideSearch?: boolean;
className?: string;
};
const LeftSideMenuDropdown = ({
trigger,
shouldHideSearch,
className,
}: OwnProps) => {
const { openLeftColumnContent, closeForumPanel, openSettingsScreen } = getActions();
const [isBotMenuOpen, markBotMenuOpen, unmarkBotMenuOpen] = useFlag();
const lang = useLang();
const versionString = IS_BETA ? `${APP_VERSION} Beta (${APP_REVISION})` : (DEBUG ? APP_REVISION : APP_VERSION);
// Disable dropdown menu RTL animation for resize
const {
shouldDisableDropdownMenuTransitionRef,
handleDropdownMenuTransitionEnd,
} = useLeftHeaderButtonRtlForumTransition(shouldHideSearch);
const handleSelectSettings = useLastCallback(() => {
openSettingsScreen({ screen: SettingsScreens.Main });
});
const handleSelectContacts = useLastCallback(() => {
openLeftColumnContent({ contentKey: LeftColumnContent.Contacts });
});
const handleSelectArchived = useLastCallback(() => {
openLeftColumnContent({ contentKey: LeftColumnContent.Archived });
closeForumPanel();
});
return (
<DropdownMenu
trigger={trigger}
footer={`${APP_NAME} ${versionString}`}
className={buildClassName(
'main-menu',
lang.isRtl && 'rtl',
shouldHideSearch && lang.isRtl && 'right-aligned',
shouldDisableDropdownMenuTransitionRef.current && lang.isRtl && 'disable-transition',
className,
)}
forceOpen={isBotMenuOpen}
positionX={shouldHideSearch && lang.isRtl ? 'right' : 'left'}
transformOriginX={90}
transformOriginY={100}
onTransitionEnd={lang.isRtl ? handleDropdownMenuTransitionEnd : undefined}
>
<LeftSideMenuItems
onSelectArchived={handleSelectArchived}
onSelectContacts={handleSelectContacts}
onSelectSettings={handleSelectSettings}
onBotMenuOpened={markBotMenuOpen}
onBotMenuClosed={unmarkBotMenuOpen}
/>
</DropdownMenu>
);
};
export default memo(LeftSideMenuDropdown);

View File

@ -107,7 +107,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
id, stickerSetInfo,
} = sticker;
const isPremium = (!sticker.isFree && isEffectEmoji) || sticker.hasEffect;
const isPremium = !sticker.isFree || sticker.hasEffect;
const isCustomEmoji = sticker.isCustomEmoji || isEffectEmoji;
const isLocked = !isCurrentUserPremium && isPremium && !shouldIgnorePremium;
@ -148,9 +148,12 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
if (isLocked) {
if (isEffectEmoji) {
openPremiumModal({ initialSection: 'effects' });
} else if (isCustomEmoji) {
openPremiumModal({ initialSection: 'animated_emoji' });
} else {
openPremiumModal({ initialSection: 'premium_stickers' });
}
onContextMenuClose?.();
return;
}
onClick?.(clickArg);

View File

@ -19,9 +19,18 @@
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 100%;
&.foldersSidebarVisible {
grid-template-columns: auto auto 1fr;
}
}
}
.foldersSidebar {
width: var(--tabs-sidebar-width);
height: 100%;
background: var(--color-background-sidebar);
}
.left {
flex: 1;

View File

@ -5,8 +5,10 @@ import { getActions, getGlobal, withGlobal } from '../../global';
import type { TabState } from '../../global/types';
import { ApiMediaFormat } from '../../api/types';
import { TABS_POSITION_LEFT } from '../../config';
import { getChatAvatarHash } from '../../global/helpers/chats'; // Direct import for better module splitting
import { selectIsRightColumnShown, selectTabState } from '../../global/selectors';
import { selectAreFoldersPresent, selectIsRightColumnShown, selectTabState } from '../../global/selectors';
import { selectSharedSettings } from '../../global/selectors/sharedState';
import buildClassName from '../../util/buildClassName';
import { preloadImage } from '../../util/files';
import preloadFonts from '../../util/fonts';
@ -48,6 +50,7 @@ type OwnProps = {
type StateProps = Pick<TabState, 'uiReadyState' | 'shouldSkipHistoryAnimations'> & {
isRightColumnShown?: boolean;
leftColumnWidth?: number;
isFoldersSidebarShown?: boolean;
};
const MAX_PRELOAD_DELAY = 700;
@ -104,6 +107,7 @@ const UiLoader: FC<OwnProps & StateProps> = ({
isRightColumnShown,
shouldSkipHistoryAnimations,
leftColumnWidth,
isFoldersSidebarShown,
}) => {
const { setIsUiReady } = getActions();
@ -151,7 +155,8 @@ const UiLoader: FC<OwnProps & StateProps> = ({
{shouldRenderMask && !shouldSkipHistoryAnimations && Boolean(page) && (
<div className={buildClassName(styles.mask, transitionClassNames)}>
{page === 'main' ? (
<div className={styles.main}>
<div className={buildClassName(styles.main, isFoldersSidebarShown && styles.foldersSidebarVisible)}>
{isFoldersSidebarShown && <div className={styles.foldersSidebar} />}
<div
className={styles.left}
style={leftColumnWidth ? `width: ${leftColumnWidth}px` : undefined}
@ -174,11 +179,14 @@ export default withGlobal<OwnProps>(
(global, { isMobile }): Complete<StateProps> => {
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);

View File

@ -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) {

View File

@ -38,6 +38,7 @@ import './LeftColumn.scss';
interface OwnProps {
ref: ElementRef<HTMLDivElement>;
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}
/>
);
}

View File

@ -14,6 +14,10 @@
display: none;
}
&.no-margin-top {
margin-top: 0 !important;
}
&:hover {
opacity: 0.85;
}

View File

@ -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<OwnProps> = ({
archiveSettings,
onDragEnter,
onClick,
isFoldersSidebarShown,
}) => {
const { updateArchiveSettings } = getActions();
const lang = useLang();
@ -158,6 +160,7 @@ const Archive: FC<OwnProps> = ({
className={buildClassName(
styles.root,
archiveSettings.isMinimized && styles.minimized,
isFoldersSidebarShown && archiveSettings.isMinimized && styles.noMarginTop,
'chat-item-clickable',
'chat-item-archive',
)}

View File

@ -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<OwnProps & StateProps> = ({
areTagsEnabled,
withTags,
onReorderAnimationEnd,
isFoldersSidebarShown,
}) => {
const {
openChat,
@ -491,6 +493,7 @@ const Chat: FC<OwnProps & StateProps> = ({
itemClassName="chat-tag"
orderedFolderIds={tagFolderIds}
chatFoldersById={chatFoldersById}
isFoldersSidebarShown={isFoldersSidebarShown}
/>
)}
</div>

View File

@ -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<OwnProps & StateProps> = ({
isStoryRibbonShown,
sessions,
isAccountFrozen,
isFoldersSidebarShown,
}) => {
const {
loadChatFolders,
setActiveChatFolder,
openChat,
openShareChatFolderModal,
openDeleteChatFolderModal,
openEditChatFolder,
openLimitReachedModal,
markChatMessagesRead,
openSettingsScreen,
} = getActions();
const transitionRef = useRef<HTMLDivElement>();
@ -118,149 +106,27 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
const isStoryRibbonClosing = useDerivedState(getIsStoryRibbonClosing);
const scrollToTop = useLastCallback(() => {
const activeList = ref.current?.querySelector<HTMLElement>('.chat-list.Transition_slide-active');
const activeList = ref.current?.querySelector<HTMLElement>('#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<OwnProps & StateProps> = ({
archiveSettings={archiveSettings}
sessions={sessions}
isAccountFrozen={isAccountFrozen}
isFoldersSidebarShown={isFoldersSidebarShown}
isStoryRibbonShown={isStoryRibbonShown}
withTags
/>
@ -381,12 +248,13 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
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 && <StoryRibbon isClosing={isStoryRibbonClosing} />}
{shouldRenderFolders ? (
{shouldRenderFolders && !isFoldersSidebarShown ? (
<TabList
contextRootElementSelector="#LeftColumn"
tabs={folderTabs}

View File

@ -51,6 +51,7 @@ type OwnProps = {
isAccountFrozen?: boolean;
isMainList?: boolean;
withTags?: boolean;
isFoldersSidebarShown?: boolean;
isStoryRibbonShown?: boolean;
foldersDispatch?: FolderEditDispatch;
};
@ -70,6 +71,7 @@ const ChatList: FC<OwnProps> = ({
isAccountFrozen,
isMainList,
withTags,
isFoldersSidebarShown,
isStoryRibbonShown,
foldersDispatch,
}) => {
@ -234,6 +236,7 @@ const ChatList: FC<OwnProps> = ({
onDragEnter={handleChatDragEnter}
onDragLeave={onDragLeave}
withTags={withTags}
isFoldersSidebarShown={isFoldersSidebarShown}
/>
);
});
@ -269,6 +272,7 @@ const ChatList: FC<OwnProps> = ({
archiveSettings={archiveSettings}
onClick={handleArchivedClick}
onDragEnter={handleArchivedDragEnter}
isFoldersSidebarShown={isFoldersSidebarShown}
/>
)}
{viewportIds?.length ? (

View File

@ -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<number, ApiChatFolder>;
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 (
<div className={styles.wrapper}>
{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)}
</div>
);
})}

View File

@ -47,6 +47,10 @@
opacity: 0.25;
}
&--tabs-sidebar-shown .chat-list {
padding-top: 0;
}
.Tab {
flex: 0 0 auto;
}

View File

@ -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<OwnProps> = ({
onReset,
onTopicSearch,
isAccountFrozen,
isFoldersSidebarShown,
}) => {
const { closeForumPanel, openLeftColumnContent } = getActions();
const { openLeftColumnContent } = getActions();
const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV);
const [tauriUpdate, setTauriUpdate] = useState<Update>();
const [isTauriUpdateDownloading, setIsTauriUpdateDownloading] = useState(false);
@ -109,19 +111,10 @@ const LeftMain: FC<OwnProps> = ({
}, 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<OwnProps> = ({
content={content}
contactsFilter={contactsFilter}
onSearchQuery={onSearchQuery}
onSelectSettings={handleSelectSettings}
onSelectContacts={handleSelectContacts}
onSelectArchived={handleSelectArchived}
onReset={onReset}
shouldSkipTransition={shouldSkipTransition}
isClosingSearch={isClosingSearch}
isFoldersSidebarShown={isFoldersSidebarShown}
/>
<Transition
name={shouldSkipTransition ? 'none' : 'zoomFade'}
@ -222,6 +213,7 @@ const LeftMain: FC<OwnProps> = ({
shouldHideFolderTabs={isForumPanelVisible}
foldersDispatch={foldersDispatch}
isForumPanelOpen={isForumPanelVisible}
isFoldersSidebarShown={isFoldersSidebarShown}
/>
);
case LeftColumnContent.GlobalSearch:

View File

@ -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 {

View File

@ -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<GlobalState, 'connectionState' | 'isSyncing' | 'isFetchingDifference'>;
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<GlobalState, 'connectionState' | 'isSyncing' | 'isFetchingDifference'>;
const CLEAR_DATE_SEARCH_PARAM = { date: undefined };
const CLEAR_CHAT_SEARCH_PARAM = { id: undefined };
const LeftMainHeader: FC<OwnProps & StateProps> = ({
const IS_WITH_WINDOW_BUTTONS = IS_TAURI && IS_MAC_OS;
const LeftMainHeader = ({
shouldHideSearch,
content,
contactsFilter,
@ -102,12 +93,11 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
areChatsLoaded,
hasPasscode,
canSetPasscode,
isFoldersSidebarShown,
isForumPanelOpen,
onSearchQuery,
onSelectSettings,
onSelectContacts,
onSelectArchived,
onReset,
}) => {
}: OwnProps & StateProps) => {
const {
setGlobalSearchDate,
setSharedSettingOption,
@ -115,17 +105,18 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}
});
const handleForumSearchClick = useLastCallback(() => {
closeForumPanel();
});
useHotkeys(useMemo(() => (canSetPasscode ? {
'Ctrl+Shift+L': handleLockScreenHotkey,
'Alt+Shift+L': handleLockScreenHotkey,
@ -165,20 +160,24 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
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')}
>
<div className={buildClassName(
'animated-menu-icon',
!hasMenu && 'state-back',
shouldSkipTransition && 'no-animation',
{isSearchButton ? (
<Icon name="search" />
) : (
<div className={buildClassName(
'animated-menu-icon',
!hasMenu && 'state-back',
shouldSkipTransition && 'no-animation',
)}
/>
)}
/>
</Button>
);
}, [hasMenu, isMobile, oldLang, onReset, shouldSkipTransition]);
}, [hasMenu, isSearchButton, isMobile, lang, onReset, shouldSkipTransition]);
const handleSearchFocus = useLastCallback(() => {
if (!searchQuery) {
@ -215,16 +214,6 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
? 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<OwnProps & StateProps> = ({
);
}, [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 (
<div className="LeftMainHeader">
<div
id="LeftMainHeader"
className="left-header"
data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}
data-tauri-drag-region={IS_WITH_WINDOW_BUTTONS ? true : undefined}
>
{lang.isRtl && <div className="DropdownMenuFiller" />}
<DropdownMenu
<MainMenuDropdown
trigger={MainButton}
footer={version}
className={buildClassName(
'main-menu',
lang.isRtl && 'rtl',
shouldHideSearch && lang.isRtl && 'right-aligned',
shouldDisableDropdownMenuTransitionRef.current && lang.isRtl && 'disable-transition',
hasMenu && isFoldersSidebarShown && !IS_WITH_WINDOW_BUTTONS && !isSearchButton && 'hide-menu-button',
isSearchButton && 'forum-search-button',
)}
forceOpen={isBotMenuOpen}
positionX={shouldHideSearch && lang.isRtl ? 'right' : 'left'}
transformOriginX={IS_TAURI && IS_MAC_OS && !isFullscreen ? 90 : undefined}
onTransitionEnd={lang.isRtl ? handleDropdownMenuTransitionEnd : undefined}
>
<LeftSideMenuItems
onSelectArchived={onSelectArchived}
onSelectContacts={onSelectContacts}
onSelectSettings={onSelectSettings}
onBotMenuOpened={markBotMenuOpen}
onBotMenuClosed={unmarkBotMenuOpen}
/>
</DropdownMenu>
/>
<SearchInput
inputId="telegram-search-input"
resultsItemSelector=".LeftSearch .ListItem-button"
className={buildClassName(
(globalSearchChatId || searchDate) ? 'with-picker-item' : undefined,
shouldHideSearch && 'SearchInput--hidden',
hasMenu && isFoldersSidebarShown && !IS_WITH_WINDOW_BUTTONS && 'SearchInput--no-left-margin',
)}
value={isClosingSearch ? undefined : (contactsFilter || searchQuery)}
focused={isSearchFocused}
@ -363,6 +327,7 @@ export default memo(withGlobal<OwnProps>(
connectionState, isSyncing, isFetchingDifference,
} = global;
const { isConnectionStatusMinimized } = selectSharedSettings(global);
const isForumPanelOpen = selectIsForumPanelOpen(global);
return {
searchQuery,
@ -380,6 +345,7 @@ export default memo(withGlobal<OwnProps>(
areChatsLoaded: Boolean(global.chats.listIds.active),
hasPasscode: Boolean(global.passcode.hasPasscode),
canSetPasscode: selectCanSetPasscode(global),
isForumPanelOpen,
};
},
)(LeftMainHeader));

View File

@ -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 (
<Menu
isOpen={isOpen}
positionX="left"
onClose={onClose}
withPortal
className="settings-folders-icon-picker-menu SymbolMenu"
>
<div className="SymbolMenu-main">
<div className="settings-folders-icon-picker-menu-folders">
{
Object.keys(folderIconMap).map((emoji) => (
<div className="EmojiButton" onClick={() => handleEmojiSelect(emoji)}>
<Icon name={folderIconMap[emoji]} />
</div>
))
}
</div>
<CustomEmojiPicker
idPrefix="folder-emoji-set-"
loadAndPlay={isOpen}
isHidden={!isOpen}
onCustomEmojiSelect={(emoji) => handleEmojiSelect(emoji)}
onContextMenuClick={onClose}
onContextMenuClose={onClose}
/>
</div>
</Menu>
);
};
export default memo(FolderIconPickerMenu);

View File

@ -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);
}

View File

@ -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<OwnProps> = ({
isActive,
onReset,
}) => {
const { isMobile } = useAppLayout();
const {
openShareChatFolderModal,
editChatFolder,
@ -164,6 +167,7 @@ const SettingsFolders: FC<OwnProps> = ({
SettingsScreens.FoldersExcludedChats,
].includes(shownScreen)}
onReset={onReset}
isMobile={isMobile}
/>
);
case SettingsScreens.FoldersCreateFolder:
@ -186,6 +190,7 @@ const SettingsFolders: FC<OwnProps> = ({
isOnlyInvites={currentScreen === SettingsScreens.FoldersEditFolderInvites}
onBack={onReset}
onSaveFolder={handleSaveFolder}
isMobile={isMobile}
/>
);
case SettingsScreens.FoldersIncludedChats:

View File

@ -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<OwnProps & StateProps> = ({
state,
dispatch,
@ -89,6 +101,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
chatListCount,
onSaveFolder,
isCurrentUserPremium,
isMobile,
}) => {
const {
loadChatlistInvites,
@ -102,6 +115,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<HTMLInputElement>) => {
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<OwnProps & StateProps> = ({
);
}
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 (
<div className="settings-fab-wrapper">
<div className="settings-content no-border custom-scroll">
@ -303,15 +393,36 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
{lang('FilterIncludeInfo')}
</p>
)}
<div className="settings-folders-input-container">
<InputText
className={buildClassName('mb-0', !isMobile && 'settings-folders-input-with-icon')}
label={lang('FilterNameHint')}
value={titleText}
maxLength={folderTitleMaxLength}
onChange={handleChange}
error={state.error && state.error === ERROR_NO_TITLE ? ERROR_NO_TITLE : undefined}
/>
<InputText
className="mb-0"
label={lang('FilterNameHint')}
value={state.folder.title.text}
maxLength={FOLDER_TITLE_MAX_LENGTH}
onChange={handleChange}
error={state.error && state.error === ERROR_NO_TITLE ? ERROR_NO_TITLE : undefined}
/>
{!isMobile && (
<div className="settings-folders-icon-picker" dir={lang.isRtl ? 'rtl' : undefined}>
<div
className="settings-folders-icon-picker-button"
onClick={handleIconPickerOpen}
>
<FolderIcon
emoji={state.folder.emoticon}
customEmojiId={currentCustomEmoji?.documentId}
shouldAnimate={state.folder.noTitleAnimations}
/>
</div>
<FolderIconPickerMenu
isOpen={isIconPickerMenuOpen}
onEmojiSelect={handleEmojiSelect}
onClose={handleIconPickerClose}
/>
</div>
)}
</div>
</div>
{!isOnlyInvites && (

View File

@ -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<OwnProps & StateProps> = ({
recommendedChatFolders,
maxFolders,
areTagsEnabled,
tabsPosition,
isMobile,
}) => {
const {
loadRecommendedChatFolders,
@ -76,6 +83,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
sortChatFolders,
toggleDialogFilterTags,
openPremiumModal,
setSharedSettingOption,
} = getActions();
const [state, setState] = useState<SortState>({
@ -207,6 +215,10 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
});
}, [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<OwnProps & StateProps> = ({
{!isPremium && <Icon name="lock-badge" className="settings-folders-lock-icon" />}
</div>
</div>
{!isMobile && (
<div className="settings-item pt-3">
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('TabsPosition')}</h4>
<RadioGroup
name="tabsPosition"
options={[{
label: lang('TabsPositionLeft'),
value: 'left',
}, {
label: lang('TabsPositionTop'),
value: 'top',
}]}
selected={tabsPosition}
onChange={handleTabsPositionChange}
/>
</div>
)}
</div>
);
};
@ -431,6 +461,7 @@ export default memo(withGlobal<OwnProps>(
recommendedChatFolders,
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
areTagsEnabled,
tabsPosition: global.sharedState.settings.tabsPosition,
};
},
)(SettingsFoldersMain));

View File

@ -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;
}

View File

@ -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<number, ApiChatFolder>;
folderInvitesById: Record<number, ApiChatlistExportedInvite[]>;
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<HTMLDivElement>();
useEffect(() => {
loadChatFolders();
}, []);
const scrollChatListToTop = useLastCallback(() => {
const activeList = document.querySelector<HTMLElement>('#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 }) => (
<Button
color="translucent"
className={buildClassName(isOpen ? 'active' : '', styles.menuButton)}
onClick={onTrigger}
ariaLabel={lang('AriaLabelOpenMenu')}
>
<Icon name="menu" className={styles.icon} />
</Button>
);
}, [lang]);
if (!isActive) {
return undefined;
}
return (
<div
className={styles.root}
id="FoldersSidebar"
>
<MainMenuDropdown
trigger={MainButton}
className={buildClassName(IS_TAURI && IS_MAC_OS && styles.hideMenuButton)}
/>
{!isAtBeginning && <div className={styles.divider} />}
<div
ref={tabsRef}
className={buildClassName(styles.tabs, 'custom-scroll', 'no-scrollbar')}
onScroll={handleScroll}
>
{folderTabs?.map((tab, i) => (
<Folder
key={tab.id}
title={tab.title}
isActive={i === activeChatFolder}
isBlocked={tab.isBlocked}
badgeCount={tab.badgeCount}
isBadgeActive={tab.isBadgeActive}
onClick={handleSwitchTab}
clickArg={i}
contextActions={tab.contextActions}
contextRootElementSelector="#FoldersSidebar"
className={styles.tab}
icon={(
<FolderIcon
emoji={(tab.emoticon as string)}
customEmojiId={(tab.emoticon as ApiMessageEntityCustomEmoji)?.documentId}
shouldAnimate={tab.noTitleAnimations}
/>
)}
/>
))}
</div>
{!isAtEnd && <div className={styles.divider} />}
<Button
color="translucent"
className={buildClassName(styles.menuButton, styles.settingsButton)}
onClick={handleSettingsClick}
>
<Icon name="tools" className={styles.icon} />
</Button>
</div>
);
};
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));

View File

@ -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;
}
}
}

View File

@ -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 (
<div ref={containerRef} id="Main" className={className}>
<LeftColumn ref={leftColumnRef} />
<FoldersSidebar isMobile={isMobile} isActive={isFoldersSidebarShown} />
<LeftColumn ref={leftColumnRef} isFoldersSidebarShown={isFoldersSidebarShown} />
<MiddleColumn leftColumnRef={leftColumnRef} isMobile={isMobile} />
<RightColumn isMobile={isMobile} />
<MediaViewer isOpen={isMediaViewerOpen} />
@ -637,7 +643,7 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
isSynced: global.isSynced,
isAccountFrozen,
isAppConfigLoaded: global.isAppConfigLoaded,
isFoldersSidebarShown: tabsPosition === TABS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global),
};
},
)(Main));

View File

@ -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);
}

View File

@ -208,7 +208,7 @@ const PaidReactionModal = ({
color="translucent"
className={buildClassName(styles.sendAsPeerMenuButton, isOpen ? 'active' : '')}
onClick={onTrigger}
ariaLabel={lang('AccDescrOpenMenu2')}
ariaLabel={lang('AriaLabelOpenMenu')}
>
<Avatar
className={styles.sendAsPeerButtonAvatar}

View File

@ -384,7 +384,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
color="translucent"
className={isOpen ? 'active' : ''}
onClick={onTrigger}
ariaLabel={lang('AccDescrOpenMenu2')}
ariaLabel={lang('AriaLabelOpenMenu')}
>
<Icon name="more" />
</Button>

View File

@ -571,13 +571,13 @@ function Story({
color="translucent-white"
onClick={onTrigger}
className={buildClassName(styles.button, isOpen && 'active')}
ariaLabel={oldLang('AccDescrOpenMenu2')}
ariaLabel={lang('AriaLabelOpenMenu')}
>
<Icon name="more" />
</Button>
);
};
}, [isMobile, oldLang]);
}, [isMobile, lang]);
function renderStoriesTabs() {
return (

View File

@ -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<HTMLElement>('.left-header');
const ribbonPeers = ribbon?.querySelectorAll<HTMLElement>(`.${ribbonStyles.peer}`);
const toggleAvatars = toggler?.querySelectorAll<HTMLElement>('.Avatar');
const sidebar = document.getElementById('FoldersSidebar');
return {
container,
@ -313,6 +319,7 @@ function getHTMLElements(isArchived?: boolean) {
leftMainHeader,
ribbonPeers,
toggleAvatars,
sidebar,
};
}

View File

@ -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);
}
}
}

View File

@ -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<HTMLDivElement>();
const {
contextMenuAnchor, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose,
handleContextMenuHide, isContextMenuOpen,
} = useContextMenuHandlers(folderRef, !contextActions);
const { handleClick, handleMouseDown } = useFastClick((e: React.MouseEvent<HTMLDivElement>) => {
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 (
<div
className={buildClassName(styles.folder, isActive && styles.active, className)}
onClick={handleClick}
onMouseDown={handleMouseDown}
onContextMenu={handleContextMenu}
ref={folderRef}
>
<div className={styles.icon}>
{icon}
{Boolean(badgeCount) && (
<span className={buildClassName(styles.badge, isBadgeActive && styles.badgeActive)}>{badgeCount}</span>
)}
</div>
<span className={styles.inner}>
<div className={styles.title}>
{isBlocked && <Icon name="lock-badge" className={styles.blocked} />}
{title}
</div>
</span>
{contextActions && contextMenuAnchor !== undefined && (
<Menu
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getMenuElement={getMenuElement}
getLayout={getLayout}
className={styles.contextMenu}
autoClose
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
withPortal
>
{contextActions.map((action) => (
('isSeparator' in action) ? (
<MenuSeparator key={action.key || 'separator'} />
) : (
<MenuItem
key={action.title}
icon={action.icon}
destructive={action.destructive}
disabled={!action.handler}
onClick={action.handler}
>
{action.title}
</MenuItem>
)
))}
</Menu>
)}
</div>
);
};
export default Folder;

View File

@ -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}
<span className="Tab_inner">
{typeof title === 'string' ? renderText(title) : title}
{Boolean(badgeCount) && (

View File

@ -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 = {

View File

@ -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

View File

@ -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);
});

View File

@ -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;
}

View File

@ -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,

View File

@ -383,3 +383,8 @@ export function selectIsChatRestricted<T extends GlobalState>(global: T, chatId:
const activeRestrictions = selectActiveRestrictionReasons(global, chat.restrictionReasons);
return activeRestrictions.length > 0;
}
export function selectAreFoldersPresent<T extends GlobalState>(global: T) {
const ids = global.chatFolders.orderedIds;
return Boolean(ids && ids.length > 1);
}

View File

@ -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';

View File

@ -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<ApiChatFolder, 'id' | 'description' | 'emoticon'>;
folder: Omit<ApiChatFolder, 'id' | 'description'>;
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<FoldersState, FoldersActions>;
@ -140,7 +140,9 @@ const foldersReducer: StateReducer<FoldersState, FoldersActions> = (
...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<FoldersState, FoldersActions> = (
},
isTouched: true,
};
case 'setEmoticon': {
return {
...state,
folder: {
...state.folder,
emoticon: action.payload,
},
isTouched: true,
};
}
case 'reset':
return INITIAL_STATE;
default:

235
src/hooks/useFolderTabs.ts Normal file
View File

@ -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<number, ApiChatFolder>;
maxFolders: number;
maxChatLists: number;
folderInvitesById: Record<number, ApiChatlistExportedInvite[]>;
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;

View File

@ -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;

View File

@ -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";
}

View File

@ -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",
);

Binary file not shown.

Binary file not shown.

View File

@ -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"],

View File

@ -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'

View File

@ -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'

View File

@ -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;

View File

@ -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',

20
src/util/folderIconMap.ts Normal file
View File

@ -0,0 +1,20 @@
import type { IconName } from '../types/icons';
export const folderIconMap: Record<string, IconName> = {
'🗂': '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);
};