Chat List: Introduce Archive item (#2566)
This commit is contained in:
parent
3cc27156cb
commit
5055f70f77
Binary file not shown.
Binary file not shown.
@ -235,7 +235,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
|
|||||||
<ListItem
|
<ListItem
|
||||||
key={id}
|
key={id}
|
||||||
className="chat-item-clickable force-rounded-corners small-icon"
|
className="chat-item-clickable force-rounded-corners small-icon"
|
||||||
style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`}
|
style={`height: ${CHAT_HEIGHT_PX}px; top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
clickArg={id}
|
clickArg={id}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -28,4 +28,8 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.archived-chats-more-menu {
|
||||||
|
margin-left: auto !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import React, { memo } from '../../lib/teact/teact';
|
import React, { memo, useCallback } from '../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { LeftColumnContent, SettingsScreens } from '../../types';
|
||||||
|
import type { FolderEditDispatch } from '../../hooks/reducers/useFoldersReducer';
|
||||||
|
import type { GlobalState } from '../../global/types';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
@ -12,22 +16,33 @@ import useForumPanelRender from '../../hooks/useForumPanelRender';
|
|||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import ChatList from './main/ChatList';
|
import ChatList from './main/ChatList';
|
||||||
import ForumPanel from './main/ForumPanel';
|
import ForumPanel from './main/ForumPanel';
|
||||||
|
import DropdownMenu from '../ui/DropdownMenu';
|
||||||
|
import MenuItem from '../ui/MenuItem';
|
||||||
|
|
||||||
import './ArchivedChats.scss';
|
import './ArchivedChats.scss';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
isForumPanelOpen?: boolean;
|
||||||
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
onTopicSearch: NoneToVoidFunction;
|
onTopicSearch: NoneToVoidFunction;
|
||||||
isForumPanelOpen?: boolean;
|
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
||||||
|
foldersDispatch: FolderEditDispatch;
|
||||||
|
onLeftColumnContentChange: (content: LeftColumnContent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArchivedChats: FC<OwnProps> = ({
|
const ArchivedChats: FC<OwnProps> = ({
|
||||||
isActive,
|
isActive,
|
||||||
isForumPanelOpen,
|
isForumPanelOpen,
|
||||||
|
archiveSettings,
|
||||||
onReset,
|
onReset,
|
||||||
onTopicSearch,
|
onTopicSearch,
|
||||||
|
onSettingsScreenSelect,
|
||||||
|
onLeftColumnContentChange,
|
||||||
|
foldersDispatch,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { updateArchiveSettings } = getActions();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
@ -35,6 +50,10 @@ const ArchivedChats: FC<OwnProps> = ({
|
|||||||
onBack: onReset,
|
onBack: onReset,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleDisplayArchiveInChats = useCallback(() => {
|
||||||
|
updateArchiveSettings({ isHidden: false });
|
||||||
|
}, [updateArchiveSettings]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldDisableDropdownMenuTransitionRef,
|
shouldDisableDropdownMenuTransitionRef,
|
||||||
handleDropdownMenuTransitionEnd,
|
handleDropdownMenuTransitionEnd,
|
||||||
@ -67,8 +86,27 @@ const ArchivedChats: FC<OwnProps> = ({
|
|||||||
<i className="icon-arrow-left" />
|
<i className="icon-arrow-left" />
|
||||||
</Button>
|
</Button>
|
||||||
{shouldRenderTitle && <h3 className={titleClassNames}>{lang('ArchivedChats')}</h3>}
|
{shouldRenderTitle && <h3 className={titleClassNames}>{lang('ArchivedChats')}</h3>}
|
||||||
|
{archiveSettings.isHidden && (
|
||||||
|
<DropdownMenu
|
||||||
|
className="archived-chats-more-menu"
|
||||||
|
positionX="right"
|
||||||
|
onTransitionEnd={lang.isRtl ? handleDropdownMenuTransitionEnd : undefined}
|
||||||
|
>
|
||||||
|
<MenuItem icon="archive-from-main" onClick={handleDisplayArchiveInChats}>
|
||||||
|
{lang('lng_context_archive_to_list')}
|
||||||
|
</MenuItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ChatList folderType="archived" isActive={isActive} isForumPanelOpen={isForumPanelOpen} />
|
<ChatList
|
||||||
|
folderType="archived"
|
||||||
|
isActive={isActive}
|
||||||
|
isForumPanelOpen={isForumPanelOpen}
|
||||||
|
onSettingsScreenSelect={onSettingsScreenSelect}
|
||||||
|
onLeftColumnContentChange={onLeftColumnContentChange}
|
||||||
|
foldersDispatch={foldersDispatch}
|
||||||
|
archiveSettings={archiveSettings}
|
||||||
|
/>
|
||||||
{shouldRenderForumPanel && (
|
{shouldRenderForumPanel && (
|
||||||
<ForumPanel
|
<ForumPanel
|
||||||
isOpen={isForumPanelOpen}
|
isOpen={isForumPanelOpen}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useRef, useState,
|
memo, useCallback, useEffect, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { GlobalState } from '../../global/types';
|
||||||
import { LeftColumnContent, SettingsScreens } from '../../types';
|
import { LeftColumnContent, SettingsScreens } from '../../types';
|
||||||
|
|
||||||
import { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/environment';
|
import { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/environment';
|
||||||
@ -36,6 +37,7 @@ type StateProps = {
|
|||||||
isForumPanelOpen?: boolean;
|
isForumPanelOpen?: boolean;
|
||||||
forumPanelChatId?: string;
|
forumPanelChatId?: string;
|
||||||
isClosingSearch?: boolean;
|
isClosingSearch?: boolean;
|
||||||
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ContentType {
|
enum ContentType {
|
||||||
@ -66,6 +68,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
isForumPanelOpen,
|
isForumPanelOpen,
|
||||||
forumPanelChatId,
|
forumPanelChatId,
|
||||||
isClosingSearch,
|
isClosingSearch,
|
||||||
|
archiveSettings,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setGlobalSearchQuery,
|
setGlobalSearchQuery,
|
||||||
@ -352,6 +355,11 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
openChat({ id: currentUserId, shouldReplaceHistory: true });
|
openChat({ id: currentUserId, shouldReplaceHistory: true });
|
||||||
}, [currentUserId, openChat]);
|
}, [currentUserId, openChat]);
|
||||||
|
|
||||||
|
const handleArchivedChats = useCallback((e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setContent(LeftColumnContent.Archived);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleHotkeySettings = useCallback((e: KeyboardEvent) => {
|
const handleHotkeySettings = useCallback((e: KeyboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContent(LeftColumnContent.Settings);
|
setContent(LeftColumnContent.Settings);
|
||||||
@ -360,7 +368,10 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
useHotkeys({
|
useHotkeys({
|
||||||
'Mod+Shift+F': handleHotkeySearch,
|
'Mod+Shift+F': handleHotkeySearch,
|
||||||
'Mod+Shift+S': handleHotkeySavedMessages,
|
'Mod+Shift+S': handleHotkeySavedMessages,
|
||||||
'Mod+0': handleHotkeySavedMessages,
|
...(IS_PWA && {
|
||||||
|
'Mod+0': handleHotkeySavedMessages,
|
||||||
|
'Mod+9': handleArchivedChats,
|
||||||
|
}),
|
||||||
...(IS_MAC_OS && IS_PWA && { 'Mod+,': handleHotkeySettings }),
|
...(IS_MAC_OS && IS_PWA && { 'Mod+,': handleHotkeySettings }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -411,7 +422,11 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
onTopicSearch={handleTopicSearch}
|
onTopicSearch={handleTopicSearch}
|
||||||
|
foldersDispatch={foldersDispatch}
|
||||||
|
onSettingsScreenSelect={handleSettingsScreenSelect}
|
||||||
|
onLeftColumnContentChange={setContent}
|
||||||
isForumPanelOpen={isForumPanelOpen}
|
isForumPanelOpen={isForumPanelOpen}
|
||||||
|
archiveSettings={archiveSettings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case ContentType.Settings:
|
case ContentType.Settings:
|
||||||
@ -458,7 +473,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
foldersDispatch={foldersDispatch}
|
foldersDispatch={foldersDispatch}
|
||||||
onContentChange={setContent}
|
onContentChange={setContent}
|
||||||
onSearchQuery={handleSearchQuery}
|
onSearchQuery={handleSearchQuery}
|
||||||
onScreenSelect={handleSettingsScreenSelect}
|
onSettingsScreenSelect={handleSettingsScreenSelect}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
shouldSkipTransition={shouldSkipHistoryAnimations}
|
shouldSkipTransition={shouldSkipHistoryAnimations}
|
||||||
isUpdateAvailable={isUpdateAvailable}
|
isUpdateAvailable={isUpdateAvailable}
|
||||||
@ -498,6 +513,7 @@ export default memo(withGlobal(
|
|||||||
hasPasscode,
|
hasPasscode,
|
||||||
},
|
},
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
|
archiveSettings,
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
const currentChat = selectCurrentChat(global);
|
const currentChat = selectCurrentChat(global);
|
||||||
@ -519,6 +535,7 @@ export default memo(withGlobal(
|
|||||||
isForumPanelOpen,
|
isForumPanelOpen,
|
||||||
forumPanelChatId,
|
forumPanelChatId,
|
||||||
isClosingSearch: tabState.globalSearch.isClosing,
|
isClosingSearch: tabState.globalSearch.isClosing,
|
||||||
|
archiveSettings,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(LeftColumn));
|
)(LeftColumn));
|
||||||
|
|||||||
90
src/components/left/main/Archive.module.scss
Normal file
90
src/components/left/main/Archive.module.scss
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
.root {
|
||||||
|
--background-color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimized {
|
||||||
|
background-color: var(--color-background-secondary);
|
||||||
|
margin: -0.5rem -0.5rem 0 -0.5rem !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0.375rem !important;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.is-ios) &,
|
||||||
|
:global(body.is-android) & {
|
||||||
|
.button {
|
||||||
|
padding-bottom: 0.3125rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
justify-content: center !important;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-count {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
line-height: 1.25rem !important;
|
||||||
|
font-size: 0.8125rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
transition: opacity 0.3s ease, transform var(--layer-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-inline-end: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarWrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
font-size: 1.625rem;
|
||||||
|
background: linear-gradient(var(--color-white) -125%, var(--color-archive)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatsPreview {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
166
src/components/left/main/Archive.tsx
Normal file
166
src/components/left/main/Archive.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||||
|
import { getActions, getGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { GlobalState } from '../../../global/types';
|
||||||
|
|
||||||
|
import { ARCHIVED_FOLDER_ID } from '../../../config';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { compact } from '../../../util/iteratees';
|
||||||
|
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import { useFolderManagerForOrderedIds, useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
|
||||||
|
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
|
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||||
|
|
||||||
|
import styles from './Archive.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
|
onDragEnter?: NoneToVoidFunction;
|
||||||
|
onClick?: NoneToVoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PREVIEW_SLICE = 5;
|
||||||
|
|
||||||
|
const Archive: FC<OwnProps> = ({
|
||||||
|
archiveSettings,
|
||||||
|
onDragEnter,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const { updateArchiveSettings } = getActions();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const orderedChatIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID);
|
||||||
|
const unreadCounters = useFolderManagerForUnreadCounters();
|
||||||
|
const archiveUnreadCount = unreadCounters[ARCHIVED_FOLDER_ID]?.chatsCount;
|
||||||
|
|
||||||
|
const previewItems = useMemo(() => {
|
||||||
|
if (!orderedChatIds?.length) return lang('Loading');
|
||||||
|
|
||||||
|
const chatsById = getGlobal().chats.byId;
|
||||||
|
|
||||||
|
return orderedChatIds.slice(0, PREVIEW_SLICE).map((chatId, i, arr) => {
|
||||||
|
const isLast = i === arr.length - 1;
|
||||||
|
const chat = chatsById[chatId];
|
||||||
|
if (!chat) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className={buildClassName(styles.chat, archiveUnreadCount && chat.unreadCount && styles.unread)}>
|
||||||
|
{renderText(chat.title)}
|
||||||
|
</span>
|
||||||
|
{isLast ? '' : ', '}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [orderedChatIds, lang, archiveUnreadCount]);
|
||||||
|
|
||||||
|
const contextActions = useMemo(() => {
|
||||||
|
const actionMinimize = !archiveSettings.isMinimized && {
|
||||||
|
title: lang('lng_context_archive_collapse'),
|
||||||
|
icon: 'collapse',
|
||||||
|
handler: () => {
|
||||||
|
updateArchiveSettings({ isMinimized: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionExpand = archiveSettings.isMinimized && {
|
||||||
|
title: lang('lng_context_archive_expand'),
|
||||||
|
icon: 'expand',
|
||||||
|
handler: () => {
|
||||||
|
updateArchiveSettings({ isMinimized: false });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionHide = {
|
||||||
|
title: lang('lng_context_archive_to_menu'),
|
||||||
|
icon: 'archive-to-main',
|
||||||
|
handler: () => {
|
||||||
|
updateArchiveSettings({ isHidden: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return compact([actionMinimize, actionExpand, actionHide]);
|
||||||
|
}, [archiveSettings.isMinimized, lang, updateArchiveSettings]);
|
||||||
|
|
||||||
|
const handleDragEnter = useCallback((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onDragEnter?.();
|
||||||
|
}, [onDragEnter]);
|
||||||
|
|
||||||
|
function renderCollapsed() {
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.info, 'info')}>
|
||||||
|
<div className="info-row">
|
||||||
|
<div className={buildClassName('title', styles.title)}>
|
||||||
|
<h3 dir="auto" className={buildClassName(styles.name, 'fullName')}>
|
||||||
|
<i className={buildClassName(styles.icon, 'icon-archive-filled')} />
|
||||||
|
{lang('ArchivedChats')}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{Boolean(archiveUnreadCount) && (
|
||||||
|
<div className={styles.unreadCount}>
|
||||||
|
<AnimatedCounter text={formatIntegerCompact(archiveUnreadCount)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRegular() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={buildClassName('status', styles.avatarWrapper)}>
|
||||||
|
<div className={buildClassName('Avatar', styles.avatar)}>
|
||||||
|
<i className="icon-archive-filled" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={buildClassName(styles.info, 'info')}>
|
||||||
|
<div className="info-row">
|
||||||
|
<div className={buildClassName('title', styles.title)}>
|
||||||
|
<h3 dir="auto" className={buildClassName(styles.name, 'fullName')}>{lang('ArchivedChats')}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="subtitle">
|
||||||
|
<div className={buildClassName('status', styles.chatsPreview)}>
|
||||||
|
{previewItems}
|
||||||
|
</div>
|
||||||
|
{Boolean(archiveUnreadCount) && (
|
||||||
|
<div className="Badge">
|
||||||
|
<AnimatedCounter text={formatIntegerCompact(archiveUnreadCount)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
onClick={onClick}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
className={buildClassName(
|
||||||
|
styles.root,
|
||||||
|
archiveSettings.isMinimized && styles.minimized,
|
||||||
|
'chat-item-clickable',
|
||||||
|
'chat-item-archive',
|
||||||
|
)}
|
||||||
|
buttonClassName={styles.button}
|
||||||
|
offsetCollapseDelta={0}
|
||||||
|
contextActions={contextActions}
|
||||||
|
withPortalForMenu
|
||||||
|
>
|
||||||
|
{archiveSettings.isMinimized ? renderCollapsed() : renderRegular()}
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Archive);
|
||||||
@ -5,8 +5,9 @@ import React, {
|
|||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiChatFolder } from '../../../api/types';
|
import type { ApiChatFolder } from '../../../api/types';
|
||||||
import type { SettingsScreens } from '../../../types';
|
import type { LeftColumnContent, SettingsScreens } from '../../../types';
|
||||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||||
|
import type { GlobalState } from '../../../global/types';
|
||||||
|
|
||||||
import { ALL_FOLDER_ID } from '../../../config';
|
import { ALL_FOLDER_ID } from '../../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
@ -25,8 +26,9 @@ import TabList from '../../ui/TabList';
|
|||||||
import ChatList from './ChatList';
|
import ChatList from './ChatList';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
onScreenSelect: (screen: SettingsScreens) => void;
|
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
||||||
foldersDispatch: FolderEditDispatch;
|
foldersDispatch: FolderEditDispatch;
|
||||||
|
onLeftColumnContentChange: (content: LeftColumnContent) => void;
|
||||||
shouldHideFolderTabs?: boolean;
|
shouldHideFolderTabs?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,6 +41,8 @@ type StateProps = {
|
|||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
maxFolders: number;
|
maxFolders: number;
|
||||||
|
hasArchivedChats?: boolean;
|
||||||
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const SAVED_MESSAGES_HOTKEY = '0';
|
const SAVED_MESSAGES_HOTKEY = '0';
|
||||||
@ -46,7 +50,8 @@ const FIRST_FOLDER_INDEX = 0;
|
|||||||
|
|
||||||
const ChatFolders: FC<OwnProps & StateProps> = ({
|
const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||||
foldersDispatch,
|
foldersDispatch,
|
||||||
onScreenSelect,
|
onSettingsScreenSelect,
|
||||||
|
onLeftColumnContentChange,
|
||||||
chatFoldersById,
|
chatFoldersById,
|
||||||
orderedFolderIds,
|
orderedFolderIds,
|
||||||
activeChatFolder,
|
activeChatFolder,
|
||||||
@ -56,6 +61,8 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
maxFolders,
|
maxFolders,
|
||||||
shouldHideFolderTabs,
|
shouldHideFolderTabs,
|
||||||
|
hasArchivedChats,
|
||||||
|
archiveSettings,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadChatFolders,
|
loadChatFolders,
|
||||||
@ -122,7 +129,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
// Prevent `activeTab` pointing at non-existing folder after update
|
// Prevent `activeTab` pointing at non-existing folder after update
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!folderTabs || !folderTabs.length) {
|
if (!folderTabs?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +218,10 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
isForumPanelOpen={isForumPanelOpen}
|
isForumPanelOpen={isForumPanelOpen}
|
||||||
lastSyncTime={lastSyncTime}
|
lastSyncTime={lastSyncTime}
|
||||||
foldersDispatch={foldersDispatch}
|
foldersDispatch={foldersDispatch}
|
||||||
onScreenSelect={onScreenSelect}
|
onSettingsScreenSelect={onSettingsScreenSelect}
|
||||||
|
onLeftColumnContentChange={onLeftColumnContentChange}
|
||||||
|
canDisplayArchive={hasArchivedChats && !archiveSettings.isHidden}
|
||||||
|
archiveSettings={archiveSettings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -249,8 +259,14 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
byId: chatFoldersById,
|
byId: chatFoldersById,
|
||||||
orderedIds: orderedFolderIds,
|
orderedIds: orderedFolderIds,
|
||||||
},
|
},
|
||||||
|
chats: {
|
||||||
|
listIds: {
|
||||||
|
archived,
|
||||||
|
},
|
||||||
|
},
|
||||||
currentUserId,
|
currentUserId,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
archiveSettings,
|
||||||
} = global;
|
} = global;
|
||||||
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
|
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
|
||||||
|
|
||||||
@ -262,7 +278,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isForumPanelOpen: selectIsForumPanelOpen(global),
|
isForumPanelOpen: selectIsForumPanelOpen(global),
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
|
hasArchivedChats: Boolean(archived?.length),
|
||||||
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
|
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
|
||||||
|
archiveSettings,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ChatFolders));
|
)(ChatFolders));
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import React, {
|
|||||||
import { getActions, getGlobal } from '../../../global';
|
import { getActions, getGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { SettingsScreens } from '../../../types';
|
|
||||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||||
|
import { LeftColumnContent } from '../../../types';
|
||||||
|
import type { SettingsScreens } from '../../../types';
|
||||||
|
import type { GlobalState } from '../../../global/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ALL_FOLDER_ID,
|
ALL_FOLDER_ID,
|
||||||
ARCHIVED_FOLDER_ID, CHAT_HEIGHT_FORUM_PX,
|
ARCHIVED_FOLDER_ID, ARCHIVE_MINIMIZED_HEIGHT, CHAT_HEIGHT_FORUM_PX,
|
||||||
CHAT_HEIGHT_PX,
|
CHAT_HEIGHT_PX,
|
||||||
CHAT_LIST_SLICE,
|
CHAT_LIST_SLICE,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
@ -24,35 +26,43 @@ import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'
|
|||||||
import { useHotkeys } from '../../../hooks/useHotkeys';
|
import { useHotkeys } from '../../../hooks/useHotkeys';
|
||||||
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
|
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
|
||||||
import useChatOrderDiff from './hooks/useChatOrderDiff';
|
import useChatOrderDiff from './hooks/useChatOrderDiff';
|
||||||
|
import useCollapseWithForumPanel from './hooks/useCollapseWithForumPanel';
|
||||||
|
|
||||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import Chat from './Chat';
|
import Chat from './Chat';
|
||||||
import EmptyFolder from './EmptyFolder';
|
import EmptyFolder from './EmptyFolder';
|
||||||
import useCollapseWithForumPanel from './hooks/useCollapseWithForumPanel';
|
import Archive from './Archive';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
folderType: 'all' | 'archived' | 'folder';
|
folderType: 'all' | 'archived' | 'folder';
|
||||||
folderId?: number;
|
folderId?: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
canDisplayArchive?: boolean;
|
||||||
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
isForumPanelOpen?: boolean;
|
isForumPanelOpen?: boolean;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
foldersDispatch?: FolderEditDispatch;
|
foldersDispatch: FolderEditDispatch;
|
||||||
onScreenSelect?: (screen: SettingsScreens) => void;
|
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
||||||
|
onLeftColumnContentChange: (content: LeftColumnContent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INTERSECTION_THROTTLE = 200;
|
const INTERSECTION_THROTTLE = 200;
|
||||||
const DRAG_ENTER_DEBOUNCE = 500;
|
const DRAG_ENTER_DEBOUNCE = 500;
|
||||||
|
const RESERVED_HOTKEYS = new Set(['9', '0']);
|
||||||
|
|
||||||
const ChatList: FC<OwnProps> = ({
|
const ChatList: FC<OwnProps> = ({
|
||||||
folderType,
|
folderType,
|
||||||
folderId,
|
folderId,
|
||||||
isActive,
|
isActive,
|
||||||
isForumPanelOpen,
|
isForumPanelOpen,
|
||||||
|
canDisplayArchive,
|
||||||
|
archiveSettings,
|
||||||
foldersDispatch,
|
foldersDispatch,
|
||||||
onScreenSelect,
|
onSettingsScreenSelect,
|
||||||
|
onLeftColumnContentChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { openChat, openNextChat } = getActions();
|
const { openChat, openNextChat, closeForumPanel } = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const shouldIgnoreDragRef = useRef(false);
|
const shouldIgnoreDragRef = useRef(false);
|
||||||
@ -61,8 +71,14 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
folderType === 'all' ? ALL_FOLDER_ID : folderType === 'archived' ? ARCHIVED_FOLDER_ID : folderId!
|
folderType === 'all' ? ALL_FOLDER_ID : folderType === 'archived' ? ARCHIVED_FOLDER_ID : folderId!
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shouldDisplayArchive = folderType === 'all' && canDisplayArchive;
|
||||||
|
|
||||||
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
|
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
|
||||||
|
|
||||||
|
const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX;
|
||||||
|
const archiveHeight = shouldDisplayArchive
|
||||||
|
? archiveSettings.isMinimized ? ARCHIVE_MINIMIZED_HEIGHT : CHAT_HEIGHT_PX : 0;
|
||||||
|
|
||||||
const { orderDiffById, getAnimationType } = useChatOrderDiff(orderedIds);
|
const { orderDiffById, getAnimationType } = useChatOrderDiff(orderedIds);
|
||||||
|
|
||||||
const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE);
|
const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE);
|
||||||
@ -88,10 +104,19 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
if (((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.code.startsWith('Digit')) {
|
if (((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.code.startsWith('Digit')) {
|
||||||
const [, digit] = e.code.match(/Digit(\d)/) || [];
|
const [, digit] = e.code.match(/Digit(\d)/) || [];
|
||||||
if (!digit) return;
|
if (!digit || RESERVED_HOTKEYS.has(digit)) return;
|
||||||
|
|
||||||
const position = Number(digit) - 1;
|
const isArchiveInList = shouldDisplayArchive && !archiveSettings.isMinimized;
|
||||||
if (position > orderedIds!.length - 1 || position < 0) return;
|
|
||||||
|
const shift = isArchiveInList ? -1 : 0;
|
||||||
|
const position = Number(digit) + shift - 1;
|
||||||
|
|
||||||
|
if (isArchiveInList && position === -1) {
|
||||||
|
onLeftColumnContentChange(LeftColumnContent.Archived);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position > orderedIds!.length - 1) return;
|
||||||
|
|
||||||
openChat({ id: orderedIds![position], shouldReplaceHistory: true });
|
openChat({ id: orderedIds![position], shouldReplaceHistory: true });
|
||||||
}
|
}
|
||||||
@ -102,7 +127,7 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [isActive, openChat, openNextChat, orderedIds]);
|
}, [archiveSettings, isActive, onLeftColumnContentChange, openChat, openNextChat, orderedIds, shouldDisplayArchive]);
|
||||||
|
|
||||||
const { observe } = useIntersectionObserver({
|
const { observe } = useIntersectionObserver({
|
||||||
rootRef: containerRef,
|
rootRef: containerRef,
|
||||||
@ -111,6 +136,19 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
|
|
||||||
useCollapseWithForumPanel(containerRef, isForumPanelOpen);
|
useCollapseWithForumPanel(containerRef, isForumPanelOpen);
|
||||||
|
|
||||||
|
const handleArchivedClick = useCallback(() => {
|
||||||
|
onLeftColumnContentChange(LeftColumnContent.Archived);
|
||||||
|
closeForumPanel();
|
||||||
|
}, [closeForumPanel, onLeftColumnContentChange]);
|
||||||
|
|
||||||
|
const handleArchivedDragEnter = useCallback(() => {
|
||||||
|
if (shouldIgnoreDragRef.current) {
|
||||||
|
shouldIgnoreDragRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleArchivedClick();
|
||||||
|
}, [handleArchivedClick]);
|
||||||
|
|
||||||
const handleDragEnter = useDebouncedCallback((chatId: string) => {
|
const handleDragEnter = useDebouncedCallback((chatId: string) => {
|
||||||
if (shouldIgnoreDragRef.current) {
|
if (shouldIgnoreDragRef.current) {
|
||||||
shouldIgnoreDragRef.current = false;
|
shouldIgnoreDragRef.current = false;
|
||||||
@ -131,13 +169,13 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
if (!viewportIds?.length) return 0;
|
if (!viewportIds?.length) return 0;
|
||||||
const global = getGlobal();
|
const global = getGlobal();
|
||||||
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
||||||
return orderedIds!.reduce((acc, id, i) => {
|
return archiveHeight + orderedIds!.reduce((acc, id, i) => {
|
||||||
if (i >= viewportOffset) {
|
if (i >= viewportOffset) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
return acc + (selectChat(global, id)!.isForum ? CHAT_HEIGHT_FORUM_PX : CHAT_HEIGHT_PX);
|
return acc + (selectChat(global, id)!.isForum ? CHAT_HEIGHT_FORUM_PX : CHAT_HEIGHT_PX);
|
||||||
}, 0);
|
}, 0);
|
||||||
}, [orderedIds, viewportIds]);
|
}, [archiveHeight, orderedIds, viewportIds]);
|
||||||
|
|
||||||
function renderChats() {
|
function renderChats() {
|
||||||
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
||||||
@ -150,7 +188,7 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
return viewportIds!.map((id, i) => {
|
return viewportIds!.map((id, i) => {
|
||||||
const isPinned = viewportOffset + i < pinnedCount;
|
const isPinned = viewportOffset + i < pinnedCount;
|
||||||
const expendedOffsetTop = currentChatListHeight;
|
const expendedOffsetTop = currentChatListHeight;
|
||||||
const collapsedOffsetTop = (viewportOffset + i) * CHAT_HEIGHT_PX;
|
const collapsedOffsetTop = archiveHeight + (viewportOffset + i) * CHAT_HEIGHT_PX;
|
||||||
|
|
||||||
currentChatListHeight += (selectChat(global, id)?.isForum ? CHAT_HEIGHT_FORUM_PX : CHAT_HEIGHT_PX);
|
currentChatListHeight += (selectChat(global, id)?.isForum ? CHAT_HEIGHT_FORUM_PX : CHAT_HEIGHT_PX);
|
||||||
|
|
||||||
@ -177,12 +215,21 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open')}
|
className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open')}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
items={viewportIds}
|
items={viewportIds}
|
||||||
|
itemSelector=".ListItem:not(.chat-item-archive)"
|
||||||
preloadBackwards={CHAT_LIST_SLICE}
|
preloadBackwards={CHAT_LIST_SLICE}
|
||||||
withAbsolutePositioning
|
withAbsolutePositioning
|
||||||
maxHeight={(orderedIds?.length || 0) * CHAT_HEIGHT_PX}
|
maxHeight={chatsHeight + archiveHeight}
|
||||||
onLoadMore={getMore}
|
onLoadMore={getMore}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
>
|
>
|
||||||
|
{shouldDisplayArchive && (
|
||||||
|
<Archive
|
||||||
|
key="archive"
|
||||||
|
archiveSettings={archiveSettings}
|
||||||
|
onClick={handleArchivedClick}
|
||||||
|
onDragEnter={handleArchivedDragEnter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{viewportIds?.length ? (
|
{viewportIds?.length ? (
|
||||||
renderChats()
|
renderChats()
|
||||||
) : viewportIds && !viewportIds.length ? (
|
) : viewportIds && !viewportIds.length ? (
|
||||||
@ -191,7 +238,7 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
folderId={folderId}
|
folderId={folderId}
|
||||||
folderType={folderType}
|
folderType={folderType}
|
||||||
foldersDispatch={foldersDispatch}
|
foldersDispatch={foldersDispatch}
|
||||||
onScreenSelect={onScreenSelect}
|
onSettingsScreenSelect={onSettingsScreenSelect}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -18,8 +18,8 @@ import styles from './EmptyFolder.module.scss';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
folderId?: number;
|
folderId?: number;
|
||||||
folderType: 'all' | 'archived' | 'folder';
|
folderType: 'all' | 'archived' | 'folder';
|
||||||
foldersDispatch?: FolderEditDispatch;
|
foldersDispatch: FolderEditDispatch;
|
||||||
onScreenSelect?: (screen: SettingsScreens) => void;
|
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -30,15 +30,15 @@ type StateProps = {
|
|||||||
const ICON_SIZE = 96;
|
const ICON_SIZE = 96;
|
||||||
|
|
||||||
const EmptyFolder: FC<OwnProps & StateProps> = ({
|
const EmptyFolder: FC<OwnProps & StateProps> = ({
|
||||||
chatFolder, animatedEmoji, foldersDispatch, onScreenSelect,
|
chatFolder, animatedEmoji, foldersDispatch, onSettingsScreenSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const { isMobile } = useAppLayout();
|
const { isMobile } = useAppLayout();
|
||||||
|
|
||||||
const handleEditFolder = useCallback(() => {
|
const handleEditFolder = useCallback(() => {
|
||||||
foldersDispatch!({ type: 'editFolder', payload: chatFolder });
|
foldersDispatch({ type: 'editFolder', payload: chatFolder });
|
||||||
onScreenSelect!(SettingsScreens.FoldersEditFolderFromChatList);
|
onSettingsScreenSelect(SettingsScreens.FoldersEditFolderFromChatList);
|
||||||
}, [chatFolder, foldersDispatch, onScreenSelect]);
|
}, [chatFolder, foldersDispatch, onSettingsScreenSelect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
@ -49,7 +49,7 @@ const EmptyFolder: FC<OwnProps & StateProps> = ({
|
|||||||
<p className={styles.description} dir="auto">
|
<p className={styles.description} dir="auto">
|
||||||
{lang(chatFolder ? 'ChatList.EmptyChatListFilterText' : 'Chat.EmptyChat')}
|
{lang(chatFolder ? 'ChatList.EmptyChatListFilterText' : 'Chat.EmptyChat')}
|
||||||
</p>
|
</p>
|
||||||
{chatFolder && foldersDispatch && onScreenSelect && (
|
{chatFolder && (
|
||||||
<Button
|
<Button
|
||||||
ripple={!isMobile}
|
ripple={!isMobile}
|
||||||
fluid
|
fluid
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import Button from '../../ui/Button';
|
|||||||
import ForumPanel from './ForumPanel';
|
import ForumPanel from './ForumPanel';
|
||||||
|
|
||||||
import './LeftMain.scss';
|
import './LeftMain.scss';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
content: LeftColumnContent;
|
content: LeftColumnContent;
|
||||||
@ -36,7 +37,7 @@ type OwnProps = {
|
|||||||
isClosingSearch?: boolean;
|
isClosingSearch?: boolean;
|
||||||
onSearchQuery: (query: string) => void;
|
onSearchQuery: (query: string) => void;
|
||||||
onContentChange: (content: LeftColumnContent) => void;
|
onContentChange: (content: LeftColumnContent) => void;
|
||||||
onScreenSelect: (screen: SettingsScreens) => void;
|
onSettingsScreenSelect: (screen: SettingsScreens) => void;
|
||||||
onTopicSearch: NoneToVoidFunction;
|
onTopicSearch: NoneToVoidFunction;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
};
|
};
|
||||||
@ -58,10 +59,11 @@ const LeftMain: FC<OwnProps> = ({
|
|||||||
isForumPanelOpen,
|
isForumPanelOpen,
|
||||||
onSearchQuery,
|
onSearchQuery,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
onScreenSelect,
|
onSettingsScreenSelect,
|
||||||
onReset,
|
onReset,
|
||||||
onTopicSearch,
|
onTopicSearch,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { closeForumPanel } = getActions();
|
||||||
const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV);
|
const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV);
|
||||||
|
|
||||||
const { shouldRenderForumPanel, handleForumPanelAnimationEnd } = useForumPanelRender(isForumPanelOpen);
|
const { shouldRenderForumPanel, handleForumPanelAnimationEnd } = useForumPanelRender(isForumPanelOpen);
|
||||||
@ -107,7 +109,8 @@ const LeftMain: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const handleSelectArchived = useCallback(() => {
|
const handleSelectArchived = useCallback(() => {
|
||||||
onContentChange(LeftColumnContent.Archived);
|
onContentChange(LeftColumnContent.Archived);
|
||||||
}, [onContentChange]);
|
closeForumPanel();
|
||||||
|
}, [closeForumPanel, onContentChange]);
|
||||||
|
|
||||||
const handleUpdateClick = useCallback(() => {
|
const handleUpdateClick = useCallback(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@ -172,7 +175,8 @@ const LeftMain: FC<OwnProps> = ({
|
|||||||
return (
|
return (
|
||||||
<ChatFolders
|
<ChatFolders
|
||||||
shouldHideFolderTabs={isForumPanelVisible}
|
shouldHideFolderTabs={isForumPanelVisible}
|
||||||
onScreenSelect={onScreenSelect}
|
onSettingsScreenSelect={onSettingsScreenSelect}
|
||||||
|
onLeftColumnContentChange={onContentChange}
|
||||||
foldersDispatch={foldersDispatch}
|
foldersDispatch={foldersDispatch}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -78,7 +78,7 @@ type StateProps =
|
|||||||
areChatsLoaded?: boolean;
|
areChatsLoaded?: boolean;
|
||||||
hasPasscode?: boolean;
|
hasPasscode?: boolean;
|
||||||
}
|
}
|
||||||
& Pick<GlobalState, 'connectionState' | 'isSyncing'> & Pick<TabState, 'canInstall'>;
|
& Pick<GlobalState, 'connectionState' | 'isSyncing' | 'archiveSettings'> & Pick<TabState, 'canInstall'>;
|
||||||
|
|
||||||
const ANIMATION_LEVEL_OPTIONS = [0, 1, 2];
|
const ANIMATION_LEVEL_OPTIONS = [0, 1, 2];
|
||||||
const WEBK_VERSION_URL = 'https://web.telegram.org/k/';
|
const WEBK_VERSION_URL = 'https://web.telegram.org/k/';
|
||||||
@ -110,6 +110,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
areChatsLoaded,
|
areChatsLoaded,
|
||||||
hasPasscode,
|
hasPasscode,
|
||||||
canInstall,
|
canInstall,
|
||||||
|
archiveSettings,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
@ -276,15 +277,17 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
{lang('SavedMessages')}
|
{lang('SavedMessages')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
{archiveSettings.isHidden && (
|
||||||
icon="archive"
|
<MenuItem
|
||||||
onClick={onSelectArchived}
|
icon="archive"
|
||||||
>
|
onClick={onSelectArchived}
|
||||||
<span className="menu-item-name">{lang('ArchivedChats')}</span>
|
>
|
||||||
{archivedUnreadChatsCount > 0 && (
|
<span className="menu-item-name">{lang('ArchivedChats')}</span>
|
||||||
<div className="right-badge">{archivedUnreadChatsCount}</div>
|
{archivedUnreadChatsCount > 0 && (
|
||||||
)}
|
<div className="right-badge">{archivedUnreadChatsCount}</div>
|
||||||
</MenuItem>
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="user"
|
icon="user"
|
||||||
onClick={onSelectContacts}
|
onClick={onSelectContacts}
|
||||||
@ -359,9 +362,9 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
), [
|
), [
|
||||||
animationLevel, archivedUnreadChatsCount, canInstall, handleAnimationLevelChange, handleBugReportClick,
|
animationLevel, archiveSettings.isHidden, archivedUnreadChatsCount, canInstall, handleAnimationLevelChange,
|
||||||
handleChangelogClick, handleDarkModeToggle, handleOpenTipsChat, handleSelectSaved, handleSwitchToWebK, lang,
|
handleBugReportClick, handleChangelogClick, handleDarkModeToggle, handleOpenTipsChat, handleSelectSaved,
|
||||||
onSelectArchived, onSelectContacts, onSelectSettings, theme, withOtherVersions,
|
handleSwitchToWebK, lang, onSelectArchived, onSelectContacts, onSelectSettings, theme, withOtherVersions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -457,7 +460,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const {
|
const {
|
||||||
query: searchQuery, fetchingStatus, chatId, date,
|
query: searchQuery, fetchingStatus, chatId, date,
|
||||||
} = tabState.globalSearch;
|
} = tabState.globalSearch;
|
||||||
const { currentUserId, connectionState, isSyncing } = global;
|
const {
|
||||||
|
currentUserId, connectionState, isSyncing, archiveSettings,
|
||||||
|
} = global;
|
||||||
const { byId: chatsById } = global.chats;
|
const { byId: chatsById } = global.chats;
|
||||||
const { isConnectionStatusMinimized, animationLevel } = global.settings.byKey;
|
const { isConnectionStatusMinimized, animationLevel } = global.settings.byKey;
|
||||||
|
|
||||||
@ -478,6 +483,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
areChatsLoaded: Boolean(global.chats.listIds.active),
|
areChatsLoaded: Boolean(global.chats.listIds.active),
|
||||||
hasPasscode: Boolean(global.passcode.hasPasscode),
|
hasPasscode: Boolean(global.passcode.hasPasscode),
|
||||||
canInstall: Boolean(tabState.canInstall),
|
canInstall: Boolean(tabState.canInstall),
|
||||||
|
archiveSettings,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(LeftMainHeader));
|
)(LeftMainHeader));
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { getActions } from '../../../../global';
|
|
||||||
import { useMemo } from '../../../../lib/teact/teact';
|
import { useMemo } from '../../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../../global';
|
||||||
|
|
||||||
import type { ApiChat, ApiTopic } from '../../../../api/types';
|
import type { ApiChat, ApiTopic } from '../../../../api/types';
|
||||||
import type { MenuItemContextAction } from '../../../ui/ListItem';
|
import type { MenuItemContextAction } from '../../../ui/ListItem';
|
||||||
|
|||||||
@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
.info {
|
.info {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
@ -128,10 +128,11 @@
|
|||||||
|
|
||||||
&.commonChats-list,
|
&.commonChats-list,
|
||||||
&.members-list {
|
&.members-list {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
.ListItem.chat-item-clickable {
|
.ListItem.chat-item-clickable {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
|
import React, {
|
||||||
|
useState, useRef, useCallback, useMemo,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, { useState, useRef, useCallback } from '../../lib/teact/teact';
|
|
||||||
|
|
||||||
import Menu from './Menu';
|
import Menu from './Menu';
|
||||||
|
import Button from './Button';
|
||||||
|
|
||||||
import './DropdownMenu.scss';
|
import './DropdownMenu.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
trigger: FC<{ onTrigger: () => void; isOpen?: boolean }>;
|
trigger?: FC<{ onTrigger: () => void; isOpen?: boolean }>;
|
||||||
positionX?: 'left' | 'right';
|
positionX?: 'left' | 'right';
|
||||||
positionY?: 'top' | 'bottom';
|
positionY?: 'top' | 'bottom';
|
||||||
footer?: string;
|
footer?: string;
|
||||||
@ -69,6 +73,23 @@ const DropdownMenu: FC<OwnProps> = ({
|
|||||||
onClose?.();
|
onClose?.();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
|
const triggerComponent: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||||
|
if (trigger) return trigger;
|
||||||
|
|
||||||
|
return ({ onTrigger, isOpen: isMenuOpen }) => (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
className={isMenuOpen ? 'active' : ''}
|
||||||
|
onClick={onTrigger}
|
||||||
|
ariaLabel="More actions"
|
||||||
|
>
|
||||||
|
<i className="icon-more" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}, [trigger]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
@ -76,7 +97,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onTransitionEnd={onTransitionEnd}
|
onTransitionEnd={onTransitionEnd}
|
||||||
>
|
>
|
||||||
{trigger({ onTrigger: toggleIsOpen, isOpen })}
|
{triggerComponent({ onTrigger: toggleIsOpen, isOpen })}
|
||||||
|
|
||||||
<Menu
|
<Menu
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
.ListItem-button {
|
.ListItem-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|||||||
@ -61,8 +61,9 @@ export const MIN_PASSWORD_LENGTH = 1;
|
|||||||
export const MESSAGE_LIST_SLICE = isBigScreen ? 60 : 40;
|
export const MESSAGE_LIST_SLICE = isBigScreen ? 60 : 40;
|
||||||
export const MESSAGE_LIST_VIEWPORT_LIMIT = MESSAGE_LIST_SLICE * 2;
|
export const MESSAGE_LIST_VIEWPORT_LIMIT = MESSAGE_LIST_SLICE * 2;
|
||||||
|
|
||||||
|
export const ARCHIVE_MINIMIZED_HEIGHT = 36;
|
||||||
export const CHAT_HEIGHT_PX = 72;
|
export const CHAT_HEIGHT_PX = 72;
|
||||||
export const CHAT_HEIGHT_FORUM_PX = 96;
|
export const CHAT_HEIGHT_FORUM_PX = 94;
|
||||||
export const TOPIC_HEIGHT_PX = 65;
|
export const TOPIC_HEIGHT_PX = 65;
|
||||||
export const CHAT_LIST_SLICE = isBigScreen ? 30 : 25;
|
export const CHAT_LIST_SLICE = isBigScreen ? 30 : 25;
|
||||||
export const CHAT_LIST_LOAD_SLICE = 100;
|
export const CHAT_LIST_LOAD_SLICE = 100;
|
||||||
|
|||||||
@ -561,6 +561,19 @@ addActionHandler('closeEditTopicPanel', (global, actions, payload): ActionReturn
|
|||||||
}, tabId);
|
}, tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('updateArchiveSettings', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { archiveSettings } = global;
|
||||||
|
const { isHidden = archiveSettings.isHidden, isMinimized = archiveSettings.isMinimized } = payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
archiveSettings: {
|
||||||
|
isHidden,
|
||||||
|
isMinimized,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('checkAppVersion', (global): ActionReturnType => {
|
addActionHandler('checkAppVersion', (global): ActionReturnType => {
|
||||||
const APP_VERSION_REGEX = /^\d+\.\d+(\.\d+)?$/;
|
const APP_VERSION_REGEX = /^\d+\.\d+(\.\d+)?$/;
|
||||||
|
|
||||||
|
|||||||
@ -195,10 +195,6 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
cached.serviceNotifications = [];
|
cached.serviceNotifications = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cached.groupCalls) {
|
|
||||||
cached.groupCalls = initialState.groupCalls;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cached.users.statusesById) {
|
if (!cached.users.statusesById) {
|
||||||
cached.users.statusesById = {};
|
cached.users.statusesById = {};
|
||||||
}
|
}
|
||||||
@ -336,10 +332,6 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
cached.availableReactions = cached.availableReactions
|
cached.availableReactions = cached.availableReactions
|
||||||
.map((r) => ({ ...r, reaction: { emoticon: r.reaction as unknown as string } }));
|
.map((r) => ({ ...r, reaction: { emoticon: r.reaction as unknown as string } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cached.attachmentSettings) {
|
|
||||||
cached.attachmentSettings = initialState.attachmentSettings;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCache() {
|
function updateCache() {
|
||||||
@ -390,6 +382,7 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
|
|||||||
'attachmentSettings',
|
'attachmentSettings',
|
||||||
'leftColumnWidth',
|
'leftColumnWidth',
|
||||||
'lastIsChatInfoShown',
|
'lastIsChatInfoShown',
|
||||||
|
'archiveSettings',
|
||||||
'mediaViewer',
|
'mediaViewer',
|
||||||
'audioPlayer',
|
'audioPlayer',
|
||||||
]),
|
]),
|
||||||
|
|||||||
@ -186,6 +186,11 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
transcriptions: {},
|
transcriptions: {},
|
||||||
|
|
||||||
byTabId: {},
|
byTabId: {},
|
||||||
|
|
||||||
|
archiveSettings: {
|
||||||
|
isMinimized: false,
|
||||||
|
isHidden: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const INITIAL_TAB_STATE: TabState = {
|
export const INITIAL_TAB_STATE: TabState = {
|
||||||
|
|||||||
@ -781,6 +781,11 @@ export type GlobalState = {
|
|||||||
serviceNotifications: ServiceNotification[];
|
serviceNotifications: ServiceNotification[];
|
||||||
|
|
||||||
byTabId: Record<number, TabState>;
|
byTabId: Record<number, TabState>;
|
||||||
|
|
||||||
|
archiveSettings: {
|
||||||
|
isMinimized: boolean;
|
||||||
|
isHidden: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CallSound = (
|
export type CallSound = (
|
||||||
@ -2088,6 +2093,11 @@ export interface ActionPayloads {
|
|||||||
shouldSendGrouped?: boolean;
|
shouldSendGrouped?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateArchiveSettings: {
|
||||||
|
isMinimized?: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
openUrl: {
|
openUrl: {
|
||||||
url: string;
|
url: string;
|
||||||
shouldSkipModal?: boolean;
|
shouldSkipModal?: boolean;
|
||||||
|
|||||||
@ -332,6 +332,7 @@ export function typify<
|
|||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
(window as any).getGlobal = getGlobal;
|
(window as any).getGlobal = getGlobal;
|
||||||
|
(window as any).setGlobal = setGlobal;
|
||||||
|
|
||||||
document.addEventListener('dblclick', () => {
|
document.addEventListener('dblclick', () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -81,24 +81,22 @@
|
|||||||
.chat-list {
|
.chat-list {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0.5rem 0.125rem 0.5rem 0.4375rem;
|
padding: 0.5rem 0.4375rem 0.5rem 0.4375rem;
|
||||||
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
body.is-android & {
|
@include overflow-y-overlay();
|
||||||
@include overflow-y-overlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
&.forum-panel-open {
|
&.forum-panel-open {
|
||||||
.info {
|
.info {
|
||||||
transform: translateX(-25%);
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
transform: translateX(-25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Chat[dir="rtl"] .info {
|
.Chat[dir="rtl"] .info {
|
||||||
transform: translateX(25%);
|
transform: translateX(25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ListItem-button {
|
.group .ListItem-button {
|
||||||
height: 4.5rem;
|
height: 4.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,6 +157,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.deleted-account {
|
&.deleted-account {
|
||||||
--color-user: #9eaab5;
|
--color-user: var(--color-deleted-account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -180,6 +180,9 @@ $color-message-reaction-own-hover: #b5e0a4;
|
|||||||
--color-forum-hover-unread-topic: #e9e9e9;
|
--color-forum-hover-unread-topic: #e9e9e9;
|
||||||
--color-forum-hover-unread-topic-hover: #dcdcdc;
|
--color-forum-hover-unread-topic-hover: #dcdcdc;
|
||||||
|
|
||||||
|
--color-deleted-account: #9eaab5;
|
||||||
|
--color-archive: #9eaab5;
|
||||||
|
|
||||||
--vh: 1vh;
|
--vh: 1vh;
|
||||||
|
|
||||||
--border-radius-default: 0.75rem;
|
--border-radius-default: 0.75rem;
|
||||||
|
|||||||
@ -51,6 +51,21 @@
|
|||||||
.icon-volume-3:before {
|
.icon-volume-3:before {
|
||||||
content: "\e991";
|
content: "\e991";
|
||||||
}
|
}
|
||||||
|
.icon-archive-filled:before {
|
||||||
|
content: "\e9ba";
|
||||||
|
}
|
||||||
|
.icon-archive-from-main:before {
|
||||||
|
content: "\e9bb";
|
||||||
|
}
|
||||||
|
.icon-archive-to-main:before {
|
||||||
|
content: "\e9bc";
|
||||||
|
}
|
||||||
|
.icon-collapse:before {
|
||||||
|
content: "\e9bd";
|
||||||
|
}
|
||||||
|
.icon-expand:before {
|
||||||
|
content: "\e9be";
|
||||||
|
}
|
||||||
.icon-replies:before {
|
.icon-replies:before {
|
||||||
content: "\e9b9";
|
content: "\e9b9";
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user