[Refactoring, Perf] Chat: Get rid of AvatarBadge container

This commit is contained in:
Alexander Zinchuk 2023-07-05 13:13:58 +02:00
parent eddb8016ea
commit 4c9815a394
6 changed files with 83 additions and 57 deletions

View File

@ -1,52 +0,0 @@
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 ChatBadge from './ChatBadge';
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">
<ChatBadge 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

@ -29,6 +29,7 @@ import {
selectChatMessage,
selectCurrentMessageList,
selectDraft,
selectIsForumPanelClosed,
selectNotifyExceptions,
selectNotifySettings,
selectOutgoingStatus,
@ -42,6 +43,7 @@ import buildClassName from '../../../util/buildClassName';
import { createLocationHash } from '../../../util/routing';
import useLastCallback from '../../../hooks/useLastCallback';
import useSelectorSignal from '../../../hooks/useSelectorSignal';
import useChatContextActions from '../../../hooks/useChatContextActions';
import useFlag from '../../../hooks/useFlag';
import useChatListEntry from './hooks/useChatListEntry';
@ -58,7 +60,6 @@ import ChatFolderModal from '../ChatFolderModal.async';
import MuteChatModal from '../MuteChatModal.async';
import ChatCallStatus from './ChatCallStatus';
import ChatBadge from './ChatBadge';
import AvatarBadge from './AvatarBadge';
import './Chat.scss';
@ -157,6 +158,8 @@ const Chat: FC<OwnProps & StateProps> = ({
orderDiff,
});
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
const handleClick = useLastCallback(() => {
if (isForum) {
if (isSelectedForum) {
@ -253,7 +256,9 @@ const Chat: FC<OwnProps & StateProps> = ({
userStatus={userStatus}
isSavedMessages={user?.isSelf}
/>
<AvatarBadge chatId={chatId} />
<div className="avatar-badge-wrapper">
<ChatBadge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={getIsForumPanelClosed} />
</div>
{chat.isCallActive && chat.isCallNotEmpty && (
<ChatCallStatus isMobile={isMobile} isSelected={isSelected} isActive={withInterfaceAnimations} />
)}

View File

@ -3,9 +3,13 @@ import React, { memo, useMemo } from '../../../lib/teact/teact';
import type { ApiChat, ApiTopic } from '../../../api/types';
import type { FC } from '../../../lib/teact/teact';
import type { Signal } from '../../../util/signals';
import { isSignal } from '../../../util/signals';
import { formatIntegerCompact } from '../../../util/textFormat';
import buildClassName from '../../../util/buildClassName';
import useDerivedState from '../../../hooks/useDerivedState';
import ShowTransition from '../../ui/ShowTransition';
import AnimatedCounter from '../../common/AnimatedCounter';
@ -18,7 +22,7 @@ type OwnProps = {
isPinned?: boolean;
isMuted?: boolean;
shouldShowOnlyMostImportant?: boolean;
forceHidden?: boolean;
forceHidden?: boolean | Signal<boolean>;
};
const ChatBadge: FC<OwnProps> = ({
@ -51,7 +55,11 @@ const ChatBadge: FC<OwnProps> = ({
const hasUnreadMark = topic ? false : chat.hasUnreadMark;
const isShown = !forceHidden && Boolean(
const resolvedForceHidden = useDerivedState(
() => (isSignal(forceHidden) ? forceHidden() : forceHidden),
[forceHidden],
);
const isShown = !resolvedForceHidden && Boolean(
unreadCount || unreadMentionsCount || hasUnreadMark || isPinned || unreadReactionsCount
|| isTopicUnopened,
);

View File

@ -73,10 +73,17 @@ export function selectIsForumPanelOpen<T extends GlobalState>(
const tabState = selectTabState(global, tabId);
return Boolean(tabState.forumPanelChatId) && (
tabState.globalSearch.query === undefined || tabState.globalSearch.isClosing
tabState.globalSearch.query === undefined || Boolean(tabState.globalSearch.isClosing)
);
}
export function selectIsForumPanelClosed<T extends GlobalState>(
global: T,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
return !selectIsForumPanelOpen(global, tabId);
}
export function selectIsReactionPickerOpen<T extends GlobalState>(
global: T,
...[tabId = getCurrentTabId()]: TabArgs<T>

View File

@ -0,0 +1,56 @@
import type { GlobalState } from '../global/types';
import type { Signal, SignalSetter } from '../util/signals';
import { getGlobal } from '../global';
import { createSignal } from '../util/signals';
import useEffectOnce from './useEffectOnce';
import { addCallback } from '../lib/teact/teactn';
/*
This hook is a more performant variation of the standard React `useSelector` hook. It allows to:
a) Avoid multiple subscriptions to global updates by leveraging a single selector reference.
b) Return a signal instead of forcing a component update right away.
*/
type Selector<T extends any> = (global: GlobalState) => T;
interface State<T extends any> {
clientsCount: number;
getter: Signal<T>;
setter: SignalSetter;
}
const bySelector = new Map<Selector<any>, State<any>>();
addCallback((global: GlobalState) => {
for (const [selector, { setter }] of bySelector) {
setter(selector(global));
}
});
function useSelectorSignal<T extends any>(selector: Selector<T>): Signal<T> {
let state = bySelector.get(selector);
if (!state) {
const [getter, setter] = createSignal(selector(getGlobal()));
state = { clientsCount: 0, getter, setter };
bySelector.set(selector, state);
}
useEffectOnce(() => {
state!.clientsCount++;
return () => {
state!.clientsCount--;
if (!state!.clientsCount) {
bySelector.delete(selector);
}
};
});
return state.getter;
}
export default useSelectorSignal;

View File

@ -13,6 +13,8 @@ export type Signal<T = any> = ((() => T) & {
subscribe: (cb: AnyToVoidFunction) => NoneToVoidFunction;
});
export type SignalSetter = (newValue: any) => void;
export function isSignal(obj: any): obj is Signal {
return typeof obj === 'function' && SIGNAL_MARK in obj;
}