Folders Sidebar: Follow-up (#6501)

This commit is contained in:
zubiden 2025-11-22 12:54:21 +01:00 committed by Alexander Zinchuk
parent 111c702d73
commit 3ba0a90d63
26 changed files with 129 additions and 162 deletions

View File

@ -29,6 +29,7 @@ type OwnProps = {
noPlay?: boolean; noPlay?: boolean;
noVideoOnMobile?: boolean; noVideoOnMobile?: boolean;
loopLimit?: number; loopLimit?: number;
shouldNotLoop?: boolean;
isSelectable?: boolean; isSelectable?: boolean;
withSharedAnimation?: boolean; withSharedAnimation?: boolean;
sharedCanvasRef?: ElementRef<HTMLCanvasElement>; sharedCanvasRef?: ElementRef<HTMLCanvasElement>;
@ -55,6 +56,7 @@ const CustomEmoji: FC<OwnProps> = ({
noPlay, noPlay,
noVideoOnMobile, noVideoOnMobile,
loopLimit, loopLimit,
shouldNotLoop,
isSelectable, isSelectable,
withSharedAnimation, withSharedAnimation,
sharedCanvasRef, sharedCanvasRef,
@ -146,7 +148,7 @@ const CustomEmoji: FC<OwnProps> = ({
noVideoOnMobile={noVideoOnMobile} noVideoOnMobile={noVideoOnMobile}
thumbClassName={styles.thumb} thumbClassName={styles.thumb}
fullMediaClassName={styles.media} fullMediaClassName={styles.media}
shouldLoop shouldLoop={!shouldNotLoop}
loopLimit={loopLimit} loopLimit={loopLimit}
shouldPreloadPreview={shouldPreloadPreview || noPlay || !canPlay} shouldPreloadPreview={shouldPreloadPreview || noPlay || !canPlay}
forceOnHeavyAnimation={forceOnHeavyAnimation} forceOnHeavyAnimation={forceOnHeavyAnimation}

View File

@ -71,9 +71,7 @@ type OwnProps = {
onCustomEmojiSelect: (sticker: ApiSticker) => void; onCustomEmojiSelect: (sticker: ApiSticker) => void;
onReactionSelect?: (reaction: ApiReactionWithPaid) => void; onReactionSelect?: (reaction: ApiReactionWithPaid) => void;
onReactionContext?: (reaction: ApiReactionWithPaid) => void; onReactionContext?: (reaction: ApiReactionWithPaid) => void;
onContextMenuOpen?: NoneToVoidFunction; onDismiss?: NoneToVoidFunction;
onContextMenuClose?: NoneToVoidFunction;
onContextMenuClick?: NoneToVoidFunction;
}; };
type StateProps = { type StateProps = {
@ -142,9 +140,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
onCustomEmojiSelect, onCustomEmojiSelect,
onReactionSelect, onReactionSelect,
onReactionContext, onReactionContext,
onContextMenuOpen, onDismiss,
onContextMenuClose,
onContextMenuClick,
}) => { }) => {
const containerRef = useRef<HTMLDivElement>(); const containerRef = useRef<HTMLDivElement>();
const headerRef = useRef<HTMLDivElement>(); const headerRef = useRef<HTMLDivElement>();
@ -466,9 +462,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
onReactionSelect={onReactionSelect} onReactionSelect={onReactionSelect}
onReactionContext={onReactionContext} onReactionContext={onReactionContext}
onStickerSelect={handleEmojiSelect} onStickerSelect={handleEmojiSelect}
onContextMenuOpen={onContextMenuOpen} onDismiss={onDismiss}
onContextMenuClose={onContextMenuClose}
onContextMenuClick={onContextMenuClick}
forcePlayback forcePlayback
/> />
); );

View File

@ -11,19 +11,19 @@ import styles from './FolderIcon.module.scss';
const ICON_SIZE = 2.25 * REM; const ICON_SIZE = 2.25 * REM;
const FolderIcon = ( type OwnProps = {
{ emoji?: string;
emoji, customEmojiId?: string;
customEmojiId, shouldAnimate?: boolean;
shouldAnimate, };
}: {
emoji?: string; const FolderIcon = ({
customEmojiId?: string; emoji,
shouldAnimate?: boolean; customEmojiId,
}, shouldAnimate,
) => { }: OwnProps) => {
if (customEmojiId) { if (customEmojiId) {
return <CustomEmoji documentId={customEmojiId} size={ICON_SIZE} noPlay={shouldAnimate} />; return <CustomEmoji documentId={customEmojiId} size={ICON_SIZE} shouldNotLoop={!shouldAnimate} />;
} }
if (!emoji) { if (!emoji) {

View File

@ -45,7 +45,7 @@
vertical-align: bottom; vertical-align: bottom;
} }
.sticker-locked { .sticker-locked, .sticker-premium {
position: absolute; position: absolute;
z-index: 2; z-index: 2;
right: 0; right: 0;
@ -55,30 +55,11 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 1.25rem; width: 1rem;
height: 1.25rem; height: 1rem;
border-radius: 50%;
color: white;
opacity: 0.75;
background: var(--premium-gradient);
}
.sticker-premium {
position: absolute;
z-index: 1;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%; border-radius: 50%;
font-size: 0.875rem;
color: white; color: white;
background: var(--premium-gradient); background: var(--premium-gradient);
@ -135,8 +116,8 @@
top: -0.125rem; top: -0.125rem;
right: -0.125rem; right: -0.125rem;
width: 1.25rem; width: 1rem;
height: 1.25rem; height: 1rem;
padding: 0.125rem; padding: 0.125rem;
opacity: 0; opacity: 0;

View File

@ -45,18 +45,17 @@ type OwnProps<T> = {
sharedCanvasRef?: ElementRef<HTMLCanvasElement>; sharedCanvasRef?: ElementRef<HTMLCanvasElement>;
withTranslucentThumb?: boolean; withTranslucentThumb?: boolean;
forcePlayback?: boolean; forcePlayback?: boolean;
isEffectEmoji?: boolean;
noShowPremium?: boolean;
noIcons?: boolean;
clickArg: T;
onClick?: (arg: OwnProps<T>['clickArg'], isSilent?: boolean, shouldSchedule?: boolean) => void;
observeIntersection: ObserveFn; observeIntersection: ObserveFn;
observeIntersectionForShowing?: ObserveFn; observeIntersectionForShowing?: ObserveFn;
noShowPremium?: boolean;
onClick?: (arg: OwnProps<T>['clickArg'], isSilent?: boolean, shouldSchedule?: boolean) => void;
clickArg: T;
onFaveClick?: (sticker: ApiSticker) => void; onFaveClick?: (sticker: ApiSticker) => void;
onUnfaveClick?: (sticker: ApiSticker) => void; onUnfaveClick?: (sticker: ApiSticker) => void;
onRemoveRecentClick?: (sticker: ApiSticker) => void; onRemoveRecentClick?: (sticker: ApiSticker) => void;
onContextMenuOpen?: NoneToVoidFunction; onDismiss?: NoneToVoidFunction;
onContextMenuClose?: NoneToVoidFunction;
onContextMenuClick?: NoneToVoidFunction;
isEffectEmoji?: boolean;
}; };
const contentForStatusMenuContext = [ const contentForStatusMenuContext = [
@ -77,8 +76,6 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
isSavedMessages, isSavedMessages,
isStatusPicker, isStatusPicker,
canViewSet, canViewSet,
observeIntersection,
observeIntersectionForShowing,
isSelected, isSelected,
isCurrentUserPremium, isCurrentUserPremium,
shouldIgnorePremium, shouldIgnorePremium,
@ -86,15 +83,16 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
sharedCanvasRef, sharedCanvasRef,
withTranslucentThumb, withTranslucentThumb,
forcePlayback, forcePlayback,
onClick, isEffectEmoji,
noIcons,
clickArg, clickArg,
onClick,
observeIntersection,
observeIntersectionForShowing,
onFaveClick, onFaveClick,
onUnfaveClick, onUnfaveClick,
onRemoveRecentClick, onRemoveRecentClick,
onContextMenuOpen, onDismiss,
onContextMenuClose,
onContextMenuClick,
isEffectEmoji,
}: OwnProps<T>) => { }: OwnProps<T>) => {
const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions(); const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions();
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();
@ -109,6 +107,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
const isPremium = !sticker.isFree || sticker.hasEffect; const isPremium = !sticker.isFree || sticker.hasEffect;
const isCustomEmoji = sticker.isCustomEmoji || isEffectEmoji; const isCustomEmoji = sticker.isCustomEmoji || isEffectEmoji;
const isPremiumSticker = !isCustomEmoji && isPremium;
const isLocked = !isCurrentUserPremium && isPremium && !shouldIgnorePremium; const isLocked = !isCurrentUserPremium && isPremium && !shouldIgnorePremium;
const isIntersecting = useIsIntersecting(ref, observeIntersection); const isIntersecting = useIsIntersecting(ref, observeIntersection);
@ -131,14 +130,6 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
}); });
const getLayout = useLastCallback(() => ({ withPortal: isStatusPicker, shouldAvoidNegativePosition: true })); const getLayout = useLastCallback(() => ({ withPortal: isStatusPicker, shouldAvoidNegativePosition: true }));
useEffect(() => {
if (isContextMenuOpen) {
onContextMenuOpen?.();
} else {
onContextMenuClose?.();
}
}, [isContextMenuOpen, onContextMenuClose, onContextMenuOpen]);
useEffect(() => { useEffect(() => {
if (!isIntersecting) handleContextMenuClose(); if (!isIntersecting) handleContextMenuClose();
}, [handleContextMenuClose, isIntersecting]); }, [handleContextMenuClose, isIntersecting]);
@ -153,7 +144,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
} else { } else {
openPremiumModal({ initialSection: 'premium_stickers' }); openPremiumModal({ initialSection: 'premium_stickers' });
} }
onContextMenuClose?.(); onDismiss?.();
return; return;
} }
onClick?.(clickArg); onClick?.(clickArg);
@ -200,7 +191,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
e.stopPropagation(); e.stopPropagation();
handleContextMenuClose(); handleContextMenuClose();
onContextMenuClick?.(); onDismiss?.();
setEmojiStatus({ setEmojiStatus({
emojiStatus: { type: 'regular', documentId: sticker.id, until: getServerTime() + duration }, emojiStatus: { type: 'regular', documentId: sticker.id, until: getServerTime() + duration },
}); });
@ -310,14 +301,14 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
forceAlways={forcePlayback} forceAlways={forcePlayback}
/> />
)} )}
{!noShowPremium && isLocked && ( {!noIcons && !noShowPremium && isLocked && (
<div <div
className="sticker-locked" className="sticker-locked"
> >
<Icon name="lock-badge" /> <Icon name="lock-badge" />
</div> </div>
)} )}
{!noShowPremium && isPremium && !isLocked && ( {!noIcons && !noShowPremium && isPremiumSticker && !isLocked && (
<div className="sticker-premium"> <div className="sticker-premium">
<Icon name="star" /> <Icon name="star" />
</div> </div>

View File

@ -1,4 +1,3 @@
import type { FC } from '../../lib/teact/teact';
import { import {
memo, useEffect, useMemo, useRef, useState, memo, useEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
@ -74,9 +73,7 @@ type OwnProps = {
onStickerUnfave?: (sticker: ApiSticker) => void; onStickerUnfave?: (sticker: ApiSticker) => void;
onStickerFave?: (sticker: ApiSticker) => void; onStickerFave?: (sticker: ApiSticker) => void;
onStickerRemoveRecent?: (sticker: ApiSticker) => void; onStickerRemoveRecent?: (sticker: ApiSticker) => void;
onContextMenuOpen?: NoneToVoidFunction; onDismiss?: NoneToVoidFunction;
onContextMenuClose?: NoneToVoidFunction;
onContextMenuClick?: NoneToVoidFunction;
}; };
type StateProps = { type StateProps = {
@ -89,7 +86,7 @@ const ITEMS_MINI_MOBILE_PER_ROW_FALLBACK = 6;
const MOBILE_WIDTH_THRESHOLD_PX = 440; const MOBILE_WIDTH_THRESHOLD_PX = 440;
const MINI_MOBILE_WIDTH_THRESHOLD_PX = 362; const MINI_MOBILE_WIDTH_THRESHOLD_PX = 362;
const StickerSet: FC<OwnProps & StateProps> = ({ const StickerSet = ({
stickerSet, stickerSet,
loadAndPlay, loadAndPlay,
index, index,
@ -110,6 +107,7 @@ const StickerSet: FC<OwnProps & StateProps> = ({
isTranslucent, isTranslucent,
noContextMenus, noContextMenus,
forcePlayback, forcePlayback,
collectibleStatuses,
observeIntersection, observeIntersection,
observeIntersectionForPlayingItems, observeIntersectionForPlayingItems,
observeIntersectionForShowingItems, observeIntersectionForShowingItems,
@ -119,11 +117,8 @@ const StickerSet: FC<OwnProps & StateProps> = ({
onStickerUnfave, onStickerUnfave,
onStickerFave, onStickerFave,
onStickerRemoveRecent, onStickerRemoveRecent,
onContextMenuOpen, onDismiss,
onContextMenuClose, }: OwnProps & StateProps) => {
onContextMenuClick,
collectibleStatuses,
}) => {
const { const {
clearRecentStickers, clearRecentStickers,
clearRecentCustomEmoji, clearRecentCustomEmoji,
@ -401,13 +396,12 @@ const StickerSet: FC<OwnProps & StateProps> = ({
withTranslucentThumb={isTranslucent} withTranslucentThumb={isTranslucent}
onClick={onStickerSelect} onClick={onStickerSelect}
clickArg={sticker} clickArg={sticker}
noIcons={isEmoji && !isRecent}
isSelected={isSelected} isSelected={isSelected}
onUnfaveClick={isFavorite && favoriteStickerIdsSet?.has(sticker.id) ? onStickerUnfave : undefined} onUnfaveClick={isFavorite && favoriteStickerIdsSet?.has(sticker.id) ? onStickerUnfave : undefined}
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined} onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined} onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
onContextMenuOpen={onContextMenuOpen} onDismiss={onDismiss}
onContextMenuClose={onContextMenuClose}
onContextMenuClick={onContextMenuClick}
forcePlayback={forcePlayback} forcePlayback={forcePlayback}
isEffectEmoji={stickerSet.id === EFFECT_EMOJIS_SET_ID} isEffectEmoji={stickerSet.id === EFFECT_EMOJIS_SET_ID}
noShowPremium={isCurrentUserPremium noShowPremium={isCurrentUserPremium

View File

@ -26,7 +26,7 @@
} }
.foldersSidebar { .foldersSidebar {
width: var(--tabs-sidebar-width); width: var(--folders-sidebar-width);
height: 100%; height: 100%;
background: var(--color-background-sidebar); background: var(--color-background-sidebar);
} }

View File

@ -5,7 +5,7 @@ import { getActions, getGlobal, withGlobal } from '../../global';
import type { TabState } from '../../global/types'; import type { TabState } from '../../global/types';
import { ApiMediaFormat } from '../../api/types'; import { ApiMediaFormat } from '../../api/types';
import { TABS_POSITION_LEFT } from '../../config'; import { FOLDERS_POSITION_LEFT } from '../../config';
import { getChatAvatarHash } from '../../global/helpers/chats'; // Direct import for better module splitting import { getChatAvatarHash } from '../../global/helpers/chats'; // Direct import for better module splitting
import { selectAreFoldersPresent, selectIsRightColumnShown, selectTabState } from '../../global/selectors'; import { selectAreFoldersPresent, selectIsRightColumnShown, selectTabState } from '../../global/selectors';
import { selectSharedSettings } from '../../global/selectors/sharedState'; import { selectSharedSettings } from '../../global/selectors/sharedState';
@ -179,14 +179,14 @@ export default withGlobal<OwnProps>(
(global, { isMobile }): Complete<StateProps> => { (global, { isMobile }): Complete<StateProps> => {
const tabState = selectTabState(global); const tabState = selectTabState(global);
const { tabsPosition } = selectSharedSettings(global); const { foldersPosition } = selectSharedSettings(global);
return { return {
shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations, shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations,
uiReadyState: tabState.uiReadyState, uiReadyState: tabState.uiReadyState,
isRightColumnShown: selectIsRightColumnShown(global, isMobile), isRightColumnShown: selectIsRightColumnShown(global, isMobile),
leftColumnWidth: global.leftColumnWidth, leftColumnWidth: global.leftColumnWidth,
isFoldersSidebarShown: tabsPosition === TABS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global), isFoldersSidebarShown: foldersPosition === FOLDERS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global),
}; };
}, },
)(UiLoader); )(UiLoader);

View File

@ -40,7 +40,7 @@
} }
} }
body.is-tauri.is-macos #Main:not(.is-fullscreen):not(.tabs-sidebar-visible) &:not(#TopicListHeader) { body.is-tauri.is-macos #Main:not(.is-fullscreen):not(.folders-sidebar-visible) &:not(#TopicListHeader) {
padding-left: var(--window-controls-width); padding-left: var(--window-controls-width);
} }

View File

@ -1,6 +1,6 @@
.menuContent { .menuContent {
--offset-y: 3.25rem !important; --offset-y: 3.25rem !important;
--offset-x: auto !important; --offset-x: var(--folders-sidebar-width) !important;
--color-text: var(--color-primary); --color-text: var(--color-primary);
--border-radius-default: 1.25rem; --border-radius-default: 1.25rem;

View File

@ -1,4 +1,4 @@
import type { ElementRef, FC } from '../../../lib/teact/teact'; import type { ElementRef } from '../../../lib/teact/teact';
import { import {
memo, useCallback, useEffect, useRef, memo, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
@ -8,8 +8,6 @@ import type { ApiSticker } from '../../../api/types';
import { selectIsContextMenuTranslucent } from '../../../global/selectors'; import { selectIsContextMenuTranslucent } from '../../../global/selectors';
import useFlag from '../../../hooks/useFlag';
import CustomEmojiPicker from '../../common/CustomEmojiPicker'; import CustomEmojiPicker from '../../common/CustomEmojiPicker';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
import Portal from '../../ui/Portal'; import Portal from '../../ui/Portal';
@ -28,18 +26,17 @@ interface StateProps {
isTranslucent?: boolean; isTranslucent?: boolean;
} }
const StatusPickerMenu: FC<OwnProps & StateProps> = ({ const StatusPickerMenu = ({
isOpen, isOpen,
statusButtonRef, statusButtonRef,
areFeaturedStickersLoaded, areFeaturedStickersLoaded,
isTranslucent, isTranslucent,
onEmojiStatusSelect, onEmojiStatusSelect,
onClose, onClose,
}) => { }: OwnProps & StateProps) => {
const { loadFeaturedEmojiStickers } = getActions(); const { loadFeaturedEmojiStickers } = getActions();
const transformOriginX = useRef<number>(); const transformOriginX = useRef<number>(0);
const [isContextMenuShown, markContextMenuShown, unmarkContextMenuShown] = useFlag();
useEffect(() => { useEffect(() => {
transformOriginX.current = statusButtonRef.current!.getBoundingClientRect().right; transformOriginX.current = statusButtonRef.current!.getBoundingClientRect().right;
}, [isOpen, statusButtonRef]); }, [isOpen, statusButtonRef]);
@ -60,11 +57,10 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
<Menu <Menu
isOpen={isOpen} isOpen={isOpen}
noCompact noCompact
positionX="right" positionX="left"
bubbleClassName={styles.menuContent} bubbleClassName={styles.menuContent}
onClose={onClose} onClose={onClose}
transformOriginX={transformOriginX.current} transformOriginX={transformOriginX.current}
noCloseOnBackdrop={isContextMenuShown}
> >
<CustomEmojiPicker <CustomEmojiPicker
idPrefix="status-emoji-set-" idPrefix="status-emoji-set-"
@ -72,10 +68,8 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
isHidden={!isOpen} isHidden={!isOpen}
isStatusPicker isStatusPicker
isTranslucent={isTranslucent} isTranslucent={isTranslucent}
onContextMenuOpen={markContextMenuShown} onDismiss={onClose}
onContextMenuClose={unmarkContextMenuShown}
onCustomEmojiSelect={handleEmojiSelect} onCustomEmojiSelect={handleEmojiSelect}
onContextMenuClick={onClose}
/> />
</Menu> </Menu>
</Portal> </Portal>

View File

@ -47,8 +47,7 @@ const FolderIconPickerMenu = ({
loadAndPlay={isOpen} loadAndPlay={isOpen}
isHidden={!isOpen} isHidden={!isOpen}
onCustomEmojiSelect={(emoji) => handleEmojiSelect(emoji)} onCustomEmojiSelect={(emoji) => handleEmojiSelect(emoji)}
onContextMenuClick={onClose} onDismiss={onClose}
onContextMenuClose={onClose}
/> />
</div> </div>
</Menu> </Menu>

View File

@ -1,13 +1,14 @@
import type { FC } from '../../../../lib/teact/teact';
import { import {
memo, useCallback, useEffect, useMemo, useState, memo, useCallback, useEffect, useMemo, useState,
} from '../../../../lib/teact/teact'; } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global'; import { getActions, withGlobal } from '../../../../global';
import type { ApiChatFolder } from '../../../../api/types'; import type { ApiChatFolder } from '../../../../api/types';
import type { TabsPosition } from '../../../../types'; import type { FoldersPosition } from '../../../../types';
import { ALL_FOLDER_ID, STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config'; import {
ALL_FOLDER_ID, FOLDERS_POSITION_LEFT, FOLDERS_POSITION_TOP, STICKER_SIZE_FOLDER_SETTINGS,
} from '../../../../config';
import { getFolderDescriptionText } from '../../../../global/helpers'; import { getFolderDescriptionText } from '../../../../global/helpers';
import { selectIsCurrentUserPremium } from '../../../../global/selectors'; import { selectIsCurrentUserPremium } from '../../../../global/selectors';
import { selectCurrentLimit } from '../../../../global/selectors/limits'; import { selectCurrentLimit } from '../../../../global/selectors/limits';
@ -36,10 +37,10 @@ import RadioGroup from '../../../ui/RadioGroup';
type OwnProps = { type OwnProps = {
isActive?: boolean; isActive?: boolean;
isMobile?: boolean;
onCreateFolder: () => void; onCreateFolder: () => void;
onEditFolder: (folder: ApiChatFolder) => void; onEditFolder: (folder: ApiChatFolder) => void;
onReset: () => void; onReset: () => void;
isMobile?: boolean;
}; };
type StateProps = { type StateProps = {
@ -49,7 +50,7 @@ type StateProps = {
maxFolders: number; maxFolders: number;
isPremium?: boolean; isPremium?: boolean;
areTagsEnabled?: boolean; areTagsEnabled?: boolean;
tabsPosition: TabsPosition; foldersPosition: FoldersPosition;
}; };
type SortState = { type SortState = {
@ -61,20 +62,20 @@ type SortState = {
const FOLDER_HEIGHT_PX = 56; const FOLDER_HEIGHT_PX = 56;
const runThrottledForLoadRecommended = throttle((cb) => cb(), 60000, true); const runThrottledForLoadRecommended = throttle((cb) => cb(), 60000, true);
const SettingsFoldersMain: FC<OwnProps & StateProps> = ({ const SettingsFoldersMain = ({
isActive, isActive,
onCreateFolder,
onEditFolder,
onReset,
folderIds,
foldersById, foldersById,
isPremium, isPremium,
recommendedChatFolders, recommendedChatFolders,
maxFolders, maxFolders,
areTagsEnabled, areTagsEnabled,
tabsPosition, foldersPosition,
isMobile, isMobile,
}) => { onCreateFolder,
onEditFolder,
onReset,
folderIds,
}: OwnProps & StateProps) => {
const { const {
loadRecommendedChatFolders, loadRecommendedChatFolders,
addChatFolder, addChatFolder,
@ -215,8 +216,8 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
}); });
}, [sortChatFolders]); }, [sortChatFolders]);
const handleTabsPositionChange = useLastCallback((value: string) => { const handleFoldersPositionChange = useLastCallback((value: string) => {
setSharedSettingOption({ tabsPosition: value as TabsPosition }); setSharedSettingOption({ foldersPosition: value as FoldersPosition });
}); });
const canCreateNewFolder = useMemo(() => { const canCreateNewFolder = useMemo(() => {
@ -430,13 +431,13 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
name="tabsPosition" name="tabsPosition"
options={[{ options={[{
label: lang('TabsPositionLeft'), label: lang('TabsPositionLeft'),
value: 'left', value: FOLDERS_POSITION_LEFT,
}, { }, {
label: lang('TabsPositionTop'), label: lang('TabsPositionTop'),
value: 'top', value: FOLDERS_POSITION_TOP,
}]} }]}
selected={tabsPosition} selected={foldersPosition}
onChange={handleTabsPositionChange} onChange={handleFoldersPositionChange}
/> />
</div> </div>
)} )}
@ -460,7 +461,7 @@ export default memo(withGlobal<OwnProps>(
recommendedChatFolders, recommendedChatFolders,
maxFolders: selectCurrentLimit(global, 'dialogFilters'), maxFolders: selectCurrentLimit(global, 'dialogFilters'),
areTagsEnabled, areTagsEnabled,
tabsPosition: global.sharedState.settings.tabsPosition, foldersPosition: global.sharedState.settings.foldersPosition,
}; };
}, },
)(SettingsFoldersMain)); )(SettingsFoldersMain));

View File

@ -4,7 +4,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: var(--tabs-sidebar-width); width: var(--folders-sidebar-width);
height: 100%; height: 100%;
background-color: var(--color-background-sidebar); background-color: var(--color-background-sidebar);
@ -78,7 +78,7 @@
} }
.menuButton { .menuButton {
width: var(--tabs-sidebar-width); width: var(--folders-sidebar-width);
height: 3.5rem; height: 3.5rem;
border-radius: 0; border-radius: 0;
} }

View File

@ -1,7 +1,7 @@
import { memo, useEffect, useMemo, useRef } from '@teact'; import { memo, useEffect, useMemo, useRef } from '@teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { ApiChatFolder, ApiChatlistExportedInvite, ApiMessageEntityCustomEmoji } from '../../api/types'; import type { ApiChatFolder, ApiChatlistExportedInvite } from '../../api/types';
import { LeftColumnContent, SettingsScreens } from '../../types'; import { LeftColumnContent, SettingsScreens } from '../../types';
import { selectTabState } from '../../global/selectors'; import { selectTabState } from '../../global/selectors';
@ -15,7 +15,6 @@ import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useScrolledState from '../../hooks/useScrolledState'; import useScrolledState from '../../hooks/useScrolledState';
import FolderIcon from '../common/FolderIcon';
import MainMenuDropdown from '../common/MainMenuDropdown'; import MainMenuDropdown from '../common/MainMenuDropdown';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Folder from '../ui/Folder'; import Folder from '../ui/Folder';
@ -158,14 +157,8 @@ const FoldersSidebar = ({
clickArg={i} clickArg={i}
contextActions={tab.contextActions} contextActions={tab.contextActions}
contextRootElementSelector="#FoldersSidebar" contextRootElementSelector="#FoldersSidebar"
icon={tab.emoticon}
className={styles.tab} className={styles.tab}
icon={(
<FolderIcon
emoji={(tab.emoticon as string)}
customEmojiId={(tab.emoticon as ApiMessageEntityCustomEmoji)?.documentId}
shouldAnimate={tab.noTitleAnimations}
/>
)}
/> />
))} ))}
</div> </div>

View File

@ -3,7 +3,7 @@
height: 100%; height: 100%;
text-align: left; text-align: left;
&.tabs-sidebar-visible { &.folders-sidebar-visible {
grid-template-columns: auto auto 1fr; grid-template-columns: auto auto 1fr;
} }
@ -218,9 +218,9 @@
} }
@media (max-width: 925px) { @media (max-width: 925px) {
.tabs-sidebar-visible { .folders-sidebar-visible {
#LeftColumn { #LeftColumn {
left: var(--tabs-sidebar-width) !important; left: var(--folders-sidebar-width) !important;
width: 21.5rem !important; width: 21.5rem !important;
} }
} }

View File

@ -11,7 +11,7 @@ import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiChatFolder, ApiLimitTypeWithModal, ApiUser } from '../../api/types'; import type { ApiChatFolder, ApiLimitTypeWithModal, ApiUser } from '../../api/types';
import type { TabState } from '../../global/types'; import type { TabState } from '../../global/types';
import { BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER, TABS_POSITION_LEFT } from '../../config'; import { BASE_EMOJI_KEYWORD_LANG, DEBUG, FOLDERS_POSITION_LEFT, INACTIVE_MARKER } from '../../config';
import { requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { requestNextMutation } from '../../lib/fasterdom/fasterdom';
import { import {
selectAreFoldersPresent, selectAreFoldersPresent,
@ -522,7 +522,7 @@ const Main = ({
isNarrowMessageList && 'narrow-message-list', isNarrowMessageList && 'narrow-message-list',
shouldSkipHistoryAnimations && 'history-animation-disabled', shouldSkipHistoryAnimations && 'history-animation-disabled',
isFullscreen && 'is-fullscreen', isFullscreen && 'is-fullscreen',
isFoldersSidebarShown && 'tabs-sidebar-visible', isFoldersSidebarShown && 'folders-sidebar-visible',
); );
const handleBlur = useLastCallback(() => { const handleBlur = useLastCallback(() => {
@ -643,7 +643,7 @@ export default memo(withGlobal<OwnProps>(
deleteFolderDialogModal, deleteFolderDialogModal,
} = selectTabState(global); } = selectTabState(global);
const { wasTimeFormatSetManually, tabsPosition } = selectSharedSettings(global); const { wasTimeFormatSetManually, foldersPosition } = selectSharedSettings(global);
const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId); const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
const gameTitle = gameMessage?.content.game?.title; const gameTitle = gameMessage?.content.game?.title;
@ -700,7 +700,7 @@ export default memo(withGlobal<OwnProps>(
isSynced: global.isSynced, isSynced: global.isSynced,
isAccountFrozen, isAccountFrozen,
isAppConfigLoaded: global.isAppConfigLoaded, isAppConfigLoaded: global.isAppConfigLoaded,
isFoldersSidebarShown: tabsPosition === TABS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global), isFoldersSidebarShown: foldersPosition === FOLDERS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global),
}; };
}, },
)(Main)); )(Main));

View File

@ -10,7 +10,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: var(--tabs-sidebar-width); width: var(--folders-sidebar-width);
min-height: 4.5rem; min-height: 4.5rem;
padding-right: 0.25rem; padding-right: 0.25rem;
padding-left: 0.375rem; padding-left: 0.375rem;

View File

@ -1,6 +1,7 @@
import type { TeactNode } from '../../lib/teact/teact'; import type { TeactNode } from '../../lib/teact/teact';
import { useRef } from '../../lib/teact/teact'; import { useRef } from '../../lib/teact/teact';
import type { ApiMessageEntityCustomEmoji } from '../../api/types';
import type { MenuItemContextAction } from './ListItem'; import type { MenuItemContextAction } from './ListItem';
import { MouseButton } from '../../util/browser/windowEnvironment'; import { MouseButton } from '../../util/browser/windowEnvironment';
@ -8,8 +9,10 @@ import buildClassName from '../../util/buildClassName';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import { useFastClick } from '../../hooks/useFastClick'; import { useFastClick } from '../../hooks/useFastClick';
import useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import FolderIcon from '../common/FolderIcon';
import Icon from '../common/icons/Icon'; import Icon from '../common/icons/Icon';
import Menu from './Menu'; import Menu from './Menu';
import MenuItem from './MenuItem'; import MenuItem from './MenuItem';
@ -26,7 +29,7 @@ type OwnProps = {
isBadgeActive?: boolean; isBadgeActive?: boolean;
contextActions?: MenuItemContextAction[]; contextActions?: MenuItemContextAction[];
contextRootElementSelector?: string; contextRootElementSelector?: string;
icon?: TeactNode; icon?: string | ApiMessageEntityCustomEmoji;
clickArg?: number; clickArg?: number;
onClick?: (arg: number) => void; onClick?: (arg: number) => void;
}; };
@ -45,6 +48,7 @@ const Folder = ({
onClick, onClick,
}: OwnProps) => { }: OwnProps) => {
const folderRef = useRef<HTMLDivElement>(); const folderRef = useRef<HTMLDivElement>();
const [isHovering, markHovering, unmarkHovering] = useFlag();
const { const {
contextMenuAnchor, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose, contextMenuAnchor, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose,
@ -78,10 +82,16 @@ const Folder = ({
onClick={handleClick} onClick={handleClick}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onMouseEnter={markHovering}
onMouseLeave={unmarkHovering}
ref={folderRef} ref={folderRef}
> >
<div className={styles.icon}> <div className={styles.icon}>
{icon} <FolderIcon
emoji={typeof icon === 'string' ? icon : undefined}
customEmojiId={typeof icon === 'object' ? icon.documentId : undefined}
shouldAnimate={isHovering}
/>
{Boolean(badgeCount) && ( {Boolean(badgeCount) && (
<span className={buildClassName(styles.badge, isBadgeActive && styles.badgeActive)}>{badgeCount}</span> <span className={buildClassName(styles.badge, isBadgeActive && styles.badgeActive)}>{badgeCount}</span>
)} )}

View File

@ -141,9 +141,9 @@ export const DEFAULT_MESSAGE_TEXT_SIZE_PX = 16;
export const IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX = 17; export const IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX = 17;
export const MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX = 15; export const MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX = 15;
export const TABS_POSITION_TOP = 'top'; export const FOLDERS_POSITION_TOP = 'top';
export const TABS_POSITION_LEFT = 'left'; export const FOLDERS_POSITION_LEFT = 'left';
export const TABS_POSITION_DEFAULT = TABS_POSITION_TOP; export const FOLDERS_POSITION_DEFAULT = FOLDERS_POSITION_TOP;
export const PREVIEW_AVATAR_COUNT = 3; export const PREVIEW_AVATAR_COUNT = 3;

View File

@ -7,12 +7,13 @@ import type {
} from '../api/types'; } from '../api/types';
import type { MessageList, ThreadId } from '../types'; import type { MessageList, ThreadId } from '../types';
import type { ActionReturnType, GlobalState, SharedState } from './types'; import type { ActionReturnType, GlobalState, SharedState } from './types';
import { MAIN_THREAD_ID } from '../api/types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../api/types';
import { import {
ALL_FOLDER_ID, ANIMATION_LEVEL_DEFAULT, ALL_FOLDER_ID, ANIMATION_LEVEL_DEFAULT,
ARCHIVED_FOLDER_ID, ARCHIVED_FOLDER_ID,
DEBUG, DEBUG,
FOLDERS_POSITION_DEFAULT,
GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT,
GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT, GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT,
GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT,
@ -21,7 +22,6 @@ import {
IS_SCREEN_LOCKED_CACHE_KEY, IS_SCREEN_LOCKED_CACHE_KEY,
SAVED_FOLDER_ID, SAVED_FOLDER_ID,
SHARED_STATE_CACHE_KEY, SHARED_STATE_CACHE_KEY,
TABS_POSITION_DEFAULT,
} from '../config'; } from '../config';
import { MAIN_IDB_STORE } from '../util/browser/idb'; import { MAIN_IDB_STORE } from '../util/browser/idb';
import { isUserId } from '../util/entities/ids'; import { isUserId } from '../util/entities/ids';
@ -314,7 +314,7 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
cached.sharedState.settings = { cached.sharedState.settings = {
canDisplayChatInTitle: untypedCached.settings.byKey.canDisplayChatInTitle, canDisplayChatInTitle: untypedCached.settings.byKey.canDisplayChatInTitle,
animationLevel: untypedCached.settings.byKey.animationLevel, animationLevel: untypedCached.settings.byKey.animationLevel,
tabsPosition: untypedCached.settings.byKey.tabsPosition, foldersPosition: FOLDERS_POSITION_DEFAULT,
messageSendKeyCombo: untypedCached.settings.byKey.messageSendKeyCombo, messageSendKeyCombo: untypedCached.settings.byKey.messageSendKeyCombo,
messageTextSize: untypedCached.settings.byKey.messageTextSize, messageTextSize: untypedCached.settings.byKey.messageTextSize,
performance: untypedCached.settings.performance, performance: untypedCached.settings.performance,
@ -350,8 +350,8 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
cachedSharedSettings.performance = INITIAL_PERFORMANCE_STATE_MED; cachedSharedSettings.performance = INITIAL_PERFORMANCE_STATE_MED;
} }
if (!cachedSharedSettings.tabsPosition) { if (!cachedSharedSettings.foldersPosition) {
cachedSharedSettings.tabsPosition = TABS_POSITION_DEFAULT; cachedSharedSettings.foldersPosition = FOLDERS_POSITION_DEFAULT;
} }
if (!cached.appConfig) { if (!cached.appConfig) {
@ -473,7 +473,13 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
function reduceCustomEmojis<T extends GlobalState>(global: T): GlobalState['customEmojis'] { function reduceCustomEmojis<T extends GlobalState>(global: T): GlobalState['customEmojis'] {
const { lastRendered, byId } = global.customEmojis; const { lastRendered, byId } = global.customEmojis;
const idsToSave = lastRendered.slice(0, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT); const folderEmojiIds = Object.values(global.chatFolders.byId)
.flatMap((folder) => (
folder.title.entities
?.filter((entity) => entity.type === ApiMessageEntityTypes.CustomEmoji)
?.map((entity) => entity.documentId) || []
));
const idsToSave = unique([...folderEmojiIds, ...lastRendered]).slice(0, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT);
const byIdToSave = pick(byId, idsToSave); const byIdToSave = pick(byId, idsToSave);
return { return {

View File

@ -11,9 +11,9 @@ import {
DEFAULT_PLAYBACK_RATE, DEFAULT_PLAYBACK_RATE,
DEFAULT_RESALE_GIFTS_FILTER_OPTIONS, DEFAULT_RESALE_GIFTS_FILTER_OPTIONS,
DEFAULT_VOLUME, DEFAULT_VOLUME,
FOLDERS_POSITION_DEFAULT,
IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
TABS_POSITION_DEFAULT,
} from '../config'; } from '../config';
import { IS_IOS, IS_MAC_OS } from '../util/browser/windowEnvironment'; import { IS_IOS, IS_MAC_OS } from '../util/browser/windowEnvironment';
import { DEFAULT_APP_CONFIG } from '../limits'; import { DEFAULT_APP_CONFIG } from '../limits';
@ -80,7 +80,7 @@ export const INITIAL_SHARED_STATE: SharedState = {
? IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX ? IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX
: (IS_MAC_OS ? MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX : DEFAULT_MESSAGE_TEXT_SIZE_PX), : (IS_MAC_OS ? MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX : DEFAULT_MESSAGE_TEXT_SIZE_PX),
animationLevel: ANIMATION_LEVEL_DEFAULT, animationLevel: ANIMATION_LEVEL_DEFAULT,
tabsPosition: TABS_POSITION_DEFAULT, foldersPosition: FOLDERS_POSITION_DEFAULT,
messageSendKeyCombo: 'enter', messageSendKeyCombo: 'enter',
performance: INITIAL_PERFORMANCE_STATE_MAX, performance: INITIAL_PERFORMANCE_STATE_MAX,
shouldSkipWebAppCloseConfirmation: false, shouldSkipWebAppCloseConfirmation: false,

View File

@ -1,5 +1,7 @@
import type { ApiLanguage } from '../../api/types'; import type { ApiLanguage } from '../../api/types';
import type { AnimationLevel, PerformanceType, Point, Size, TabsPosition, ThemeKey, TimeFormat } from '../../types'; import type {
AnimationLevel, FoldersPosition, PerformanceType, Point, Size, ThemeKey, TimeFormat,
} from '../../types';
export interface SharedState { export interface SharedState {
settings: SharedSettings; settings: SharedSettings;
@ -14,7 +16,7 @@ export interface SharedSettings {
performance: PerformanceType; performance: PerformanceType;
messageTextSize: number; messageTextSize: number;
animationLevel: AnimationLevel; animationLevel: AnimationLevel;
tabsPosition: TabsPosition; foldersPosition: FoldersPosition;
// This can be deleted after September 2025, along with the corresponding migration // This can be deleted after September 2025, along with the corresponding migration
wasAnimationLevelSetManually?: boolean; wasAnimationLevelSetManually?: boolean;
messageSendKeyCombo: 'enter' | 'ctrl-enter'; messageSendKeyCombo: 'enter' | 'ctrl-enter';

View File

@ -177,7 +177,7 @@ const useFolderTabs = ({
title: sidebarMode ? lang('TabsPositionTop') : lang('TabsPositionLeft'), title: sidebarMode ? lang('TabsPositionTop') : lang('TabsPositionLeft'),
icon: 'forums', icon: 'forums',
handler: () => { handler: () => {
setSharedSettingOption({ tabsPosition: sidebarMode ? 'top' : 'left' }); setSharedSettingOption({ foldersPosition: sidebarMode ? 'top' : 'left' });
}, },
}); });
} }

View File

@ -221,7 +221,7 @@ $color-message-story-mention-to: #74bcff;
--border-radius-forum-avatar: 33.3333%; --border-radius-forum-avatar: 33.3333%;
--messages-container-width: 45.5rem; --messages-container-width: 45.5rem;
--right-column-width: 26.5rem; --right-column-width: 26.5rem;
--tabs-sidebar-width: 5rem; --folders-sidebar-width: 5rem;
--window-controls-width: 0rem; --window-controls-width: 0rem;
--header-height: 3.5rem; --header-height: 3.5rem;
--custom-emoji-size: 1.25rem; --custom-emoji-size: 1.25rem;

View File

@ -100,7 +100,7 @@ export type ThreadId = string | number;
export type ThemeKey = 'light' | 'dark'; export type ThemeKey = 'light' | 'dark';
export type AnimationLevel = 0 | 1 | 2; export type AnimationLevel = 0 | 1 | 2;
export type TabsPosition = 'top' | 'left'; export type FoldersPosition = 'top' | 'left';
export type PerformanceTypeKey = ( export type PerformanceTypeKey = (
'pageTransitions' | 'messageSendingAnimations' | 'mediaViewerAnimations' 'pageTransitions' | 'messageSendingAnimations' | 'mediaViewerAnimations'
| 'messageComposerAnimations' | 'contextMenuAnimations' | 'contextMenuBlur' | 'rightColumnAnimations' | 'messageComposerAnimations' | 'contextMenuAnimations' | 'contextMenuBlur' | 'rightColumnAnimations'