diff --git a/packages/pl-fe/src/actions/emoji-reacts.ts b/packages/pl-fe/src/actions/emoji-reacts.ts index 5531452fb..d78b9ccba 100644 --- a/packages/pl-fe/src/actions/emoji-reacts.ts +++ b/packages/pl-fe/src/actions/emoji-reacts.ts @@ -4,7 +4,6 @@ import { getClient } from '../api'; import { importEntities } from './importer'; -import type { Status } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST' as const; @@ -14,26 +13,26 @@ const UNEMOJI_REACT_REQUEST = 'UNEMOJI_REACT_REQUEST' as const; const noOp = () => () => new Promise(f => f(undefined)); -const emojiReact = (status: Pick, emoji: string, custom?: string) => +const emojiReact = (statusId: string, emoji: string, custom?: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return dispatch(noOp()); - dispatch(emojiReactRequest(status.id, emoji, custom)); + dispatch(emojiReactRequest(statusId, emoji, custom)); - return getClient(getState).statuses.createStatusReaction(status.id, emoji).then((response) => { + return getClient(getState).statuses.createStatusReaction(statusId, emoji).then((response) => { dispatch(importEntities({ statuses: [response] })); }).catch((error) => { - dispatch(emojiReactFail(status.id, emoji, error)); + dispatch(emojiReactFail(statusId, emoji, error)); }); }; -const unEmojiReact = (status: Pick, emoji: string) => +const unEmojiReact = (statusId: string, emoji: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return dispatch(noOp()); - dispatch(unEmojiReactRequest(status.id, emoji)); + dispatch(unEmojiReactRequest(statusId, emoji)); - return getClient(getState).statuses.deleteStatusReaction(status.id, emoji).then(response => { + return getClient(getState).statuses.deleteStatusReaction(statusId, emoji).then(response => { dispatch(importEntities({ statuses: [response] })); }); }; diff --git a/packages/pl-fe/src/components/hover-account-wrapper.tsx b/packages/pl-fe/src/components/hover-account-wrapper.tsx index 057c01a61..fa3bb48a9 100644 --- a/packages/pl-fe/src/components/hover-account-wrapper.tsx +++ b/packages/pl-fe/src/components/hover-account-wrapper.tsx @@ -19,7 +19,7 @@ interface IHoverAccountWrapper { } /** Makes a profile hover card appear when the wrapped element is hovered. */ -const HoverAccountWrapper: React.FC = ({ accountId, children, element: Elem = 'div', className }) => { +const HoverAccountWrapper: React.FC = React.memo(({ accountId, children, element: Elem = 'div', className }) => { const dispatch = useAppDispatch(); const { openAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardStore(); @@ -54,6 +54,6 @@ const HoverAccountWrapper: React.FC = ({ accountId, childr {children} ); -}; +}); export { HoverAccountWrapper as default, showAccountHoverCard }; diff --git a/packages/pl-fe/src/components/sidebar-navigation-link.tsx b/packages/pl-fe/src/components/sidebar-navigation-link.tsx index d26e7b898..bc8cb5d4a 100644 --- a/packages/pl-fe/src/components/sidebar-navigation-link.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation-link.tsx @@ -25,7 +25,7 @@ interface ISidebarNavigationLink { } /** Desktop sidebar navigation link. */ -const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef): JSX.Element => { +const SidebarNavigationLink = React.memo(React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef): JSX.Element => { const { icon, activeIcon, text, to = '', count, countMax, onClick } = props; const isActive = location.pathname === to; @@ -72,6 +72,6 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r {text} ); -}); +}), (prevProps, nextProps) => prevProps.count === nextProps.count); export { SidebarNavigationLink as default }; diff --git a/packages/pl-fe/src/components/sidebar-navigation.tsx b/packages/pl-fe/src/components/sidebar-navigation.tsx index 4a364fb3a..072617f5b 100644 --- a/packages/pl-fe/src/components/sidebar-navigation.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import Icon from 'pl-fe/components/ui/icon'; @@ -57,7 +57,7 @@ const SidebarNavigation = React.memo(() => { const restrictUnauth = instance.pleroma.metadata.restrict_unauthenticated; - const makeMenu = (): Menu => { + const menu = useMemo((): Menu => { const menu: Menu = []; if (account) { @@ -155,9 +155,7 @@ const SidebarNavigation = React.memo(() => { } return menu; - }; - - const menu = makeMenu(); + }, [!!account, features, isDeveloper, followRequestsCount, interactionRequestsCount, scheduledStatusCount, draftCount]); return ( diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 6eeedba87..42e42c764 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -1,5 +1,5 @@ import { type CustomEmoji, GroupRoles } from 'pl-api'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -484,15 +484,15 @@ const WrenchButton: React.FC = ({ const handleWrenchClick: React.EventHandler = (e) => { if (wrenches?.me) { - dispatch(unEmojiReact(status, '🔧')); + dispatch(unEmojiReact(status.id, '🔧')); } else { - dispatch(emojiReact(status, '🔧')); + dispatch(emojiReact(status.id, '🔧')); } }; const handleWrenchLongPress = () => { if (features.customEmojiReacts && hasLongerWrench) { - dispatch(emojiReact(status, hasLongerWrench.shortcode, hasLongerWrench.url)); + dispatch(emojiReact(status.id, hasLongerWrench.shortcode, hasLongerWrench.url)); } else if (wrenches?.count) { openModal('REACTIONS', { statusId: status.id, reaction: wrenches.name }); } @@ -524,7 +524,7 @@ const EmojiPickerButton: React.FC const features = useFeatures(); const handlePickEmoji = (emoji: EmojiType) => { - dispatch(emojiReact(status, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined)); + dispatch(emojiReact(status.id, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined)); }; return me && !withLabels && features.emojiReacts && ( @@ -613,178 +613,177 @@ const MenuButton: React.FC = ({ const isStaff = account ? account.is_admin || account.is_moderator : false; const isAdmin = account ? account.is_admin : false; - const handleBookmarkClick: React.EventHandler = (e) => { - dispatch(toggleBookmark(status)); - }; - - const handleBookmarkFolderClick = () => { - openModal('SELECT_BOOKMARK_FOLDER', { - statusId: status.id, - }); - }; - - const doDeleteStatus = (withRedraft = false) => { - if (!deleteModal) { - dispatch(deleteStatus(status.id, withRedraft)); - } else { - openModal('CONFIRM', { - heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading), - message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), - confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.id, withRedraft)), - }); - } - }; - - const handleDeleteClick: React.EventHandler = (e) => { - doDeleteStatus(); - }; - - const handleRedraftClick: React.EventHandler = (e) => { - doDeleteStatus(true); - }; - - const handleEditClick: React.EventHandler = () => { - if (status.event) history.push(`/@${status.account.acct}/events/${status.id}/edit`); - else dispatch(editStatus(status.id)); - }; - - const handlePinClick: React.EventHandler = (e) => { - dispatch(togglePin(status)); - }; - - const handleReblogClick: React.EventHandler = (e) => { - const modalReblog = () => dispatch(toggleReblog(status)); - if ((e && e.shiftKey) || !boostModal) { - modalReblog(); - } else { - openModal('BOOST', { statusId: status.id, onReblog: modalReblog }); - } - }; - - const handleMentionClick: React.EventHandler = (e) => { - dispatch(mentionCompose(status.account)); - }; - - const handleDirectClick: React.EventHandler = (e) => { - dispatch(directCompose(status.account)); - }; - - const handleChatClick: React.EventHandler = (e) => { - const account = status.account; - - getOrCreateChatByAccountId(account.id) - .then((chat) => history.push(`/chats/${chat.id}`)) - .catch(() => {}); - }; - - const handleMuteClick: React.EventHandler = (e) => { - openModal('MUTE', { accountId: status.account.id }); - }; - - const handleBlockClick: React.EventHandler = (e) => { - const account = status.account; - - openModal('CONFIRM', { - heading: , - message: @{account.acct} }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.id)), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.id)); - dispatch(initReport(ReportableEntities.STATUS, account, { status })); - }, - }); - }; - - const handleEmbed = () => { - openModal('EMBED', { - url: status.url, - onError: (error: any) => toast.showAlertForError(error), - }); - }; - - const handleOpenReactionsModal = () => { - openModal('REACTIONS', { statusId: status.id }); - }; - - const handleReport: React.EventHandler = (e) => { - dispatch(initReport(ReportableEntities.STATUS, status.account, { status })); - }; - - const handleConversationMuteClick: React.EventHandler = (e) => { - dispatch(toggleMuteStatus(status)); - }; - - const handleCopy: React.EventHandler = (e) => { - const { uri } = status; - - copy(uri); - }; - - const onModerate: React.MouseEventHandler = (e) => { - const account = status.account; - openModal('ACCOUNT_MODERATION', { accountId: account.id }); - }; - - const handleDeleteStatus: React.EventHandler = (e) => { - dispatch(deleteStatusModal(intl, status.id)); - }; - - const handleToggleStatusSensitivity: React.EventHandler = (e) => { - dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive)); - }; - - const handleDeleteFromGroup: React.EventHandler = () => { - const account = status.account; - - openModal('CONFIRM', { - heading: intl.formatMessage(messages.deleteHeading), - message: intl.formatMessage(messages.deleteFromGroupMessage, { name: {account.username} }), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => { - deleteGroupStatus.mutate(status.id, { - onSuccess() { - dispatch(deleteFromTimelines(status.id)); - }, - }); - }, - }); - }; - - const handleBlockFromGroup = () => { - openModal('CONFIRM', { - heading: intl.formatMessage(messages.groupBlockFromGroupHeading), - message: intl.formatMessage(messages.groupBlockFromGroupMessage, { name: status.account.username }), - confirm: intl.formatMessage(messages.groupBlockConfirm), - onConfirm: () => { - blockGroupMember(undefined, { - onSuccess: () => { - toast.success(intl.formatMessage(messages.blocked, { name: account?.acct })); - }, - }); - }, - }); - }; - - const handleIgnoreLanguage = () => { - dispatch(changeSetting(['autoTranslate'], [...knownLanguages, status.language], { showAlert: true })); - }; - - const handleTranslate = () => { - if (targetLanguage) { - hideTranslation(status.id); - } else { - fetchTranslation(status.id, intl.locale); - } - }; - - const _makeMenu = (publicStatus: boolean) => { + const menu = useMemo(() => { const mutingConversation = status.muted; const ownAccount = status.account_id === me; - const username = status.account.username; - const account = status.account; + const { username, local: localAccount } = status.account; + + const handleBookmarkClick: React.EventHandler = (e) => { + dispatch(toggleBookmark(status)); + }; + + const handleBookmarkFolderClick = () => { + openModal('SELECT_BOOKMARK_FOLDER', { + statusId: status.id, + }); + }; + + const doDeleteStatus = (withRedraft = false) => { + if (!deleteModal) { + dispatch(deleteStatus(status.id, withRedraft)); + } else { + openModal('CONFIRM', { + heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading), + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.id, withRedraft)), + }); + } + }; + + const handleDeleteClick: React.EventHandler = (e) => { + doDeleteStatus(); + }; + + const handleRedraftClick: React.EventHandler = (e) => { + doDeleteStatus(true); + }; + + const handleEditClick: React.EventHandler = () => { + if (status.event) history.push(`/@${status.account.acct}/events/${status.id}/edit`); + else dispatch(editStatus(status.id)); + }; + + const handlePinClick: React.EventHandler = (e) => { + dispatch(togglePin(status)); + }; + + const handleReblogClick: React.EventHandler = (e) => { + const modalReblog = () => dispatch(toggleReblog(status)); + if ((e && e.shiftKey) || !boostModal) { + modalReblog(); + } else { + openModal('BOOST', { statusId: status.id, onReblog: modalReblog }); + } + }; + + const handleMentionClick: React.EventHandler = (e) => { + dispatch(mentionCompose(status.account)); + }; + + const handleDirectClick: React.EventHandler = (e) => { + dispatch(directCompose(status.account)); + }; + + const handleChatClick: React.EventHandler = (e) => { + const account = status.account; + + getOrCreateChatByAccountId(account.id) + .then((chat) => history.push(`/chats/${chat.id}`)) + .catch(() => {}); + }; + + const handleMuteClick: React.EventHandler = (e) => { + openModal('MUTE', { accountId: status.account.id }); + }; + + const handleBlockClick: React.EventHandler = (e) => { + const account = status.account; + + openModal('CONFIRM', { + heading: , + message: @{account.acct} }} />, + confirm: intl.formatMessage(messages.blockConfirm), + onConfirm: () => dispatch(blockAccount(account.id)), + secondary: intl.formatMessage(messages.blockAndReport), + onSecondary: () => { + dispatch(blockAccount(account.id)); + dispatch(initReport(ReportableEntities.STATUS, account, { status })); + }, + }); + }; + + const handleEmbed = () => { + openModal('EMBED', { + url: status.url, + onError: (error: any) => toast.showAlertForError(error), + }); + }; + + const handleOpenReactionsModal = () => { + openModal('REACTIONS', { statusId: status.id }); + }; + + const handleReport: React.EventHandler = (e) => { + dispatch(initReport(ReportableEntities.STATUS, status.account, { status })); + }; + + const handleConversationMuteClick: React.EventHandler = (e) => { + dispatch(toggleMuteStatus(status)); + }; + + const handleCopy: React.EventHandler = (e) => { + const { uri } = status; + + copy(uri); + }; + + const onModerate: React.MouseEventHandler = (e) => { + const account = status.account; + openModal('ACCOUNT_MODERATION', { accountId: account.id }); + }; + + const handleDeleteStatus: React.EventHandler = (e) => { + dispatch(deleteStatusModal(intl, status.id)); + }; + + const handleToggleStatusSensitivity: React.EventHandler = (e) => { + dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive)); + }; + + const handleDeleteFromGroup: React.EventHandler = () => { + const account = status.account; + + openModal('CONFIRM', { + heading: intl.formatMessage(messages.deleteHeading), + message: intl.formatMessage(messages.deleteFromGroupMessage, { name: {account.username} }), + confirm: intl.formatMessage(messages.deleteConfirm), + onConfirm: () => { + deleteGroupStatus.mutate(status.id, { + onSuccess() { + dispatch(deleteFromTimelines(status.id)); + }, + }); + }, + }); + }; + + const handleBlockFromGroup = () => { + openModal('CONFIRM', { + heading: intl.formatMessage(messages.groupBlockFromGroupHeading), + message: intl.formatMessage(messages.groupBlockFromGroupMessage, { name: status.account.username }), + confirm: intl.formatMessage(messages.groupBlockConfirm), + onConfirm: () => { + blockGroupMember(undefined, { + onSuccess: () => { + toast.success(intl.formatMessage(messages.blocked, { name: account?.acct })); + }, + }); + }, + }); + }; + + const handleIgnoreLanguage = () => { + dispatch(changeSetting(['autoTranslate'], [...knownLanguages, status.language], { showAlert: true })); + }; + + const handleTranslate = () => { + if (targetLanguage) { + hideTranslation(status.id); + } else { + fetchTranslation(status.id, intl.locale); + } + }; const menu: Menu = []; @@ -803,7 +802,7 @@ const MenuButton: React.FC = ({ icon: require('@tabler/icons/outline/clipboard-copy.svg'), }); - if (features.embeds && account.local) { + if (features.embeds && localAccount) { menu.push({ text: intl.formatMessage(messages.embed), action: handleEmbed, @@ -842,7 +841,7 @@ const MenuButton: React.FC = ({ }); } - if (features.federating && !account.local) { + if (features.federating && !localAccount) { const { hostname: domain } = new URL(status.uri); menu.push({ text: intl.formatMessage(messages.external, { domain }), @@ -1026,11 +1025,9 @@ const MenuButton: React.FC = ({ } return menu; - }; + }, [me, targetLanguage, status.muted, status.emoji_reactions.length > 0, status.pinned, status.reblogged]); - const menu = _makeMenu(publicStatus); - - return ( + return useMemo(() => ( = ({ theme={statusActionButtonTheme} /> - ); + ), [menu, statusActionButtonTheme]); }; interface IStatusActionBar { @@ -1065,18 +1062,20 @@ const StatusActionBar: React.FC = ({ const me = useAppSelector(state => state.me); - if (!status) { - return null; - } + const publicStatus = useMemo(() => status ? ['public', 'unlisted', 'group'].includes(status.visibility) : false, [status.visibility]); - const onOpenUnauthorizedModal = (action?: UnauthorizedModalAction) => { + const onContainerClick: React.MouseEventHandler = useCallback((e) => e.stopPropagation(), []); + + const onOpenUnauthorizedModal = useCallback((action?: UnauthorizedModalAction) => { openModal('UNAUTHORIZED', { action, ap_id: status.url, }); - }; + }, []); - const publicStatus = useMemo(() => ['public', 'unlisted', 'group'].includes(status.visibility), [status.visibility]); + if (!status) { + return null; + } const spacing: { [key: string]: React.ComponentProps['space']; @@ -1087,79 +1086,77 @@ const StatusActionBar: React.FC = ({ }; return ( - - e.stopPropagation()} - alignItems='center' - > - + + - + - + - + - + - + - + - - + ); }; diff --git a/packages/pl-fe/src/components/status-action-button.tsx b/packages/pl-fe/src/components/status-action-button.tsx index e1bf3f007..dae7b897e 100644 --- a/packages/pl-fe/src/components/status-action-button.tsx +++ b/packages/pl-fe/src/components/status-action-button.tsx @@ -23,7 +23,7 @@ interface IStatusActionCounter { } /** Action button numerical counter, eg "5" likes. */ -const StatusActionCounter: React.FC = ({ count = 0 }): JSX.Element => { +const StatusActionCounter: React.FC = React.memo(({ count = 0 }): JSX.Element => { const { demetricator } = useSettings(); return ( @@ -31,7 +31,7 @@ const StatusActionCounter: React.FC = ({ count = 0 }): JSX ); -}; +}); interface IStatusActionButton extends React.ButtonHTMLAttributes { iconClassName?: string; diff --git a/packages/pl-fe/src/components/status-language-picker.tsx b/packages/pl-fe/src/components/status-language-picker.tsx index 9db6409cc..dbbf4e1ad 100644 --- a/packages/pl-fe/src/components/status-language-picker.tsx +++ b/packages/pl-fe/src/components/status-language-picker.tsx @@ -20,7 +20,7 @@ interface IStatusLanguagePicker { showLabel?: boolean; } -const StatusLanguagePicker: React.FC = ({ status, showLabel }) => { +const StatusLanguagePicker: React.FC = React.memo(({ status, showLabel }) => { const intl = useIntl(); const { statuses, setStatusLanguage } = useStatusMetaStore(); @@ -55,7 +55,7 @@ const StatusLanguagePicker: React.FC = ({ status, showLab ); -}; +}); export { StatusLanguagePicker as default, diff --git a/packages/pl-fe/src/components/status-list.tsx b/packages/pl-fe/src/components/status-list.tsx index c089ad78b..73643fbd2 100644 --- a/packages/pl-fe/src/components/status-list.tsx +++ b/packages/pl-fe/src/components/status-list.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import debounce from 'lodash/debounce'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import LoadGap from 'pl-fe/components/load-gap'; @@ -128,45 +128,45 @@ const StatusList: React.FC = ({ ); }; - const renderFeaturedStatuses = (): React.ReactNode[] => { - if (!featuredStatusIds) return []; - - return featuredStatusIds.map(statusId => ( - - )); - }; - - const renderStatuses = (): React.ReactNode[] => { - if (isLoading || statusIds.length > 0) { - return statusIds.reduce((acc, statusId, index) => { - if (statusId === null) { - const gap = renderLoadGap(index); - if (gap) { - acc.push(gap); + const scrollableContent = useMemo(() => { + const renderFeaturedStatuses = (): React.ReactNode[] => { + if (!featuredStatusIds) return []; + + return featuredStatusIds.map(statusId => ( + + )); + }; + + const renderStatuses = (): React.ReactNode[] => { + if (isLoading || statusIds.length > 0) { + return statusIds.reduce((acc, statusId, index) => { + if (statusId === null) { + const gap = renderLoadGap(index); + if (gap) { + acc.push(gap); + } + } else if (statusId.startsWith('末pending-')) { + acc.push(renderPendingStatus(statusId)); + } else { + acc.push(renderStatus(statusId)); } - } else if (statusId.startsWith('末pending-')) { - acc.push(renderPendingStatus(statusId)); - } else { - acc.push(renderStatus(statusId)); - } + + return acc; + }, [] as React.ReactNode[]); + } else { + return []; + } + }; - return acc; - }, [] as React.ReactNode[]); - } else { - return []; - } - }; - - const renderScrollableContent = () => { const featuredStatuses = renderFeaturedStatuses(); const statuses = renderStatuses(); @@ -175,7 +175,7 @@ const StatusList: React.FC = ({ } else { return statuses; } - }; + }, [featuredStatusIds, statusIds, isLoading, timelineId, showGroup, divideType]); if (isPartial) { return ( @@ -209,7 +209,7 @@ const StatusList: React.FC = ({ })} {...other} > - {renderScrollableContent()} + {scrollableContent} ); }; diff --git a/packages/pl-fe/src/components/status-reactions-bar.tsx b/packages/pl-fe/src/components/status-reactions-bar.tsx index 178b2e0a2..a15b8bb12 100644 --- a/packages/pl-fe/src/components/status-reactions-bar.tsx +++ b/packages/pl-fe/src/components/status-reactions-bar.tsx @@ -33,13 +33,13 @@ interface IStatusReactionsBar { } interface IStatusReaction { - status: Pick; + statusId: string; reaction: EmojiReaction; obfuscate?: boolean; unauthenticated?: boolean; } -const StatusReaction: React.FC = ({ reaction, status, obfuscate, unauthenticated }) => { +const StatusReaction: React.FC = ({ reaction, statusId, obfuscate, unauthenticated }) => { const dispatch = useAppDispatch(); const intl = useIntl(); const features = useFeatures(); @@ -51,7 +51,7 @@ const StatusReaction: React.FC = ({ reaction, status, obfuscate e.stopPropagation(); if ('vibrate' in navigator) navigator.vibrate(1); - openModal('REACTIONS', { statusId: status.id, reaction: reaction.name }); + openModal('REACTIONS', { statusId: statusId, reaction: reaction.name }); }); if (!reaction.count) return null; @@ -61,11 +61,11 @@ const StatusReaction: React.FC = ({ reaction, status, obfuscate if (unauthenticated) { if (!features.emojiReactsList) return; - openModal('REACTIONS', { statusId: status.id, reaction: reaction.name }); + openModal('REACTIONS', { statusId, reaction: reaction.name }); } else if (reaction.me) { - dispatch(unEmojiReact(status, reaction.name)); + dispatch(unEmojiReact(statusId, reaction.name)); } else { - dispatch(emojiReact(status, reaction.name, reaction.url)); + dispatch(emojiReact(statusId, reaction.name, reaction.url)); } }; @@ -112,7 +112,7 @@ const StatusReactionsBar: React.FC = ({ status, collapsed } const features = useFeatures(); const handlePickEmoji = (emoji: EmojiType) => { - dispatch(emojiReact(status, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined)); + dispatch(emojiReact(status.id, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined)); }; if ((demetricator || status.emoji_reactions.length === 0) && collapsed) return null; @@ -125,7 +125,7 @@ const StatusReactionsBar: React.FC = ({ status, collapsed } {sortedReactions.map((reaction) => reaction.count ? ( = (props) => { const didShowCard = useRef(false); const node = useRef(null); - const getStatus = useCallback(makeGetStatus(), []); + const getStatus = useMemo(makeGetStatus, []); const actualStatus = useAppSelector(state => status.reblog_id && getStatus(state, { id: status.reblog_id }) || status)!; const isReblog = status.reblog_id; @@ -185,7 +185,7 @@ const Status: React.FC = (props) => { const handleUnfilter = () => dispatch(unfilterStatus(status.filtered.length ? status.id : actualStatus.id)); - const renderStatusInfo = () => { + const statusInfo = useMemo(() => { if (isReblog && showGroup && group) { return ( = (props) => { /> ); } - }; + }, [status.accounts, group?.id]); if (!status) return null; @@ -366,7 +366,7 @@ const Status: React.FC = (props) => { })} data-id={status.id} > - {renderStatusInfo()} + {statusInfo} = (props) => { avatarSize={avatarSize} items={( <> - + )} diff --git a/packages/pl-fe/src/components/thumb-navigation.tsx b/packages/pl-fe/src/components/thumb-navigation.tsx index f2f18e4e7..dc4838abd 100644 --- a/packages/pl-fe/src/components/thumb-navigation.tsx +++ b/packages/pl-fe/src/components/thumb-navigation.tsx @@ -24,7 +24,7 @@ const messages = defineMessages({ sidebar: { id: 'navigation.sidebar', defaultMessage: 'Open sidebar' }, }); -const ThumbNavigation: React.FC = (): JSX.Element => { +const ThumbNavigation: React.FC = React.memo((): JSX.Element => { const intl = useIntl(); const dispatch = useAppDispatch(); const { account } = useOwnAccount(); @@ -132,6 +132,6 @@ const ThumbNavigation: React.FC = (): JSX.Element => { )} ); -}; +}); export { ThumbNavigation as default }; diff --git a/packages/pl-fe/src/containers/status-container.tsx b/packages/pl-fe/src/containers/status-container.tsx index 1a71db238..622951b25 100644 --- a/packages/pl-fe/src/containers/status-container.tsx +++ b/packages/pl-fe/src/containers/status-container.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import Status, { IStatus } from 'pl-fe/components/status'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; @@ -16,13 +16,13 @@ interface IStatusContainer extends Omit { * @deprecated Use the Status component directly. */ const StatusContainer: React.FC = (props) => { - const { id, contextType, ...rest } = props; + const { id, contextType } = props; - const getStatus = useCallback(makeGetStatus(), []); + const getStatus = useMemo(makeGetStatus, []); const status = useAppSelector(state => getStatus(state, { id, contextType })); if (status) { - return ; + return ; } else { return null; } diff --git a/packages/pl-fe/src/features/home-timeline/index.tsx b/packages/pl-fe/src/features/home-timeline/index.tsx index cab35870d..bd5758e2f 100644 --- a/packages/pl-fe/src/features/home-timeline/index.tsx +++ b/packages/pl-fe/src/features/home-timeline/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; @@ -31,38 +31,33 @@ const HomeTimeline: React.FC = () => { const isPartial = useAppSelector(state => state.timelines.home?.isPartial === true); - const handleLoadMore = (maxId: string) => { - dispatch(fetchHomeTimeline(true)); - }; - // Mastodon generates the feed in Redis, and can return a partial timeline // (HTTP 206) for new users. Poll until we get a full page of results. - const checkIfReloadNeeded = () => { + const checkIfReloadNeeded = useCallback((isPartial: boolean) => { if (isPartial) { polling.current = setInterval(() => { dispatch(fetchHomeTimeline()); }, 3000); } else { - stopPolling(); + if (polling.current) { + clearInterval(polling.current); + polling.current = null; + } } - }; - - const stopPolling = () => { - if (polling.current) { - clearInterval(polling.current); - polling.current = null; - } - }; - - const handleRefresh = () => dispatch(fetchHomeTimeline(true)); - - useEffect(() => { - checkIfReloadNeeded(); return () => { - stopPolling(); + if (polling.current) { + clearInterval(polling.current); + polling.current = null; + } }; - }, [isPartial]); + }, []); + + const handleLoadMore = useCallback(() => dispatch(fetchHomeTimeline(true)), []); + + const handleRefresh = useCallback(() => dispatch(fetchHomeTimeline(false)), []); + + useEffect(() => checkIfReloadNeeded(isPartial), [isPartial]); return ( diff --git a/packages/pl-fe/src/features/status/components/detailed-status.tsx b/packages/pl-fe/src/features/status/components/detailed-status.tsx index 160d2d4e9..a7451f0c1 100644 --- a/packages/pl-fe/src/features/status/components/detailed-status.tsx +++ b/packages/pl-fe/src/features/status/components/detailed-status.tsx @@ -151,7 +151,7 @@ const DetailedStatus: React.FC = ({ - + diff --git a/packages/pl-fe/src/features/status/components/status-type-icon.tsx b/packages/pl-fe/src/features/status/components/status-type-icon.tsx index 2889b9066..4a030df2b 100644 --- a/packages/pl-fe/src/features/status/components/status-type-icon.tsx +++ b/packages/pl-fe/src/features/status/components/status-type-icon.tsx @@ -7,7 +7,7 @@ import Text from 'pl-fe/components/ui/text'; import type { Status } from 'pl-fe/normalizers/status'; interface IStatusTypeIcon { - status: Pick; + visibility: Status['visibility']; } const messages: Record = defineMessages({ @@ -29,11 +29,11 @@ const STATUS_TYPE_ICONS: Record = { subscribers: require('@tabler/icons/outline/coin.svg'), }; -const StatusTypeIcon: React.FC = ({ status }) => { +const StatusTypeIcon: React.FC = React.memo(({ visibility }) => { const intl = useIntl(); - const icon = STATUS_TYPE_ICONS[status.visibility]; - const message = messages[status.visibility]; + const icon = STATUS_TYPE_ICONS[visibility]; + const message = messages[visibility]; if (!icon) return null; @@ -44,6 +44,6 @@ const StatusTypeIcon: React.FC = ({ status }) => { ); -}; +}); export { StatusTypeIcon as default }; diff --git a/packages/pl-fe/src/features/ui/components/timeline.tsx b/packages/pl-fe/src/features/ui/components/timeline.tsx index 68498deaf..52cff6092 100644 --- a/packages/pl-fe/src/features/ui/components/timeline.tsx +++ b/packages/pl-fe/src/features/ui/components/timeline.tsx @@ -1,5 +1,5 @@ import debounce from 'lodash/debounce'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { defineMessages } from 'react-intl'; import { dequeueTimeline, scrollTopTimeline } from 'pl-fe/actions/timelines'; @@ -31,7 +31,7 @@ const Timeline: React.FC = ({ ...rest }) => { const dispatch = useAppDispatch(); - const getStatusIds = useCallback(makeGetStatusIds(), []); + const getStatusIds = useMemo(makeGetStatusIds, []); const statusIds = useAppSelector(state => getStatusIds(state, { type: timelineId, prefix })); const lastStatusId = statusIds.at(-1); diff --git a/packages/pl-fe/src/features/ui/index.tsx b/packages/pl-fe/src/features/ui/index.tsx index 441322aa7..1f81acebc 100644 --- a/packages/pl-fe/src/features/ui/index.tsx +++ b/packages/pl-fe/src/features/ui/index.tsx @@ -152,7 +152,7 @@ interface ISwitchingColumnsArea { children: React.ReactNode; } -const SwitchingColumnsArea: React.FC = ({ children }) => { +const SwitchingColumnsArea: React.FC = React.memo(({ children }) => { const instance = useInstance(); const features = useFeatures(); const { search } = useLocation(); @@ -349,13 +349,13 @@ const SwitchingColumnsArea: React.FC = ({ children }) => ); -}; +}); interface IUI { children?: React.ReactNode; } -const UI: React.FC = ({ children }) => { +const UI: React.FC = React.memo(({ children }) => { const history = useHistory(); const dispatch = useAppDispatch(); const node = useRef(null); @@ -507,6 +507,6 @@ const UI: React.FC = ({ children }) => { ); -}; +}); export { UI as default }; diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index 1eddc1584..512136f65 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -121,49 +121,52 @@ const checkFiltered = (index: string, filters: Array) => type APIStatus = { id: string; username?: string }; -const makeGetStatus = () => createSelector( - [ - (state: RootState, { id }: APIStatus) => state.statuses[id], - (state: RootState, { id }: APIStatus) => state.statuses[state.statuses[id]?.reblog_id || ''] || null, - (state: RootState, { id }: APIStatus) => state.statuses[state.statuses[id]?.quote_id || ''] || null, - (state: RootState, { id }: APIStatus) => { - const group = state.statuses[id]?.group_id; - if (group) return state.entities[Entities.GROUPS]?.store[group] as Group; - return undefined; - }, - (state: RootState, { id }: APIStatus) => state.polls[id] || null, - (_state: RootState, { username }: APIStatus) => username, - getFilters, - (state: RootState) => state.me, - (state: RootState) => state.auth.client.features, - ], +const makeGetStatus = () => { + console.log('making get status'); + return createSelector( + [ + (state: RootState, { id }: APIStatus) => state.statuses[id], + (state: RootState, { id }: APIStatus) => state.statuses[state.statuses[id]?.reblog_id || ''] || null, + (state: RootState, { id }: APIStatus) => state.statuses[state.statuses[id]?.quote_id || ''] || null, + (state: RootState, { id }: APIStatus) => { + const group = state.statuses[id]?.group_id; + if (group) return state.entities[Entities.GROUPS]?.store[group] as Group; + return undefined; + }, + (state: RootState, { id }: APIStatus) => state.polls[id] || null, + (_state: RootState, { username }: APIStatus) => username, + getFilters, + (state: RootState) => state.me, + (state: RootState) => state.auth.client.features, + ], - (statusBase, statusReblog, statusQuote, statusGroup, poll, username, filters, me, features) => { + (statusBase, statusReblog, statusQuote, statusGroup, poll, username, filters, me, features) => { // const locale = getLocale('en'); - if (!statusBase) return null; - const { account } = statusBase; - const accountUsername = account.acct; + if (!statusBase) return null; + const { account } = statusBase; + const accountUsername = account.acct; - // Must be owner of status if username exists. - if (accountUsername !== username && username !== undefined) { - return null; - } + // Must be owner of status if username exists. + if (accountUsername !== username && username !== undefined) { + return null; + } - const filtered = features.filtersV2 - ? statusBase.filtered - : features.filters && account.id !== me && checkFiltered(statusReblog?.search_index || statusBase.search_index || '', filters) || []; + const filtered = features.filtersV2 + ? statusBase.filtered + : features.filters && account.id !== me && checkFiltered(statusReblog?.search_index || statusBase.search_index || '', filters) || []; - return { - ...statusBase, - reblog: statusReblog || null, - quote: statusQuote || null, - group: statusGroup || null, - poll, - filtered, - }; - }, -); + return { + ...statusBase, + reblog: statusReblog || null, + quote: statusQuote || null, + group: statusGroup || null, + poll, + filtered, + }; + }, + ); +}; type SelectedStatus = Exclude>, null>;