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) {
|
||||
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 }} />
|
||||
</span>,
|
||||
);
|
||||
|
||||
@ -68,7 +68,10 @@ interface IStatusFollowedTagInfo {
|
||||
const StatusFollowedTagInfo: React.FC<IStatusFollowedTagInfo> = ({ status, avatarSize }) => {
|
||||
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) {
|
||||
return null;
|
||||
|
||||
@ -88,7 +88,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||
localTranslationAvailability(status, intl.locale).then((availability) => {
|
||||
setLocalTranslate(availability === 'unavailable' ? false : availability);
|
||||
if (availability) setLanguageModelAvailability(status.language!, intl.locale, availability);
|
||||
}).catch(() => {});
|
||||
}).catch(() => setLocalTranslate(false));
|
||||
}, [status.language, intl.locale]);
|
||||
|
||||
const handleTranslate: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
import { changeMediaOrder } from '@/actions/compose';
|
||||
import HStack from '@/components/ui/hstack';
|
||||
@ -19,7 +19,7 @@ const UploadForm: React.FC<IUploadForm> = ({ composeId, onSubmit }) => {
|
||||
|
||||
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 dragOverItem = useRef<string | null>();
|
||||
|
||||
@ -19,9 +19,10 @@ const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit, onDr
|
||||
const dispatch = useAppDispatch();
|
||||
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]) => {
|
||||
if (!media) return;
|
||||
return dispatch(changeUploadCompose(composeId, media.id, {
|
||||
description,
|
||||
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 = () => {
|
||||
if (!media) return;
|
||||
dispatch(undoUploadCompose(composeId, media.id));
|
||||
};
|
||||
|
||||
@ -40,6 +42,8 @@ const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit, onDr
|
||||
onDragEnter(id);
|
||||
}, [onDragEnter, id]);
|
||||
|
||||
if (!media) return null;
|
||||
|
||||
return (
|
||||
<Upload
|
||||
media={media}
|
||||
|
||||
@ -18,11 +18,14 @@ const Conversation: React.FC<IConversation> = ({ conversationId, onMoveUp, onMov
|
||||
const navigate = useNavigate();
|
||||
|
||||
const conversation = useAppSelector((state) =>
|
||||
state.conversations.items.find(x => x.id === conversationId)!,
|
||||
state.conversations.items.find(x => x.id === conversationId),
|
||||
);
|
||||
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 lastStatusId = conversation.last_status;
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
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 getIcon = (ticker: string): string => {
|
||||
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 modules: Record<string, () => Promise<any>> = import.meta.glob(
|
||||
'../../../../node_modules/cryptocurrency-icons/svg/color/*.svg',
|
||||
);
|
||||
|
||||
const iconCache: Record<string, string> = {};
|
||||
|
||||
interface ICryptoIcon {
|
||||
ticker: string;
|
||||
@ -15,14 +14,26 @@ interface ICryptoIcon {
|
||||
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}>
|
||||
<img
|
||||
className={imgClassName}
|
||||
src={getIcon(ticker)}
|
||||
alt={title || ticker}
|
||||
/>
|
||||
<img className={imgClassName} src={icon} alt={title || ticker} />
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export { CryptoIcon as default };
|
||||
|
||||
@ -331,8 +331,8 @@ const Preferences = () => {
|
||||
</ListItem>
|
||||
|
||||
{features.emojiReacts && (
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.wrench_label' defaultMessage='Display wrench reaction button' />} >
|
||||
<SettingToggle settings={settings} settingPath={['showWrenchButton']} onChange={onToggleChange} />
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.eggplant_label' defaultMessage='Display eggplant reaction button' />} >
|
||||
<SettingToggle settings={settings} settingPath={['showEggplantButton']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
|
||||
@ -10,8 +10,6 @@ import * as BuildConfig from '@/build-config';
|
||||
import PlFe from '@/init/pl-fe';
|
||||
import { printConsoleWarning } from '@/utils/console';
|
||||
|
||||
import '@fontsource/inter/200.css';
|
||||
import '@fontsource/inter/300.css';
|
||||
import '@fontsource/inter/400.css';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/600.css';
|
||||
@ -19,7 +17,6 @@ import '@fontsource/inter/700.css';
|
||||
import '@fontsource/inter/900.css';
|
||||
import '@fontsource/roboto-mono/400.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/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 Button from '@/components/ui/button';
|
||||
@ -46,7 +46,8 @@ const OtherActionsStep = ({
|
||||
const features = useFeatures();
|
||||
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 isForward = forward;
|
||||
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 { eventDiscussionCompose } from '@/actions/compose';
|
||||
@ -32,7 +32,8 @@ const EventDiscussionPage: React.FC = () => {
|
||||
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user