diff --git a/src/components/left/main/AvatarBadge.tsx b/src/components/left/main/AvatarBadge.tsx deleted file mode 100644 index 81fd89424..000000000 --- a/src/components/left/main/AvatarBadge.tsx +++ /dev/null @@ -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 = ({ - chat, - isMuted, - isForumPanelActive, -}) => { - return chat && ( -
- -
- ); -}; - -export default memo(withGlobal( - (global, { chatId }): StateProps => { - const chat = selectChat(global, chatId); - if (!chat) { - return {}; - } - - return { - chat, - isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)), - isForumPanelActive: selectIsForumPanelOpen(global), - }; - }, -)(AvatarBadge)); diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index e0e18d170..9a672db0b 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -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 = ({ orderDiff, }); + const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed); + const handleClick = useLastCallback(() => { if (isForum) { if (isSelectedForum) { @@ -253,7 +256,9 @@ const Chat: FC = ({ userStatus={userStatus} isSavedMessages={user?.isSelf} /> - +
+ +
{chat.isCallActive && chat.isCallNotEmpty && ( )} diff --git a/src/components/left/main/ChatBadge.tsx b/src/components/left/main/ChatBadge.tsx index a7e8872e2..e915fe76c 100644 --- a/src/components/left/main/ChatBadge.tsx +++ b/src/components/left/main/ChatBadge.tsx @@ -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; }; const ChatBadge: FC = ({ @@ -51,7 +55,11 @@ const ChatBadge: FC = ({ 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, ); diff --git a/src/global/selectors/ui.ts b/src/global/selectors/ui.ts index 879d635eb..e765f4cd5 100644 --- a/src/global/selectors/ui.ts +++ b/src/global/selectors/ui.ts @@ -73,10 +73,17 @@ export function selectIsForumPanelOpen( 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( + global: T, + ...[tabId = getCurrentTabId()]: TabArgs +) { + return !selectIsForumPanelOpen(global, tabId); +} + export function selectIsReactionPickerOpen( global: T, ...[tabId = getCurrentTabId()]: TabArgs diff --git a/src/hooks/useSelectorSignal.ts b/src/hooks/useSelectorSignal.ts new file mode 100644 index 000000000..497dd2c0c --- /dev/null +++ b/src/hooks/useSelectorSignal.ts @@ -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 = (global: GlobalState) => T; + +interface State { + clientsCount: number; + getter: Signal; + setter: SignalSetter; +} + +const bySelector = new Map, State>(); + +addCallback((global: GlobalState) => { + for (const [selector, { setter }] of bySelector) { + setter(selector(global)); + } +}); + +function useSelectorSignal(selector: Selector): Signal { + 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; diff --git a/src/util/signals.ts b/src/util/signals.ts index edae72be3..790298d55 100644 --- a/src/util/signals.ts +++ b/src/util/signals.ts @@ -13,6 +13,8 @@ export type Signal = ((() => 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; }