perf/UX audit round 2, fix eggplant reaction button
Some checks failed
Some checks failed
Runtime: memoize selectors in report modal, event discussion, followed tag filter, and upload form to avoid unnecessary allocations. Bundle: lazy-load crypto icons (500+ SVGs), remove datepicker CSS from main entry (already in lazy wrapper), drop unused Inter 200/300 weights. Stability: add null guards in compose upload and conversation components, add keyboard handler to reply mentions button, surface local translation unavailability instead of swallowing errors. Fix eggplant button: preferences toggle was still writing to the old showWrenchButton setting key, so the eggplant reaction button could never be enabled.
This commit is contained in:
@ -84,7 +84,19 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
|||||||
|
|
||||||
if (to.length > 2) {
|
if (to.length > 2) {
|
||||||
accounts.push(
|
accounts.push(
|
||||||
<span key='more' className='cursor-pointer hover:underline' role='button' onClick={handleOpenMentionsModal} tabIndex={0}>
|
<span
|
||||||
|
key='more'
|
||||||
|
className='cursor-pointer hover:underline'
|
||||||
|
role='button'
|
||||||
|
onClick={handleOpenMentionsModal}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleOpenMentionsModal(e as any);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.length - 2 }} />
|
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.length - 2 }} />
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -68,7 +68,10 @@ interface IStatusFollowedTagInfo {
|
|||||||
const StatusFollowedTagInfo: React.FC<IStatusFollowedTagInfo> = ({ status, avatarSize }) => {
|
const StatusFollowedTagInfo: React.FC<IStatusFollowedTagInfo> = ({ status, avatarSize }) => {
|
||||||
const { data: followedTags } = useFollowedTags();
|
const { data: followedTags } = useFollowedTags();
|
||||||
|
|
||||||
const filteredTags = status.tags.filter(tag => followedTags?.some(followed => followed.name.toLowerCase() === tag.name.toLowerCase()));
|
const filteredTags = useMemo(
|
||||||
|
() => status.tags.filter(tag => followedTags?.some(followed => followed.name.toLowerCase() === tag.name.toLowerCase())),
|
||||||
|
[status.tags, followedTags],
|
||||||
|
);
|
||||||
|
|
||||||
if (!filteredTags.length) {
|
if (!filteredTags.length) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -88,7 +88,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||||||
localTranslationAvailability(status, intl.locale).then((availability) => {
|
localTranslationAvailability(status, intl.locale).then((availability) => {
|
||||||
setLocalTranslate(availability === 'unavailable' ? false : availability);
|
setLocalTranslate(availability === 'unavailable' ? false : availability);
|
||||||
if (availability) setLanguageModelAvailability(status.language!, intl.locale, availability);
|
if (availability) setLanguageModelAvailability(status.language!, intl.locale, availability);
|
||||||
}).catch(() => {});
|
}).catch(() => setLocalTranslate(false));
|
||||||
}, [status.language, intl.locale]);
|
}, [status.language, intl.locale]);
|
||||||
|
|
||||||
const handleTranslate: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
const handleTranslate: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useCallback, useRef } from 'react';
|
import React, { useCallback, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { changeMediaOrder } from '@/actions/compose';
|
import { changeMediaOrder } from '@/actions/compose';
|
||||||
import HStack from '@/components/ui/hstack';
|
import HStack from '@/components/ui/hstack';
|
||||||
@ -19,7 +19,7 @@ const UploadForm: React.FC<IUploadForm> = ({ composeId, onSubmit }) => {
|
|||||||
|
|
||||||
const { isUploading, mediaAttachments } = useCompose(composeId);
|
const { isUploading, mediaAttachments } = useCompose(composeId);
|
||||||
|
|
||||||
const mediaIds = mediaAttachments.map((item) => item.id);
|
const mediaIds = useMemo(() => mediaAttachments.map((item) => item.id), [mediaAttachments]);
|
||||||
|
|
||||||
const dragItem = useRef<string | null>();
|
const dragItem = useRef<string | null>();
|
||||||
const dragOverItem = useRef<string | null>();
|
const dragOverItem = useRef<string | null>();
|
||||||
|
|||||||
@ -19,9 +19,10 @@ const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit, onDr
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { pleroma: { metadata: { description_limit: descriptionLimit } } } = useInstance();
|
const { pleroma: { metadata: { description_limit: descriptionLimit } } } = useInstance();
|
||||||
|
|
||||||
const media = useCompose(composeId).mediaAttachments.find(item => item.id === id)!;
|
const media = useCompose(composeId).mediaAttachments.find(item => item.id === id);
|
||||||
|
|
||||||
const handleDescriptionChange = (description: string, position?: [number, number]) => {
|
const handleDescriptionChange = (description: string, position?: [number, number]) => {
|
||||||
|
if (!media) return;
|
||||||
return dispatch(changeUploadCompose(composeId, media.id, {
|
return dispatch(changeUploadCompose(composeId, media.id, {
|
||||||
description,
|
description,
|
||||||
focus: position ? `${((position[0] - 0.5) * 2).toFixed(2)},${((position[1] - 0.5) * -2).toFixed(2)}` : undefined,
|
focus: position ? `${((position[0] - 0.5) * 2).toFixed(2)},${((position[1] - 0.5) * -2).toFixed(2)}` : undefined,
|
||||||
@ -29,6 +30,7 @@ const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit, onDr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
|
if (!media) return;
|
||||||
dispatch(undoUploadCompose(composeId, media.id));
|
dispatch(undoUploadCompose(composeId, media.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,6 +42,8 @@ const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit, onDr
|
|||||||
onDragEnter(id);
|
onDragEnter(id);
|
||||||
}, [onDragEnter, id]);
|
}, [onDragEnter, id]);
|
||||||
|
|
||||||
|
if (!media) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Upload
|
<Upload
|
||||||
media={media}
|
media={media}
|
||||||
|
|||||||
@ -18,11 +18,14 @@ const Conversation: React.FC<IConversation> = ({ conversationId, onMoveUp, onMov
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const conversation = useAppSelector((state) =>
|
const conversation = useAppSelector((state) =>
|
||||||
state.conversations.items.find(x => x.id === conversationId)!,
|
state.conversations.items.find(x => x.id === conversationId),
|
||||||
);
|
);
|
||||||
const accounts = useAppSelector((state) =>
|
const accounts = useAppSelector((state) =>
|
||||||
conversation.accounts.map((accountId: string) => selectAccount(state, accountId)!),
|
conversation?.accounts.map((accountId: string) => selectAccount(state, accountId)!) ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!conversation) return null;
|
||||||
|
|
||||||
const unread = conversation.unread;
|
const unread = conversation.unread;
|
||||||
const lastStatusId = conversation.last_status;
|
const lastStatusId = conversation.last_status;
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import genericIcon from 'cryptocurrency-icons/svg/color/generic.svg';
|
import genericIcon from 'cryptocurrency-icons/svg/color/generic.svg';
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
/** Get crypto icon URL by ticker symbol, or fall back to generic icon */
|
const modules: Record<string, () => Promise<any>> = import.meta.glob(
|
||||||
const getIcon = (ticker: string): string => {
|
'../../../../node_modules/cryptocurrency-icons/svg/color/*.svg',
|
||||||
const modules: Record<string, any> = import.meta.glob('../../../../node_modules/cryptocurrency-icons/svg/color/*.svg', { eager: true });
|
);
|
||||||
const key = `../../../../node_modules/cryptocurrency-icons/svg/color/${ticker}.svg`;
|
|
||||||
return modules[key]?.default || genericIcon;
|
const iconCache: Record<string, string> = {};
|
||||||
};
|
|
||||||
|
|
||||||
interface ICryptoIcon {
|
interface ICryptoIcon {
|
||||||
ticker: string;
|
ticker: string;
|
||||||
@ -15,14 +14,26 @@ interface ICryptoIcon {
|
|||||||
imgClassName?: string;
|
imgClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CryptoIcon: React.FC<ICryptoIcon> = ({ ticker, title, className, imgClassName }): JSX.Element => (
|
const CryptoIcon: React.FC<ICryptoIcon> = ({ ticker, title, className, imgClassName }) => {
|
||||||
|
const [icon, setIcon] = useState<string>(iconCache[ticker] || genericIcon);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const key = `../../../../node_modules/cryptocurrency-icons/svg/color/${ticker}.svg`;
|
||||||
|
if (iconCache[ticker]) {
|
||||||
|
setIcon(iconCache[ticker]);
|
||||||
|
} else if (modules[key]) {
|
||||||
|
modules[key]().then((mod) => {
|
||||||
|
iconCache[ticker] = mod.default;
|
||||||
|
setIcon(mod.default);
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}, [ticker]);
|
||||||
|
|
||||||
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<img
|
<img className={imgClassName} src={icon} alt={title || ticker} />
|
||||||
className={imgClassName}
|
|
||||||
src={getIcon(ticker)}
|
|
||||||
alt={title || ticker}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export { CryptoIcon as default };
|
export { CryptoIcon as default };
|
||||||
|
|||||||
@ -331,8 +331,8 @@ const Preferences = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{features.emojiReacts && (
|
{features.emojiReacts && (
|
||||||
<ListItem label={<FormattedMessage id='preferences.fields.wrench_label' defaultMessage='Display wrench reaction button' />} >
|
<ListItem label={<FormattedMessage id='preferences.fields.eggplant_label' defaultMessage='Display eggplant reaction button' />} >
|
||||||
<SettingToggle settings={settings} settingPath={['showWrenchButton']} onChange={onToggleChange} />
|
<SettingToggle settings={settings} settingPath={['showEggplantButton']} onChange={onToggleChange} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import * as BuildConfig from '@/build-config';
|
|||||||
import PlFe from '@/init/pl-fe';
|
import PlFe from '@/init/pl-fe';
|
||||||
import { printConsoleWarning } from '@/utils/console';
|
import { printConsoleWarning } from '@/utils/console';
|
||||||
|
|
||||||
import '@fontsource/inter/200.css';
|
|
||||||
import '@fontsource/inter/300.css';
|
|
||||||
import '@fontsource/inter/400.css';
|
import '@fontsource/inter/400.css';
|
||||||
import '@fontsource/inter/500.css';
|
import '@fontsource/inter/500.css';
|
||||||
import '@fontsource/inter/600.css';
|
import '@fontsource/inter/600.css';
|
||||||
@ -19,7 +17,6 @@ import '@fontsource/inter/700.css';
|
|||||||
import '@fontsource/inter/900.css';
|
import '@fontsource/inter/900.css';
|
||||||
import '@fontsource/roboto-mono/400.css';
|
import '@fontsource/roboto-mono/400.css';
|
||||||
import 'line-awesome/dist/font-awesome-line-awesome/css/all.css';
|
import 'line-awesome/dist/font-awesome-line-awesome/css/all.css';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
|
||||||
|
|
||||||
import './styles/i18n.css';
|
import './styles/i18n.css';
|
||||||
import './styles/application.scss';
|
import './styles/application.scss';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import Button from '@/components/ui/button';
|
import Button from '@/components/ui/button';
|
||||||
@ -46,7 +46,8 @@ const OtherActionsStep = ({
|
|||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const statusIds = useAppSelector((state) => [...new Set([...state.timelines[`account:${account.id}:with_replies`]!.items, ...selectedStatusIds])]);
|
const timelineItems = useAppSelector((state) => state.timelines[`account:${account.id}:with_replies`]?.items);
|
||||||
|
const statusIds = useMemo(() => [...new Set([...(timelineItems || []), ...selectedStatusIds])], [timelineItems, selectedStatusIds]);
|
||||||
const isBlocked = block;
|
const isBlocked = block;
|
||||||
const isForward = forward;
|
const isForward = forward;
|
||||||
const canForward = !account.local && features.federating;
|
const canForward = !account.local && features.federating;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { eventDiscussionCompose } from '@/actions/compose';
|
import { eventDiscussionCompose } from '@/actions/compose';
|
||||||
@ -32,7 +32,8 @@ const EventDiscussionPage: React.FC = () => {
|
|||||||
|
|
||||||
const me = useAppSelector((state) => state.me);
|
const me = useAppSelector((state) => state.me);
|
||||||
|
|
||||||
const descendantsIds = useAppSelector(state => getDescendantsIds(state, statusId).filter(id => id !== statusId));
|
const allDescendantsIds = useAppSelector(state => getDescendantsIds(state, statusId));
|
||||||
|
const descendantsIds = useMemo(() => allDescendantsIds.filter(id => id !== statusId), [allDescendantsIds, statusId]);
|
||||||
|
|
||||||
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
|
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user