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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { getActions, getGlobal, withGlobal } from '../../global';
import type { TabState } from '../../global/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 { selectAreFoldersPresent, selectIsRightColumnShown, selectTabState } from '../../global/selectors';
import { selectSharedSettings } from '../../global/selectors/sharedState';
@ -179,14 +179,14 @@ export default withGlobal<OwnProps>(
(global, { isMobile }): Complete<StateProps> => {
const tabState = selectTabState(global);
const { tabsPosition } = selectSharedSettings(global);
const { foldersPosition } = selectSharedSettings(global);
return {
shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations,
uiReadyState: tabState.uiReadyState,
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
leftColumnWidth: global.leftColumnWidth,
isFoldersSidebarShown: tabsPosition === TABS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global),
isFoldersSidebarShown: foldersPosition === FOLDERS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global),
};
},
)(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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
height: 100%;
text-align: left;
&.tabs-sidebar-visible {
&.folders-sidebar-visible {
grid-template-columns: auto auto 1fr;
}
@ -218,9 +218,9 @@
}
@media (max-width: 925px) {
.tabs-sidebar-visible {
.folders-sidebar-visible {
#LeftColumn {
left: var(--tabs-sidebar-width) !important;
left: var(--folders-sidebar-width) !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 { 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 {
selectAreFoldersPresent,
@ -522,7 +522,7 @@ const Main = ({
isNarrowMessageList && 'narrow-message-list',
shouldSkipHistoryAnimations && 'history-animation-disabled',
isFullscreen && 'is-fullscreen',
isFoldersSidebarShown && 'tabs-sidebar-visible',
isFoldersSidebarShown && 'folders-sidebar-visible',
);
const handleBlur = useLastCallback(() => {
@ -643,7 +643,7 @@ export default memo(withGlobal<OwnProps>(
deleteFolderDialogModal,
} = selectTabState(global);
const { wasTimeFormatSetManually, tabsPosition } = selectSharedSettings(global);
const { wasTimeFormatSetManually, foldersPosition } = selectSharedSettings(global);
const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
const gameTitle = gameMessage?.content.game?.title;
@ -700,7 +700,7 @@ export default memo(withGlobal<OwnProps>(
isSynced: global.isSynced,
isAccountFrozen,
isAppConfigLoaded: global.isAppConfigLoaded,
isFoldersSidebarShown: tabsPosition === TABS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global),
isFoldersSidebarShown: foldersPosition === FOLDERS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global),
};
},
)(Main));

View File

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

View File

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

View File

@ -7,12 +7,13 @@ import type {
} from '../api/types';
import type { MessageList, ThreadId } 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 {
ALL_FOLDER_ID, ANIMATION_LEVEL_DEFAULT,
ARCHIVED_FOLDER_ID,
DEBUG,
FOLDERS_POSITION_DEFAULT,
GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT,
GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT,
GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT,
@ -21,7 +22,6 @@ 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';
@ -314,7 +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,
foldersPosition: FOLDERS_POSITION_DEFAULT,
messageSendKeyCombo: untypedCached.settings.byKey.messageSendKeyCombo,
messageTextSize: untypedCached.settings.byKey.messageTextSize,
performance: untypedCached.settings.performance,
@ -350,8 +350,8 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
cachedSharedSettings.performance = INITIAL_PERFORMANCE_STATE_MED;
}
if (!cachedSharedSettings.tabsPosition) {
cachedSharedSettings.tabsPosition = TABS_POSITION_DEFAULT;
if (!cachedSharedSettings.foldersPosition) {
cachedSharedSettings.foldersPosition = FOLDERS_POSITION_DEFAULT;
}
if (!cached.appConfig) {
@ -473,7 +473,13 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
function reduceCustomEmojis<T extends GlobalState>(global: T): GlobalState['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);
return {

View File

@ -11,9 +11,9 @@ import {
DEFAULT_PLAYBACK_RATE,
DEFAULT_RESALE_GIFTS_FILTER_OPTIONS,
DEFAULT_VOLUME,
FOLDERS_POSITION_DEFAULT,
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';
@ -80,7 +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,
foldersPosition: FOLDERS_POSITION_DEFAULT,
messageSendKeyCombo: 'enter',
performance: INITIAL_PERFORMANCE_STATE_MAX,
shouldSkipWebAppCloseConfirmation: false,

View File

@ -1,5 +1,7 @@
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 {
settings: SharedSettings;
@ -14,7 +16,7 @@ export interface SharedSettings {
performance: PerformanceType;
messageTextSize: number;
animationLevel: AnimationLevel;
tabsPosition: TabsPosition;
foldersPosition: FoldersPosition;
// This can be deleted after September 2025, along with the corresponding migration
wasAnimationLevelSetManually?: boolean;
messageSendKeyCombo: 'enter' | 'ctrl-enter';

View File

@ -177,7 +177,7 @@ const useFolderTabs = ({
title: sidebarMode ? lang('TabsPositionTop') : lang('TabsPositionLeft'),
icon: 'forums',
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%;
--messages-container-width: 45.5rem;
--right-column-width: 26.5rem;
--tabs-sidebar-width: 5rem;
--folders-sidebar-width: 5rem;
--window-controls-width: 0rem;
--header-height: 3.5rem;
--custom-emoji-size: 1.25rem;

View File

@ -100,7 +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 FoldersPosition = 'top' | 'left';
export type PerformanceTypeKey = (
'pageTransitions' | 'messageSendingAnimations' | 'mediaViewerAnimations'
| 'messageComposerAnimations' | 'contextMenuAnimations' | 'contextMenuBlur' | 'rightColumnAnimations'