Forum Panel: Various optimizations

This commit is contained in:
Alexander Zinchuk 2023-01-07 19:09:03 +01:00
parent d65f1d99fc
commit 556c4c0b21
17 changed files with 203 additions and 154 deletions

View File

@ -68,7 +68,7 @@ const ArchivedChats: FC<OwnProps> = ({
</Button> </Button>
{shouldRenderTitle && <h3 className={titleClassNames}>{lang('ArchivedChats')}</h3>} {shouldRenderTitle && <h3 className={titleClassNames}>{lang('ArchivedChats')}</h3>}
</div> </div>
<ChatList folderType="archived" isActive={isActive} /> <ChatList folderType="archived" isActive={isActive} isForumPanelOpen={isForumPanelOpen} />
{shouldRenderForumPanel && ( {shouldRenderForumPanel && (
<ForumPanel <ForumPanel
isOpen={isForumPanelOpen} isOpen={isForumPanelOpen}

View File

@ -0,0 +1,52 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global';
import type { ApiChat } from '../../../api/types';
import { selectIsChatMuted } from '../../../global/helpers';
import {
selectChat,
selectNotifySettings,
selectNotifyExceptions,
selectIsForumPanelOpen,
} from '../../../global/selectors';
import Badge from './Badge';
type OwnProps = {
chatId: string;
};
type StateProps = {
chat?: ApiChat;
isMuted?: boolean;
isForumPanelActive?: boolean;
};
const AvatarBadge: FC<OwnProps & StateProps> = ({
chat,
isMuted,
isForumPanelActive,
}) => {
return chat && (
<div className="avatar-badge-wrapper">
<Badge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={!isForumPanelActive} />
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const chat = selectChat(global, chatId);
if (!chat) {
return {};
}
return {
chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
isForumPanelActive: selectIsForumPanelOpen(global),
};
},
)(AvatarBadge));

View File

@ -15,12 +15,17 @@
&.animate-opacity { &.animate-opacity {
will-change: opacity; will-change: opacity;
transition: opacity 250ms ease; transition: opacity 0.2s ease-out;
} }
&.animate-transform { &.animate-transform {
will-change: transform; will-change: transform;
transition: transform 250ms ease; transition: transform 0.2s ease-out;
}
&.animate-collapse {
will-change: transform;
transition: transform var(--layer-transition);
} }
&:hover, &:hover,
@ -29,7 +34,7 @@
border-color: var(--color-chat-hover); border-color: var(--color-chat-hover);
} }
.status-badge-wrapper { .avatar-badge-wrapper {
--outline-color: var(--color-chat-hover); --outline-color: var(--color-chat-hover);
} }
@ -38,6 +43,16 @@
} }
} }
// Super specific selector to override the same in `ListItem`
@media (min-width: 600px) {
&:not(.has-ripple):not(.is-static),
body.animation-level-0 & {
.ListItem-button:active {
--background-color: var(--color-chat-hover) !important;
}
}
}
&:last-of-type { &:last-of-type {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
@ -55,17 +70,17 @@
} }
} }
&.active-forum { &.selected-forum {
.status-badge-wrapper { .avatar-badge-wrapper {
--outline-color: var(--color-chat-hover); --outline-color: var(--color-chat-hover);
} }
} }
} }
@media (min-width: 600px) { @media (min-width: 600px) {
&.active-forum.forum, &.selected-forum.forum,
&.active-forum.forum:hover { &.selected-forum.forum:hover {
.status-badge-wrapper { .avatar-badge-wrapper {
--outline-color: var(--color-chat-hover); --outline-color: var(--color-chat-hover);
} }
} }
@ -111,7 +126,6 @@
} }
.Badge:not(.pinned) { .Badge:not(.pinned) {
background: var(--color-white);
color: var(--color-chat-active); color: var(--color-chat-active);
} }
@ -120,35 +134,18 @@
background: #FFFFFF33; background: #FFFFFF33;
} }
.status-badge-wrapper-visible .Badge:not(.pinned).muted { .avatar-badge-wrapper .Badge:not(.pinned) {
background: var(--color-chat-active-greyed);
--outline-color: transparent; --outline-color: transparent;
} }
.status-badge-wrapper-visible .Badge:not(.pinned):not(.muted) { .avatar-badge-wrapper .Badge:not(.pinned).muted {
--outline-color: transparent; background: var(--color-gray);
} }
} }
} }
&.smaller .ListItem-button { &.selected-forum {
height: 4.5rem; --background-color: var(--color-chat-hover) !important;
}
&.active-forum::before {
content: '';
position: absolute;
top: 50%;
left: -0.5rem;
width: 0.375rem;
height: 75%;
transform: translateY(-50%);
background: var(--color-primary);
z-index: 1;
border-start-end-radius: var(--border-radius-default);
border-end-end-radius: var(--border-radius-default);
} }
@media (max-width: 600px) { @media (max-width: 600px) {
@ -169,7 +166,7 @@
background: var(--background-color); background: var(--background-color);
} }
.status-badge-wrapper { .avatar-badge-wrapper {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 0.5rem; right: 0.5rem;
@ -182,12 +179,12 @@
} }
.Badge-transition { .Badge-transition {
transition: opacity 250ms ease, transform 250ms ease; // Same as Forum Panel transition: opacity var(--layer-transition), transform var(--layer-transition);
} }
} }
.info { .info {
transition: opacity 250ms ease, transform 250ms ease; // Same as Forum Panel transition: opacity 300ms ease, transform var(--layer-transition);
.subtitle { .subtitle {
margin-top: -0.125rem; margin-top: -0.125rem;
@ -316,19 +313,5 @@
unicode-bidi: plaintext; unicode-bidi: plaintext;
} }
} }
&.active-forum::before {
left: auto;
right: 0.0625rem;
}
}
&.smaller .info {
transform: translateX(-25%);
opacity: 0;
}
&[dir="rtl"].smaller .info {
transform: translateX(25%);
} }
} }

View File

@ -1,7 +1,5 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact';
memo, useCallback, useEffect, useLayoutEffect, useRef,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
@ -18,7 +16,6 @@ import type {
import type { AnimationLevel } from '../../../types'; import type { AnimationLevel } from '../../../types';
import type { ChatAnimationTypes } from './hooks'; import type { ChatAnimationTypes } from './hooks';
import { ANIMATION_END_DELAY } from '../../../config';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { import {
@ -40,40 +37,35 @@ import {
selectIsDefaultEmojiStatusPack, selectIsDefaultEmojiStatusPack,
selectTopicFromMessage, selectTopicFromMessage,
selectThreadParam, selectThreadParam,
selectIsForumPanelOpen,
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { fastRaf } from '../../../util/schedulers';
import buildStyle from '../../../util/buildStyle';
import useChatContextActions from '../../../hooks/useChatContextActions'; import useChatContextActions from '../../../hooks/useChatContextActions';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useChatListEntry from './hooks/useChatListEntry'; import useChatListEntry from './hooks/useChatListEntry';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import usePrevious from '../../../hooks/usePrevious';
import ListItem from '../../ui/ListItem';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
import LastMessageMeta from '../../common/LastMessageMeta'; import LastMessageMeta from '../../common/LastMessageMeta';
import DeleteChatModal from '../../common/DeleteChatModal'; import DeleteChatModal from '../../common/DeleteChatModal';
import ListItem from '../../ui/ListItem';
import Badge from './Badge';
import ChatFolderModal from '../ChatFolderModal.async';
import ChatCallStatus from './ChatCallStatus';
import ReportModal from '../../common/ReportModal'; import ReportModal from '../../common/ReportModal';
import FullNameTitle from '../../common/FullNameTitle'; import FullNameTitle from '../../common/FullNameTitle';
import ChatFolderModal from '../ChatFolderModal.async';
import ChatCallStatus from './ChatCallStatus';
import Badge from './Badge';
import AvatarBadge from './AvatarBadge';
import './Chat.scss'; import './Chat.scss';
const TRANSFORM_TO_TOPIC_LIST_ANIMATION_DELAY = 300;
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
folderId?: number; folderId?: number;
orderDiff: number; orderDiff: number;
animationType: ChatAnimationTypes; animationType: ChatAnimationTypes;
isPinned?: boolean; isPinned?: boolean;
offsetTopInSmallerMode: number;
offsetTop: number; offsetTop: number;
offsetCollapseDelta: number;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
onDragEnter?: (chatId: string) => void; onDragEnter?: (chatId: string) => void;
}; };
@ -92,13 +84,12 @@ type StateProps = {
draft?: ApiFormattedText; draft?: ApiFormattedText;
animationLevel?: AnimationLevel; animationLevel?: AnimationLevel;
isSelected?: boolean; isSelected?: boolean;
isForumPanelActive?: boolean; isSelectedForum?: boolean;
canScrollDown?: boolean; canScrollDown?: boolean;
canChangeFolder?: boolean; canChangeFolder?: boolean;
lastSyncTime?: number; lastSyncTime?: number;
lastMessageTopic?: ApiTopic; lastMessageTopic?: ApiTopic;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
forumPanelChatId?: string;
}; };
const Chat: FC<OwnProps & StateProps> = ({ const Chat: FC<OwnProps & StateProps> = ({
@ -118,24 +109,22 @@ const Chat: FC<OwnProps & StateProps> = ({
lastMessageOutgoingStatus, lastMessageOutgoingStatus,
actionTargetMessage, actionTargetMessage,
actionTargetChatId, actionTargetChatId,
offsetTopInSmallerMode,
offsetTop, offsetTop,
offsetCollapseDelta,
draft, draft,
animationLevel, animationLevel,
isSelected, isSelected,
isForumPanelActive, isSelectedForum,
canScrollDown, canScrollDown,
canChangeFolder, canChangeFolder,
lastSyncTime, lastSyncTime,
lastMessageTopic, lastMessageTopic,
typingStatus, typingStatus,
forumPanelChatId,
onDragEnter, onDragEnter,
}) => { }) => {
const { const {
openChat, openChat,
openForumPanel, openForumPanel,
closeForumPanel,
focusLastMessage, focusLastMessage,
loadTopics, loadTopics,
} = getActions(); } = getActions();
@ -161,27 +150,24 @@ const Chat: FC<OwnProps & StateProps> = ({
lastMessageTopic, lastMessageTopic,
lastMessageSender, lastMessageSender,
observeIntersection, observeIntersection,
animationType, animationType,
animationLevel, animationLevel,
orderDiff, orderDiff,
}); });
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (chat?.isForum) { if (isForum) {
openForumPanel({ chatId }); openForumPanel({ chatId });
return; return;
} }
if (forumPanelChatId) closeForumPanel();
openChat({ id: chatId, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true }); openChat({ id: chatId, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true });
if (isSelected && canScrollDown) { if (isSelected && canScrollDown) {
focusLastMessage(); focusLastMessage();
} }
}, [ }, [
chat?.isForum, forumPanelChatId, closeForumPanel, openChat, chatId, isSelected, canScrollDown, openForumPanel, isForum, openChat, chatId, isSelected, canScrollDown, openForumPanel, focusLastMessage,
focusLastMessage,
]); ]);
const handleDragEnter = useCallback((e) => { const handleDragEnter = useCallback((e) => {
@ -225,31 +211,6 @@ const Chat: FC<OwnProps & StateProps> = ({
} }
}, [chat, chatId, isForum, isIntersecting, lastSyncTime, loadTopics]); }, [chat, chatId, isForum, isIntersecting, lastSyncTime, loadTopics]);
const isOnForumPanel = chatId === forumPanelChatId;
const prevIsForumPanelActive = usePrevious(isForumPanelActive);
const isAnimatingRef = useRef(false);
if (prevIsForumPanelActive !== isForumPanelActive) {
isAnimatingRef.current = true;
}
// Animate changing to smaller chat size when navigating to/from forum topic list
useLayoutEffect(() => {
const current = ref.current;
if (current && isAnimatingRef.current && isForumPanelActive !== prevIsForumPanelActive) {
current.classList.add('animate-transform');
current.style.transform = '';
setTimeout(() => {
// Wait one more frame for better animation performance
fastRaf(() => {
isAnimatingRef.current = false;
current.classList.remove('animate-transform');
});
}, TRANSFORM_TO_TOPIC_LIST_ANIMATION_DELAY + ANIMATION_END_DELAY);
}
}, [ref, isForumPanelActive, prevIsForumPanelActive]);
if (!chat) { if (!chat) {
return undefined; return undefined;
} }
@ -259,23 +220,20 @@ const Chat: FC<OwnProps & StateProps> = ({
isUserId(chatId) ? 'private' : 'group', isUserId(chatId) ? 'private' : 'group',
isForum && 'forum', isForum && 'forum',
isSelected && 'selected', isSelected && 'selected',
isForumPanelActive && 'smaller', isSelectedForum && 'selected-forum',
isOnForumPanel && 'active-forum',
); );
const chatTop = isForumPanelActive ? (offsetTop - offsetTopInSmallerMode) : offsetTop;
const offsetAnimate = isForumPanelActive ? offsetTopInSmallerMode : -offsetTopInSmallerMode;
return ( return (
<ListItem <ListItem
ref={ref} ref={ref}
className={className} className={className}
style={buildStyle(`top: ${chatTop}px`, isAnimatingRef.current && `transform: translateY(${offsetAnimate}px)`)} style={`top: ${offsetTop}px`}
ripple={!isForum && !IS_SINGLE_COLUMN_LAYOUT} ripple={!isForum && !IS_SINGLE_COLUMN_LAYOUT}
contextActions={contextActions} contextActions={contextActions}
onClick={handleClick} onClick={handleClick}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
shouldUsePortalForMenu={isForumPanelActive} offsetCollapseDelta={offsetCollapseDelta}
withPortalForMenu
> >
<div className="status"> <div className="status">
<Avatar <Avatar
@ -288,9 +246,7 @@ const Chat: FC<OwnProps & StateProps> = ({
withVideo withVideo
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
/> />
<div className="status-badge-wrapper"> <AvatarBadge chatId={chatId} />
<Badge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={!isForumPanelActive} />
</div>
{chat.isCallActive && chat.isCallNotEmpty && ( {chat.isCallActive && chat.isCallNotEmpty && (
<ChatCallStatus isSelected={isSelected} isActive={animationLevel !== 0} /> <ChatCallStatus isSelected={isSelected} isActive={animationLevel !== 0} />
)} )}
@ -368,8 +324,8 @@ export default memo(withGlobal<OwnProps>(
threadId: currentThreadId, threadId: currentThreadId,
type: messageListType, type: messageListType,
} = selectCurrentMessageList(global) || {}; } = selectCurrentMessageList(global) || {};
const isForumPanelActive = selectIsForumPanelOpen(global);
const isSelected = chatId === currentChatId && currentThreadId === MAIN_THREAD_ID; const isSelected = chatId === currentChatId && currentThreadId === MAIN_THREAD_ID;
const isSelectedForum = chatId === global.forumPanelChatId;
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined; const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined; const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined;
@ -388,8 +344,8 @@ export default memo(withGlobal<OwnProps>(
actionTargetMessage, actionTargetMessage,
draft: selectDraft(global, chatId, MAIN_THREAD_ID), draft: selectDraft(global, chatId, MAIN_THREAD_ID),
animationLevel: global.settings.byKey.animationLevel, animationLevel: global.settings.byKey.animationLevel,
isForumPanelActive,
isSelected, isSelected,
isSelectedForum,
canScrollDown: isSelected && messageListType === 'thread', canScrollDown: isSelected && messageListType === 'thread',
canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1, canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1,
lastSyncTime: global.lastSyncTime, lastSyncTime: global.lastSyncTime,
@ -401,7 +357,6 @@ export default memo(withGlobal<OwnProps>(
isEmojiStatusColored, isEmojiStatusColored,
lastMessageTopic, lastMessageTopic,
typingStatus, typingStatus,
forumPanelChatId: global.forumPanelChatId,
}; };
}, },
)(Chat)); )(Chat));

View File

@ -22,6 +22,7 @@ import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManag
import Transition from '../../ui/Transition'; import Transition from '../../ui/Transition';
import TabList from '../../ui/TabList'; import TabList from '../../ui/TabList';
import ChatList from './ChatList'; import ChatList from './ChatList';
import { selectIsForumPanelOpen } from '../../../global/selectors';
type OwnProps = { type OwnProps = {
onScreenSelect: (screen: SettingsScreens) => void; onScreenSelect: (screen: SettingsScreens) => void;
@ -34,6 +35,7 @@ type StateProps = {
orderedFolderIds?: number[]; orderedFolderIds?: number[];
activeChatFolder: number; activeChatFolder: number;
currentUserId?: string; currentUserId?: string;
isForumPanelOpen?: boolean;
lastSyncTime?: number; lastSyncTime?: number;
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
maxFolders: number; maxFolders: number;
@ -49,6 +51,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
orderedFolderIds, orderedFolderIds,
activeChatFolder, activeChatFolder,
currentUserId, currentUserId,
isForumPanelOpen,
lastSyncTime, lastSyncTime,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
maxFolders, maxFolders,
@ -195,27 +198,17 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
function renderCurrentTab(isActive: boolean) { function renderCurrentTab(isActive: boolean) {
const activeFolder = Object.values(chatFoldersById) const activeFolder = Object.values(chatFoldersById)
.find(({ id }) => id === folderTabs![activeChatFolder].id); .find(({ id }) => id === folderTabs![activeChatFolder].id);
const isFolder = activeFolder && !isInAllChatsFolder;
if (!activeFolder || isInAllChatsFolder) {
return (
<ChatList
folderType="all"
isActive={isActive}
lastSyncTime={lastSyncTime}
foldersDispatch={foldersDispatch}
onScreenSelect={onScreenSelect}
/>
);
}
return ( return (
<ChatList <ChatList
folderType="folder" folderType={isFolder ? 'folder' : 'all'}
folderId={activeFolder.id} folderId={isFolder ? activeFolder.id : undefined}
isActive={isActive} isActive={isActive}
isForumPanelOpen={isForumPanelOpen}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
onScreenSelect={onScreenSelect}
foldersDispatch={foldersDispatch} foldersDispatch={foldersDispatch}
onScreenSelect={onScreenSelect}
/> />
); );
} }
@ -259,16 +252,15 @@ export default memo(withGlobal<OwnProps>(
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
} = global; } = global;
const maxFolders = selectCurrentLimit(global, 'dialogFilters');
return { return {
chatFoldersById, chatFoldersById,
orderedFolderIds, orderedFolderIds,
activeChatFolder, activeChatFolder,
currentUserId, currentUserId,
isForumPanelOpen: selectIsForumPanelOpen(global),
lastSyncTime, lastSyncTime,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
maxFolders, maxFolders: selectCurrentLimit(global, 'dialogFilters'),
}; };
}, },
)(ChatFolders)); )(ChatFolders));

View File

@ -16,6 +16,7 @@ import {
import { IS_MAC_OS, IS_PWA } from '../../../util/environment'; import { IS_MAC_OS, IS_PWA } from '../../../util/environment';
import { getPinnedChatsCount, getOrderKey } from '../../../util/folderManager'; import { getPinnedChatsCount, getOrderKey } from '../../../util/folderManager';
import { selectChat } from '../../../global/selectors'; import { selectChat } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager'; import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
@ -28,11 +29,13 @@ 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';
type OwnProps = { type OwnProps = {
folderType: 'all' | 'archived' | 'folder'; folderType: 'all' | 'archived' | 'folder';
folderId?: number; folderId?: number;
isActive: boolean; isActive: boolean;
isForumPanelOpen?: boolean;
lastSyncTime?: number; lastSyncTime?: number;
foldersDispatch?: FolderEditDispatch; foldersDispatch?: FolderEditDispatch;
onScreenSelect?: (screen: SettingsScreens) => void; onScreenSelect?: (screen: SettingsScreens) => void;
@ -45,6 +48,7 @@ const ChatList: FC<OwnProps> = ({
folderType, folderType,
folderId, folderId,
isActive, isActive,
isForumPanelOpen,
foldersDispatch, foldersDispatch,
onScreenSelect, onScreenSelect,
}) => { }) => {
@ -105,6 +109,8 @@ const ChatList: FC<OwnProps> = ({
throttleMs: INTERSECTION_THROTTLE, throttleMs: INTERSECTION_THROTTLE,
}); });
useCollapseWithForumPanel(containerRef, isForumPanelOpen);
const handleDragEnter = useDebouncedCallback((chatId: string) => { const handleDragEnter = useDebouncedCallback((chatId: string) => {
if (shouldIgnoreDragRef.current) { if (shouldIgnoreDragRef.current) {
shouldIgnoreDragRef.current = false; shouldIgnoreDragRef.current = false;
@ -143,8 +149,9 @@ 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 chatTop = currentChatListHeight; const expendedOffsetTop = currentChatListHeight;
const chatTopSmaller = (viewportOffset + i) * CHAT_HEIGHT_PX; const collapsedOffsetTop = (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);
return ( return (
@ -156,8 +163,8 @@ const ChatList: FC<OwnProps> = ({
folderId={folderId} folderId={folderId}
animationType={getAnimationType(id)} animationType={getAnimationType(id)}
orderDiff={orderDiffById[id]} orderDiff={orderDiffById[id]}
offsetTop={chatTop} offsetTop={isForumPanelOpen ? collapsedOffsetTop : expendedOffsetTop}
offsetTopInSmallerMode={chatTop - chatTopSmaller} offsetCollapseDelta={expendedOffsetTop - collapsedOffsetTop}
observeIntersection={observe} observeIntersection={observe}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
/> />
@ -167,7 +174,7 @@ const ChatList: FC<OwnProps> = ({
return ( return (
<InfiniteScroll <InfiniteScroll
className="chat-list custom-scroll" className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open')}
ref={containerRef} ref={containerRef}
items={viewportIds} items={viewportIds}
preloadBackwards={CHAT_LIST_SLICE} preloadBackwards={CHAT_LIST_SLICE}

View File

@ -10,6 +10,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@media (max-width: 600px) {
left: 4.3125rem;
}
&.rtl { &.rtl {
left: 0; left: 0;
right: 4.75rem; right: 4.75rem;
@ -18,7 +22,7 @@
border-right: 1px solid var(--color-borders); border-right: 1px solid var(--color-borders);
} }
transition: transform 250ms ease; // Same as `.Chat > .info` transition: transform var(--layer-transition);
transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0);
:global(.chat-list) { :global(.chat-list) {

View File

@ -35,7 +35,7 @@
z-index: 1; z-index: 1;
opacity: 1; opacity: 1;
transition: opacity 250ms ease; // Same as Forum Panel transition: opacity var(--layer-transition);
} }
&--tabs-hidden .TabList { &--tabs-hidden .TabList {

View File

@ -102,7 +102,7 @@
} }
.SearchInput { .SearchInput {
transition: opacity 250ms ease; // Same as Forum Panel transition: opacity var(--layer-transition);
&--hidden { &--hidden {
opacity: 0; opacity: 0;

View File

@ -0,0 +1,36 @@
import { useLayoutEffect } from '../../../../lib/teact/teact';
import { fastRaf } from '../../../../util/schedulers';
import { ANIMATION_END_DELAY } from '../../../../config';
const ANIMATION_DURATION = 450;
// Reduce height of forum chat items when opening Forum Panel
export default function useCollapseWithForumPanel(
containerRef: React.RefObject<HTMLDivElement>,
isForumPanelOpen = false,
) {
useLayoutEffect(() => {
const chatEls = Array.from(containerRef.current!.querySelectorAll<HTMLDivElement>('.Chat'));
chatEls.forEach((chatEl) => {
const offsetCollapseDelta = Number(chatEl.dataset.offsetCollapseDelta);
chatEl.style.transform = `translateY(${isForumPanelOpen ? offsetCollapseDelta : -offsetCollapseDelta!}px)`;
});
fastRaf(() => {
chatEls.forEach((chatEl) => {
chatEl.classList.add('animate-collapse');
chatEl.style.transform = '';
});
});
setTimeout(() => {
// Wait one more frame for better animation performance
fastRaf(() => {
chatEls.forEach((chatEl) => {
chatEl.classList.remove('animate-collapse');
});
});
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
}, [containerRef, isForumPanelOpen]);
}

View File

@ -94,7 +94,6 @@
} }
@media (hover: hover) { @media (hover: hover) {
&:hover, &:hover,
&:focus { &:focus {
--background-color: var(--color-chat-hover); --background-color: var(--color-chat-hover);

View File

@ -44,13 +44,14 @@ interface OwnProps {
destructive?: boolean; destructive?: boolean;
multiline?: boolean; multiline?: boolean;
isStatic?: boolean; isStatic?: boolean;
clickArg?: any;
contextActions?: MenuItemContextAction[]; contextActions?: MenuItemContextAction[];
offsetCollapseDelta?: number;
withPortalForMenu?: boolean;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void; onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>, arg?: any) => void; onClick?: (e: React.MouseEvent<HTMLDivElement>, arg?: any) => void;
clickArg?: any;
onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void; onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void;
shouldUsePortalForMenu?: boolean;
} }
const ListItem: FC<OwnProps> = ({ const ListItem: FC<OwnProps> = ({
@ -74,12 +75,13 @@ const ListItem: FC<OwnProps> = ({
multiline, multiline,
isStatic, isStatic,
contextActions, contextActions,
withPortalForMenu,
offsetCollapseDelta,
onMouseDown, onMouseDown,
onClick, onClick,
clickArg, clickArg,
onSecondaryIconClick, onSecondaryIconClick,
onDragEnter, onDragEnter,
shouldUsePortalForMenu,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
let containerRef = useRef<HTMLDivElement>(null); let containerRef = useRef<HTMLDivElement>(null);
@ -107,8 +109,8 @@ const ListItem: FC<OwnProps> = ({
); );
const getLayout = useCallback( const getLayout = useCallback(
() => ({ shouldUsePortalPositioning: shouldUsePortalForMenu }), () => ({ withPortal: withPortalForMenu }),
[shouldUsePortalForMenu], [withPortalForMenu],
); );
const { const {
@ -183,6 +185,7 @@ const ListItem: FC<OwnProps> = ({
className={fullClassName} className={fullClassName}
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
style={style} style={style}
data-offset-collapse-delta={offsetCollapseDelta}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onDragEnter={onDragEnter} onDragEnter={onDragEnter}
> >
@ -230,7 +233,7 @@ const ListItem: FC<OwnProps> = ({
autoClose autoClose
onClose={handleContextMenuClose} onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide} onCloseAnimationEnd={handleContextMenuHide}
shouldUsePortalForMenu={shouldUsePortalForMenu} withPortal={withPortalForMenu}
> >
{contextActions.map((action) => ( {contextActions.map((action) => (
<MenuItem <MenuItem

View File

@ -41,7 +41,7 @@ type OwnProps = {
onClose: () => void; onClose: () => void;
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
shouldUsePortalForMenu?: boolean; withPortal?: boolean;
children: React.ReactNode; children: React.ReactNode;
}; };
@ -70,7 +70,7 @@ const Menu: FC<OwnProps> = ({
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
shouldSkipTransition, shouldSkipTransition,
shouldUsePortalForMenu, withPortal,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
let menuRef = useRef<HTMLDivElement>(null); let menuRef = useRef<HTMLDivElement>(null);
@ -161,7 +161,7 @@ const Menu: FC<OwnProps> = ({
</div> </div>
); );
if (shouldUsePortalForMenu) { if (withPortal) {
return <Portal>{menu}</Portal>; return <Portal>{menu}</Portal>;
} }

View File

@ -54,6 +54,10 @@ addActionHandler('openChat', (global, actions, payload) => {
}; };
} }
if (id !== global.forumPanelChatId) {
actions.closeForumPanel();
}
return updateCurrentMessageList(global, id, threadId, type, shouldReplaceHistory); return updateCurrentMessageList(global, id, threadId, type, shouldReplaceHistory);
}); });

View File

@ -6,7 +6,7 @@ interface Layout {
extraTopPadding?: number; extraTopPadding?: number;
marginSides?: number; marginSides?: number;
extraMarginTop?: number; extraMarginTop?: number;
shouldUsePortalPositioning?: boolean; withPortal?: boolean;
} }
const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16; const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16;
@ -48,7 +48,7 @@ export default function useContextMenuPosition(
extraTopPadding = 0, extraTopPadding = 0,
marginSides = 0, marginSides = 0,
extraMarginTop = 0, extraMarginTop = 0,
shouldUsePortalPositioning = false, withPortal = false,
} = getLayout?.() || {}; } = getLayout?.() || {};
const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) + extraMarginTop : undefined; const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) + extraMarginTop : undefined;
@ -100,8 +100,8 @@ export default function useContextMenuPosition(
const triggerRect = triggerEl.getBoundingClientRect(); const triggerRect = triggerEl.getBoundingClientRect();
const addedYForPortalPositioning = (shouldUsePortalPositioning ? triggerRect.top : 0); const addedYForPortalPositioning = (withPortal ? triggerRect.top : 0);
const addedXForPortalPositioning = (shouldUsePortalPositioning ? triggerRect.left : 0); const addedXForPortalPositioning = (withPortal ? triggerRect.left : 0);
const left = (horizontalPosition === 'left' const left = (horizontalPosition === 'left'
? Math.max(MENU_POSITION_VISUAL_COMFORT_SPACE_PX, Math.min( ? Math.max(MENU_POSITION_VISUAL_COMFORT_SPACE_PX, Math.min(

View File

@ -88,6 +88,21 @@
@include overflow-y-overlay(); @include overflow-y-overlay();
} }
&.forum-panel-open {
.info {
transform: translateX(-25%);
opacity: 0;
}
.Chat[dir="rtl"] .info {
transform: translateX(25%);
}
.ListItem-button {
height: 4.5rem;
}
}
.scroll-container { .scroll-container {
position: relative; position: relative;
} }

View File

@ -150,7 +150,6 @@ $color-message-reaction-own-hover: #b5e0a4;
--color-chat-hover: #{$color-chat-hover}; --color-chat-hover: #{$color-chat-hover};
--color-chat-active: #{$color-chat-active}; --color-chat-active: #{$color-chat-active};
--color-chat-active-greyed: #60a7f0;
--color-item-active: #{$color-item-active}; --color-item-active: #{$color-item-active};
--color-selection-highlight: #{$color-selection}; --color-selection-highlight: #{$color-selection};