From ccffa11f2be5df500e6a69fdc6c0c2288fa85427 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Thu, 15 May 2025 10:07:38 +0200 Subject: [PATCH] refactor: Remove duplicated `AvatarGroup` CSS and familiar followers cleanup (#34681) --- .../components/__tests__/avatar-test.jsx | 6 ++-- app/javascript/mastodon/components/avatar.tsx | 30 ++++++++++++---- .../mastodon/components/avatar_group.tsx | 31 ++++------------ .../components/familiar_followers.tsx | 24 ++++++++----- .../mastodon/features/lists/members.tsx | 13 ++++--- .../mastodon/features/lists/new.tsx | 35 +++++++++++-------- .../notification_group_with_status.tsx | 22 ++++++++---- app/javascript/mastodon/locales/en.json | 1 - .../styles/mastodon/components.scss | 14 ++++++-- app/javascript/styles/mastodon/forms.scss | 34 ++++-------------- 10 files changed, 110 insertions(+), 100 deletions(-) diff --git a/app/javascript/mastodon/components/__tests__/avatar-test.jsx b/app/javascript/mastodon/components/__tests__/avatar-test.jsx index 21c3ae5800..fc87c0bf72 100644 --- a/app/javascript/mastodon/components/__tests__/avatar-test.jsx +++ b/app/javascript/mastodon/components/__tests__/avatar-test.jsx @@ -1,11 +1,13 @@ -import { fromJS } from 'immutable'; import renderer from 'react-test-renderer'; +import { accountDefaultValues, createAccountFromServerJSON } from '@/mastodon/models/account'; + import { Avatar } from '../avatar'; describe('', () => { - const account = fromJS({ + const account = createAccountFromServerJSON({ + ...accountDefaultValues, username: 'alice', acct: 'alice', display_name: 'Alice', diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx index a2dc0b782e..fb331813a9 100644 --- a/app/javascript/mastodon/components/avatar.tsx +++ b/app/javascript/mastodon/components/avatar.tsx @@ -1,17 +1,21 @@ import { useState, useCallback } from 'react'; import classNames from 'classnames'; +import { Link } from 'react-router-dom'; import { useHovering } from 'mastodon/hooks/useHovering'; import { autoPlayGif } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; interface Props { - account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there - size: number; + account: + | Pick + | undefined; // FIXME: remove `undefined` once we know for sure its always there + size?: number; style?: React.CSSProperties; inline?: boolean; animate?: boolean; + withLink?: boolean; counter?: number | string; counterBorderColor?: string; } @@ -21,6 +25,7 @@ export const Avatar: React.FC = ({ animate = autoPlayGif, size = 20, inline = false, + withLink = false, style: styleFromParent, counter, counterBorderColor, @@ -35,10 +40,7 @@ export const Avatar: React.FC = ({ height: `${size}px`, }; - const src = - hovering || animate - ? account?.get('avatar') - : account?.get('avatar_static'); + const src = hovering || animate ? account?.avatar : account?.avatar_static; const handleLoad = useCallback(() => { setLoading(false); @@ -48,7 +50,7 @@ export const Avatar: React.FC = ({ setError(true); }, [setError]); - return ( + const avatar = (
= ({ )}
); + + if (withLink) { + return ( + + {avatar} + + ); + } + + return avatar; }; diff --git a/app/javascript/mastodon/components/avatar_group.tsx b/app/javascript/mastodon/components/avatar_group.tsx index 4f583defcf..2420728542 100644 --- a/app/javascript/mastodon/components/avatar_group.tsx +++ b/app/javascript/mastodon/components/avatar_group.tsx @@ -1,34 +1,17 @@ import classNames from 'classnames'; -import { Link } from 'react-router-dom'; -import { Avatar } from 'mastodon/components/avatar'; -import { useAppSelector } from 'mastodon/store'; - -const AvatarWrapper: React.FC<{ accountId: string }> = ({ accountId }) => { - const account = useAppSelector((state) => state.accounts.get(accountId)); - - if (!account) return null; - - return ( - - - - ); -}; +/** + * Wrapper for displaying a number of Avatar components horizontally, + * either spaced out (default) or overlapping (using the `compact` prop). + */ export const AvatarGroup: React.FC<{ - accountIds: string[]; compact?: boolean; -}> = ({ accountIds, compact = false }) => ( + children: React.ReactNode; +}> = ({ children, compact = false }) => (
- {accountIds.map((accountId) => ( - - ))} + {children}
); diff --git a/app/javascript/mastodon/features/account_timeline/components/familiar_followers.tsx b/app/javascript/mastodon/features/account_timeline/components/familiar_followers.tsx index b3b97c317b..1beaceabb4 100644 --- a/app/javascript/mastodon/features/account_timeline/components/familiar_followers.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/familiar_followers.tsx @@ -5,16 +5,21 @@ import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; import { fetchAccountsFamiliarFollowers } from '@/mastodon/actions/accounts_familiar_followers'; +import { Avatar } from '@/mastodon/components/avatar'; import { AvatarGroup } from '@/mastodon/components/avatar_group'; import type { Account } from '@/mastodon/models/account'; import { getAccountFamiliarFollowers } from '@/mastodon/selectors/accounts'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; -const AccountLink: React.FC<{ account?: Account }> = ({ account }) => ( - - {account?.display_name} - -); +const AccountLink: React.FC<{ account?: Account }> = ({ account }) => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const name = account?.display_name || `@${account?.acct}`; + return ( + + {name} + + ); +}; const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({ familiarFollowers, @@ -74,10 +79,11 @@ export const FamiliarFollowers: React.FC<{ accountId: string }> = ({ return (
- account.id)} - /> + + {familiarFollowers.map((account) => ( + + ))} +
); diff --git a/app/javascript/mastodon/features/lists/members.tsx b/app/javascript/mastodon/features/lists/members.tsx index 41d02ad9fc..e31943b78b 100644 --- a/app/javascript/mastodon/features/lists/members.tsx +++ b/app/javascript/mastodon/features/lists/members.tsx @@ -35,8 +35,11 @@ import { VerifiedBadge } from 'mastodon/components/verified_badge'; import { me } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -const messages = defineMessages({ - heading: { id: 'column.list_members', defaultMessage: 'Manage list members' }, +export const messages = defineMessages({ + manageMembers: { + id: 'column.list_members', + defaultMessage: 'Manage list members', + }, placeholder: { id: 'lists.search', defaultMessage: 'Search', @@ -255,10 +258,10 @@ const ListMembers: React.FC<{ return ( - {intl.formatMessage(messages.heading)} + {intl.formatMessage(messages.manageMembers)} diff --git a/app/javascript/mastodon/features/lists/new.tsx b/app/javascript/mastodon/features/lists/new.tsx index 100f126c37..8253ab58b7 100644 --- a/app/javascript/mastodon/features/lists/new.tsx +++ b/app/javascript/mastodon/features/lists/new.tsx @@ -9,16 +9,23 @@ import { isFulfilled } from '@reduxjs/toolkit'; import Toggle from 'react-toggle'; +import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import { fetchList } from 'mastodon/actions/lists'; import { createList, updateList } from 'mastodon/actions/lists_typed'; import { apiGetAccounts } from 'mastodon/api/lists'; +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { RepliesPolicyType } from 'mastodon/api_types/lists'; +import { Avatar } from 'mastodon/components/avatar'; +import { AvatarGroup } from 'mastodon/components/avatar_group'; import { Column } from 'mastodon/components/column'; import { ColumnHeader } from 'mastodon/components/column_header'; +import { Icon } from 'mastodon/components/icon'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; +import { messages as membersMessages } from './members'; + const messages = defineMessages({ edit: { id: 'column.edit_list', defaultMessage: 'Edit list' }, create: { id: 'column.create_list', defaultMessage: 'Create list' }, @@ -27,42 +34,40 @@ const messages = defineMessages({ const MembersLink: React.FC<{ id: string; }> = ({ id }) => { - const [count, setCount] = useState(0); - const [avatars, setAvatars] = useState([]); + const intl = useIntl(); + const [avatarCount, setAvatarCount] = useState(0); + const [avatarAccounts, setAvatarAccounts] = useState([]); useEffect(() => { void apiGetAccounts(id) .then((data) => { - setCount(data.length); - setAvatars(data.slice(0, 3).map((a) => a.avatar)); - return ''; + setAvatarCount(data.length); + setAvatarAccounts(data.slice(0, 3)); }) .catch(() => { // Nothing }); - }, [id, setCount, setAvatars]); + }, [id]); return (
- + {intl.formatMessage(membersMessages.manageMembers)} +
-
- {avatars.map((url) => ( - + + {avatarAccounts.map((a) => ( + ))} -
+ ); }; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx index cbb0b85f1d..b0b86eead1 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx @@ -7,6 +7,7 @@ import { HotKeys } from 'react-hotkeys'; import { replyComposeById } from 'mastodon/actions/compose'; import { navigateToStatus } from 'mastodon/actions/statuses'; +import { Avatar } from 'mastodon/components/avatar'; import { AvatarGroup } from 'mastodon/components/avatar_group'; import type { IconProp } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon'; @@ -17,6 +18,14 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { DisplayedName } from './displayed_name'; import { EmbeddedStatus } from './embedded_status'; +export const AvatarById: React.FC<{ accountId: string }> = ({ accountId }) => { + const account = useAppSelector((state) => state.accounts.get(accountId)); + + if (!account) return null; + + return ; +}; + export type LabelRenderer = ( displayedName: JSX.Element, total: number, @@ -99,12 +108,13 @@ export const NotificationGroupWithStatus: React.FC<{
- + + {accountIds + .slice(0, NOTIFICATIONS_GROUP_MAX_AVATARS) + .map((id) => ( + + ))} + {actions && (
{actions}
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 45bc6109b7..2d49346ac7 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -518,7 +518,6 @@ "lists.exclusive": "Hide members in Home", "lists.exclusive_hint": "If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.", "lists.find_users_to_add": "Find users to add", - "lists.list_members": "List members", "lists.list_members_count": "{count, plural, one {# member} other {# members}}", "lists.list_name": "List name", "lists.new_list_name": "New list name", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index ea48d98ec6..f3271b2d3b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2179,10 +2179,18 @@ a .account__avatar { & > :not(:first-child) { margin-inline-start: -8px; + } - .account__avatar { - box-shadow: 0 0 0 2px var(--background-color); - } + & > :first-child { + transform: rotate(-4deg); + } + + & > :nth-child(2) { + transform: rotate(-2deg); + } + + .account__avatar { + box-shadow: 0 0 0 2px var(--background-color); } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 6ec6a4199f..5dbe4f9e08 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -1440,34 +1440,12 @@ code { display: block; color: $primary-text-color; } - } - } -} - -.avatar-pile { - display: flex; - align-items: center; - - img { - display: block; - border-radius: 8px; - width: 32px; - height: 32px; - border: 2px solid var(--background-color); - background: var(--surface-background-color); - margin-inline-end: -16px; - transform: rotate(0); - - &:first-child { - transform: rotate(-4deg); - } - - &:nth-child(2) { - transform: rotate(-2deg); - } - - &:last-child { - margin-inline-end: 0; + + .icon { + vertical-align: -5px; + width: 20px; + height: 20px; + } } } }