[Glitch] Update collection account item design
Port 7d9b1e6d1edcadcbe8a62ede3d94860be785be13 to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
2a5a64d057
commit
2ee6e447b4
@ -20,6 +20,7 @@ import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { apiFollowAccount } from 'flavours/glitch/api/accounts';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { VerifiedBadge } from 'flavours/glitch/components/badge';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { FollowersCounter } from 'flavours/glitch/components/counters';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
@ -28,7 +29,6 @@ import { FollowButton } from 'flavours/glitch/components/follow_button';
|
||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { Skeleton } from 'flavours/glitch/components/skeleton';
|
||||
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
|
||||
import { useIdentity } from 'flavours/glitch/identity_context';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import type { MenuItem } from 'flavours/glitch/models/dropdown_menu';
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { accountFactoryState } from '@/testing/factories';
|
||||
|
||||
import { AccountListItem } from './index';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/AccountListItem',
|
||||
component: AccountListItem,
|
||||
args: {
|
||||
accountId: '1',
|
||||
withBorder: false,
|
||||
},
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState(),
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof AccountListItem>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithCustomStats: Story = {
|
||||
args: {
|
||||
stats: ['posts', 'last-active'],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithBorder: Story = {
|
||||
args: {
|
||||
withBorder: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutButton: Story = {
|
||||
args: {
|
||||
renderButton: () => null,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,184 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Account } from 'flavours/glitch/components/account';
|
||||
import { VerifiedBadge } from 'flavours/glitch/components/badge';
|
||||
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
||||
import { useRelationship } from 'flavours/glitch/hooks/useRelationship';
|
||||
import type { Relationship } from 'flavours/glitch/models/relationship';
|
||||
|
||||
import { EmojiHTML } from '../emoji/html';
|
||||
import { FamiliarFollowers } from '../familiar_followers';
|
||||
import { FollowButton } from '../follow_button';
|
||||
import { FormattedDateWrapper } from '../formatted_date';
|
||||
import { NumberFields, NumberFieldsItem } from '../number_fields';
|
||||
import { RelativeTimestamp } from '../relative_timestamp';
|
||||
import { ShortNumber } from '../short_number';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export interface RenderButtonOptions {
|
||||
accountId: string | undefined;
|
||||
relationship: Relationship | null | undefined;
|
||||
}
|
||||
|
||||
type Stat = 'followers' | 'following' | 'posts' | 'joined' | 'last-active';
|
||||
|
||||
interface Props {
|
||||
accountId: string | undefined;
|
||||
stats?: Stat[];
|
||||
renderButton?: (options: RenderButtonOptions) => React.ReactNode;
|
||||
withBorder?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_STATS: Stat[] = ['followers', 'following', 'joined'];
|
||||
|
||||
/**
|
||||
* Extended account list item with bio, verified link badge,
|
||||
* and familiar follower widget.
|
||||
*
|
||||
* The displayed account stats can be customised using the `stats` prop,
|
||||
* and button rendering can be customised via the `renderButton` prop.
|
||||
*/
|
||||
export const AccountListItem: React.FC<Props> = ({
|
||||
accountId,
|
||||
stats = DEFAULT_STATS,
|
||||
withBorder = true,
|
||||
renderButton = defaultRenderButton,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const account = useAccount(accountId);
|
||||
const relationship = useRelationship(accountId);
|
||||
|
||||
const createdThisYear = useMemo(
|
||||
() => account?.created_at.includes(new Date().getFullYear().toString()),
|
||||
[account?.created_at],
|
||||
);
|
||||
|
||||
if (!accountId || !account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstVerifiedField = account.fields.find((item) => !!item.verified_at);
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper} data-with-border={withBorder}>
|
||||
<div className={classes.header}>
|
||||
<Account
|
||||
id={accountId}
|
||||
minimal
|
||||
size={40}
|
||||
withMenu={false}
|
||||
withBorder={false}
|
||||
className={classes.account}
|
||||
/>
|
||||
|
||||
{renderButton({ accountId, relationship })}
|
||||
</div>
|
||||
|
||||
<NumberFields>
|
||||
{stats.includes('followers') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.followers'
|
||||
defaultMessage='Followers'
|
||||
/>
|
||||
}
|
||||
hint={intl.formatNumber(account.followers_count)}
|
||||
>
|
||||
<ShortNumber value={account.followers_count} />
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{stats.includes('following') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.following'
|
||||
defaultMessage='Following'
|
||||
/>
|
||||
}
|
||||
hint={intl.formatNumber(account.following_count)}
|
||||
link={`/@${account.acct}/following`}
|
||||
>
|
||||
<ShortNumber value={account.following_count} />
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{stats.includes('posts') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||
}
|
||||
hint={intl.formatNumber(account.statuses_count)}
|
||||
>
|
||||
<ShortNumber value={account.statuses_count} />
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{stats.includes('joined') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.joined_short'
|
||||
defaultMessage='Joined'
|
||||
/>
|
||||
}
|
||||
hint={intl.formatDate(account.created_at)}
|
||||
>
|
||||
{createdThisYear ? (
|
||||
<FormattedDateWrapper
|
||||
value={account.created_at}
|
||||
month='short'
|
||||
day='2-digit'
|
||||
/>
|
||||
) : (
|
||||
<FormattedDateWrapper value={account.created_at} year='numeric' />
|
||||
)}
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{stats.includes('last-active') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.last_active'
|
||||
defaultMessage='Last active'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RelativeTimestamp
|
||||
long
|
||||
timestamp={account.last_status_at}
|
||||
noFuture
|
||||
/>
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{firstVerifiedField && (
|
||||
<VerifiedBadge
|
||||
link={firstVerifiedField.value}
|
||||
className={classes.verifiedBadge}
|
||||
/>
|
||||
)}
|
||||
</NumberFields>
|
||||
<FamiliarFollowers accountId={accountId} />
|
||||
{account.note.length > 0 && (
|
||||
<EmojiHTML
|
||||
className={classNames(classes.bio, 'translate')}
|
||||
htmlString={account.note_emojified}
|
||||
extraEmojis={account.emojis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultRenderButton = ({ accountId }: RenderButtonOptions) => (
|
||||
<AccountListItemFollowButton accountId={accountId} />
|
||||
);
|
||||
|
||||
export const AccountListItemFollowButton: React.FC<{
|
||||
accountId: string | undefined;
|
||||
}> = ({ accountId }) => (
|
||||
<FollowButton compact labelLength='short' accountId={accountId} />
|
||||
);
|
||||
@ -0,0 +1,50 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
|
||||
&[data-with-border='true'] {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
:global(.account__note) {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account {
|
||||
--account-name-size: 15px;
|
||||
--account-handle-color: var(--color-text-secondary);
|
||||
--account-handle-size: 13px;
|
||||
--account-bio-color: var(--color-text-primary);
|
||||
--account-bio-size: 13px;
|
||||
--account-outer-spacing: 0;
|
||||
|
||||
flex-grow: 1;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
.verifiedBadge {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.bio {
|
||||
:any-link {
|
||||
color: var(--color-text-status-links);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,12 @@ export const Domain: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Verified: Story = {
|
||||
render() {
|
||||
return <badges.VerifiedBadge link='example.com' />;
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomIcon: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
|
||||
@ -4,19 +4,23 @@ import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { OnAttributeHandler } from '@/flavours/glitch/utils/html';
|
||||
import AdminIcon from '@/images/icons/icon_admin.svg?react';
|
||||
import IconVerified from '@/images/icons/icon_verified.svg?react';
|
||||
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
|
||||
import GroupsIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
|
||||
import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react';
|
||||
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
|
||||
|
||||
import { EmojiHTML } from '../emoji/html';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
interface BadgeProps {
|
||||
interface BadgeProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
label: ReactNode;
|
||||
icon?: ReactNode;
|
||||
className?: string;
|
||||
domain?: ReactNode;
|
||||
roleId?: string;
|
||||
variant?:
|
||||
@ -35,9 +39,16 @@ export const Badge: FC<BadgeProps> = ({
|
||||
className,
|
||||
domain,
|
||||
roleId,
|
||||
...otherProps
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(classes.badge, classes[variant], className)}
|
||||
{...otherProps}
|
||||
className={classNames(
|
||||
classes.badge,
|
||||
!icon && classes.badgeWithoutIcon,
|
||||
classes[variant],
|
||||
className,
|
||||
)}
|
||||
data-account-role-id={roleId}
|
||||
>
|
||||
{icon}
|
||||
@ -134,3 +145,31 @@ export const BlockedBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
||||
if (name === 'rel' && tagName === 'a') {
|
||||
if (value === 'me') {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
name,
|
||||
value
|
||||
.split(' ')
|
||||
.filter((x) => x !== 'me')
|
||||
.join(' '),
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const VerifiedBadge: React.FC<{ link: string; className?: string }> = ({
|
||||
link,
|
||||
className,
|
||||
}) => (
|
||||
<Badge
|
||||
variant='success'
|
||||
icon={<Icon id='verified' icon={IconVerified} noFill />}
|
||||
label={<EmojiHTML as='span' htmlString={link} onAttribute={onAttribute} />}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -2,18 +2,28 @@
|
||||
color: var(--color-text-primary);
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
padding: 4px;
|
||||
padding-inline-end: 8px;
|
||||
gap: 4px;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
> svg {
|
||||
width: auto;
|
||||
height: 15px;
|
||||
height: 17px;
|
||||
fill: currentColor;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:not(.badgeWithoutIcon) {
|
||||
padding-inline-end: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.domain {
|
||||
@ -41,7 +51,6 @@
|
||||
|
||||
.success {
|
||||
background-color: var(--color-bg-success-softest);
|
||||
color: var(--color-text-success);
|
||||
}
|
||||
|
||||
.warning {
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Avatar } from '@/flavours/glitch/components/avatar';
|
||||
import { AvatarGroup } from '@/flavours/glitch/components/avatar_group';
|
||||
import { LinkedDisplayName } from '@/flavours/glitch/components/display_name';
|
||||
import type { Account } from '@/flavours/glitch/models/account';
|
||||
|
||||
import { useFetchFamiliarFollowers } from '../hooks/familiar_followers';
|
||||
import classes from './styles.module.scss';
|
||||
import { useFetchFamiliarFollowers } from './use_fetch_familiar_followers';
|
||||
|
||||
const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({
|
||||
familiarFollowers,
|
||||
@ -51,9 +54,10 @@ const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({
|
||||
}
|
||||
};
|
||||
|
||||
export const FamiliarFollowers: React.FC<{ accountId: string }> = ({
|
||||
accountId,
|
||||
}) => {
|
||||
export const FamiliarFollowers: React.FC<{
|
||||
accountId: string;
|
||||
className?: string;
|
||||
}> = ({ accountId, className }) => {
|
||||
const { familiarFollowers, isLoading } = useFetchFamiliarFollowers({
|
||||
accountId,
|
||||
});
|
||||
@ -63,10 +67,10 @@ export const FamiliarFollowers: React.FC<{ accountId: string }> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account__header__familiar-followers'>
|
||||
<AvatarGroup compact>
|
||||
<div className={classNames(classes.wrapper, className)}>
|
||||
<AvatarGroup compact avatarHeight={24}>
|
||||
{familiarFollowers.slice(0, 3).map((account) => (
|
||||
<Avatar withLink key={account.id} account={account} size={28} />
|
||||
<Avatar withLink key={account.id} account={account} size={24} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
<span>
|
||||
@ -0,0 +1,11 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
a:any-link {
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useFetchFamiliarFollowers } from '@/flavours/glitch/components/familiar_followers/use_fetch_familiar_followers';
|
||||
import { fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { AccountBio } from 'flavours/glitch/components/account_bio';
|
||||
import { AccountFields } from 'flavours/glitch/components/account_fields';
|
||||
@ -18,7 +19,6 @@ import { FollowButton } from 'flavours/glitch/components/follow_button';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { useFetchFamiliarFollowers } from 'flavours/glitch/features/account_timeline/hooks/familiar_followers';
|
||||
import { domain } from 'flavours/glitch/initial_state';
|
||||
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
@ -1,22 +1,21 @@
|
||||
import classNames from 'classnames';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import type { MastodonLocationDescriptor } from 'flavours/glitch/components/router';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
interface WrapperProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const NumberFields: React.FC<WrapperProps> = ({ children }) => {
|
||||
return <ul className={classes.list}>{children}</ul>;
|
||||
export const NumberFields: React.FC<React.ComponentPropsWithoutRef<'ul'>> = ({
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return <ul className={classNames(classes.list, className)}>{children}</ul>;
|
||||
};
|
||||
|
||||
interface ItemProps {
|
||||
interface ItemProps extends React.ComponentPropsWithoutRef<'li'> {
|
||||
label: React.ReactNode;
|
||||
hint?: string;
|
||||
link?: MastodonLocationDescriptor;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const NumberFieldsItem: React.FC<ItemProps> = ({
|
||||
@ -24,9 +23,10 @@ export const NumberFieldsItem: React.FC<ItemProps> = ({
|
||||
hint,
|
||||
link,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<li className={classes.item} title={hint}>
|
||||
<li className={classNames(classes.item, className)} title={hint}>
|
||||
{label}
|
||||
{link ? (
|
||||
<NavLink exact to={link}>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
.list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 8px 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 4px 20px;
|
||||
gap: 4px 24px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
@ -19,6 +19,7 @@
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
|
||||
import type { OnAttributeHandler } from '../utils/html';
|
||||
|
||||
import { Icon } from './icon';
|
||||
|
||||
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
||||
if (name === 'rel' && tagName === 'a') {
|
||||
if (value === 'me') {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
name,
|
||||
value
|
||||
.split(' ')
|
||||
.filter((x) => x !== 'me')
|
||||
.join(' '),
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
link: string;
|
||||
}
|
||||
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
||||
<span className='verified-badge'>
|
||||
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
||||
<EmojiHTML as='span' htmlString={link} onAttribute={onAttribute} />
|
||||
</span>
|
||||
);
|
||||
@ -19,10 +19,11 @@ import type { Account } from '@/flavours/glitch/models/account';
|
||||
import { getAccountHidden } from '@/flavours/glitch/selectors/accounts';
|
||||
import { useAppSelector, useAppDispatch } from '@/flavours/glitch/store';
|
||||
|
||||
import { FamiliarFollowers } from '../../../components/familiar_followers';
|
||||
|
||||
import { AccountName } from './account_name';
|
||||
import { AccountSubscriptionForm } from './account_subscription_form';
|
||||
import { AccountButtons } from './buttons';
|
||||
import { FamiliarFollowers } from './familiar_followers';
|
||||
import { AccountHeaderFields } from './fields';
|
||||
import { AccountInfo } from './info';
|
||||
import { MemorialNote } from './memorial_note';
|
||||
@ -165,7 +166,10 @@ export const AccountHeader: React.FC<{
|
||||
<AccountNumberFields accountId={accountId} />
|
||||
|
||||
{!isMe && !suspendedOrHidden && (
|
||||
<FamiliarFollowers accountId={accountId} />
|
||||
<FamiliarFollowers
|
||||
accountId={accountId}
|
||||
className={classes.familiarFollowers}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!suspendedOrHidden && (
|
||||
|
||||
@ -3,22 +3,17 @@ import { useCallback, useRef, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
||||
import { Account } from 'flavours/glitch/components/account';
|
||||
import type { RenderButtonOptions } from 'flavours/glitch/components/account_list_item';
|
||||
import {
|
||||
AccountListItem,
|
||||
AccountListItemFollowButton,
|
||||
} from 'flavours/glitch/components/account_list_item';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { Callout } from 'flavours/glitch/components/callout';
|
||||
import { FollowButton } from 'flavours/glitch/components/follow_button';
|
||||
import {
|
||||
NumberFields,
|
||||
NumberFieldsItem,
|
||||
} from 'flavours/glitch/components/number_fields';
|
||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||
import {
|
||||
Article,
|
||||
ItemList,
|
||||
} from 'flavours/glitch/components/scrollable_list/components';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
||||
import { useRelationship } from 'flavours/glitch/hooks/useRelationship';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
|
||||
import { useConfirmRevoke } from './revoke_collection_inclusion_modal';
|
||||
@ -31,99 +26,6 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const AccountItem: React.FC<{
|
||||
accountId: string | undefined;
|
||||
collectionOwnerId: string;
|
||||
onRevoke: () => void;
|
||||
withBio?: boolean;
|
||||
withBorder?: boolean;
|
||||
}> = ({
|
||||
accountId,
|
||||
collectionOwnerId,
|
||||
onRevoke,
|
||||
withBio = true,
|
||||
withBorder = true,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const account = useAccount(accountId);
|
||||
const relationship = useRelationship(accountId);
|
||||
|
||||
if (!accountId || !account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// When viewing your own collection, only show the Follow button
|
||||
// for accounts you're not following (anymore).
|
||||
// Otherwise, always show the follow button in its various states.
|
||||
const isOwnAccount = accountId === me;
|
||||
const withoutButton =
|
||||
isOwnAccount ||
|
||||
!relationship ||
|
||||
(collectionOwnerId === me &&
|
||||
(relationship.following || relationship.requested));
|
||||
|
||||
return (
|
||||
<div className={classes.accountItemWrapper} data-with-border={withBorder}>
|
||||
<Account
|
||||
minimal
|
||||
id={accountId}
|
||||
withBio={withBio}
|
||||
withBorder={false}
|
||||
withMenu={false}
|
||||
className={classes.accountItem}
|
||||
extraAccountInfo={
|
||||
<NumberFields>
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.followers'
|
||||
defaultMessage='Followers'
|
||||
/>
|
||||
}
|
||||
hint={intl.formatNumber(account.followers_count)}
|
||||
>
|
||||
<ShortNumber value={account.followers_count} />
|
||||
</NumberFieldsItem>
|
||||
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||
}
|
||||
hint={intl.formatNumber(account.statuses_count)}
|
||||
>
|
||||
<ShortNumber value={account.statuses_count} />
|
||||
</NumberFieldsItem>
|
||||
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.last_active'
|
||||
defaultMessage='Last active'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RelativeTimestamp
|
||||
long
|
||||
timestamp={account.last_status_at}
|
||||
noFuture
|
||||
/>
|
||||
</NumberFieldsItem>
|
||||
</NumberFields>
|
||||
}
|
||||
/>
|
||||
{!withoutButton && <FollowButton compact accountId={accountId} />}
|
||||
{isOwnAccount && (
|
||||
<Button secondary compact onClick={onRevoke}>
|
||||
<FormattedMessage
|
||||
id='collections.detail.revoke_inclusion'
|
||||
defaultMessage='Remove me'
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SensitiveScreen: React.FC<{
|
||||
sensitive: boolean | undefined;
|
||||
focusTargetRef: React.RefObject<HTMLHeadingElement>;
|
||||
@ -158,6 +60,7 @@ const SensitiveScreen: React.FC<{
|
||||
/>
|
||||
}
|
||||
onPrimary={showAnyway}
|
||||
className={classes.sensitiveScreen}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='collections.detail.sensitive_note'
|
||||
@ -176,14 +79,37 @@ export const CollectionAccountsList: React.FC<{
|
||||
const listHeadingRef = useRef<HTMLHeadingElement>(null);
|
||||
|
||||
const isOwnCollection = collection?.account_id === me;
|
||||
const { items = [] } = collection ?? {};
|
||||
const { items = [], account_id: collectionOwnerId } = collection ?? {};
|
||||
|
||||
const renderAccountItemButton = useCallback(
|
||||
({ relationship, accountId }: RenderButtonOptions) => {
|
||||
// When viewing your own collection, only show the Follow button
|
||||
// for accounts you're not following anymore.
|
||||
const withoutButton =
|
||||
!relationship ||
|
||||
(collectionOwnerId === me &&
|
||||
(relationship.following || relationship.requested));
|
||||
|
||||
if (withoutButton) return null;
|
||||
|
||||
if (accountId === me) {
|
||||
return (
|
||||
<Button secondary compact onClick={confirmRevoke}>
|
||||
<FormattedMessage
|
||||
id='collections.detail.revoke_inclusion'
|
||||
defaultMessage='Remove me'
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return <AccountListItemFollowButton accountId={accountId} />;
|
||||
},
|
||||
[collectionOwnerId, confirmRevoke],
|
||||
);
|
||||
|
||||
return (
|
||||
<ItemList
|
||||
isLoading={isLoading}
|
||||
emptyMessage={intl.formatMessage(messages.empty)}
|
||||
className={classes.itemList}
|
||||
>
|
||||
<>
|
||||
<h3
|
||||
className={classes.columnSubheading}
|
||||
tabIndex={-1}
|
||||
@ -207,22 +133,27 @@ export const CollectionAccountsList: React.FC<{
|
||||
sensitive={!isOwnCollection && collection.sensitive}
|
||||
focusTargetRef={listHeadingRef}
|
||||
>
|
||||
{items.map(({ account_id }, index) => (
|
||||
<Article
|
||||
key={account_id}
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={items.length}
|
||||
>
|
||||
<AccountItem
|
||||
withBorder={index !== items.length - 1}
|
||||
accountId={account_id}
|
||||
collectionOwnerId={collection.account_id}
|
||||
onRevoke={confirmRevoke}
|
||||
/>
|
||||
</Article>
|
||||
))}
|
||||
<ItemList
|
||||
isLoading={isLoading}
|
||||
emptyMessage={intl.formatMessage(messages.empty)}
|
||||
>
|
||||
{items.map(({ account_id }, index) => (
|
||||
<Article
|
||||
key={account_id}
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={items.length}
|
||||
>
|
||||
<AccountListItem
|
||||
accountId={account_id}
|
||||
withBorder={index !== items.length - 1}
|
||||
stats={['followers', 'last-active']}
|
||||
renderButton={renderAccountItemButton}
|
||||
/>
|
||||
</Article>
|
||||
))}
|
||||
</ItemList>
|
||||
</SensitiveScreen>
|
||||
)}
|
||||
</ItemList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@ import { useAccountHandle } from '@/flavours/glitch/components/display_name/defa
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
||||
import { Badge } from 'flavours/glitch/components/badge';
|
||||
import { Callout } from 'flavours/glitch/components/callout';
|
||||
import { Column } from 'flavours/glitch/components/column';
|
||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||
@ -138,7 +139,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
|
||||
<header className={classes.header}>
|
||||
<div className={classes.titleWithMenu}>
|
||||
<div className={classes.titleWrapper}>
|
||||
{tag && <span className={classes.tag}>#{tag.name}</span>}
|
||||
{tag && <Badge label={`#${tag.name}`} icon={null} />}
|
||||
<h2 className={classes.name}>{name}</h2>
|
||||
<AuthorNote id={account_id} />
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
padding: 16px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.titleWithMenu {
|
||||
@ -20,15 +21,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
@ -71,45 +63,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.itemList {
|
||||
padding-inline: 24px;
|
||||
.sensitiveScreen {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.columnSubheading {
|
||||
padding-bottom: 12px;
|
||||
padding-inline: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.accountItemWrapper {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
padding-block: 16px;
|
||||
|
||||
&[data-with-border='true'] {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
:global(.account__note) {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
}
|
||||
|
||||
// Hide 'No description provided' message added by `Account` component
|
||||
:global(.account__note--missing) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.accountItem {
|
||||
--account-name-size: 15px;
|
||||
--account-handle-color: var(--color-text-secondary);
|
||||
--account-handle-size: 13px;
|
||||
--account-bio-color: var(--color-text-primary);
|
||||
--account-bio-size: 13px;
|
||||
--account-outer-spacing: 0;
|
||||
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
import { Account } from '@/flavours/glitch/components/account';
|
||||
import { AccountListItem } from '@/flavours/glitch/components/account_list_item';
|
||||
import type { ColumnRef } from '@/flavours/glitch/components/column';
|
||||
import { Column } from '@/flavours/glitch/components/column';
|
||||
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
|
||||
@ -55,12 +55,12 @@ export const AccountList: FC<AccountListProps> = ({
|
||||
}
|
||||
const children =
|
||||
list?.items.map((followerId) => (
|
||||
<Account key={followerId} id={followerId} />
|
||||
<AccountListItem key={followerId} accountId={followerId} />
|
||||
)) ?? [];
|
||||
|
||||
if (prependAccountId) {
|
||||
children.unshift(
|
||||
<Account key={prependAccountId} id={prependAccountId} minimal />,
|
||||
<AccountListItem key={prependAccountId} accountId={prependAccountId} />,
|
||||
);
|
||||
}
|
||||
return children;
|
||||
|
||||
@ -16,12 +16,12 @@ import {
|
||||
} from 'flavours/glitch/actions/suggestions';
|
||||
import type { ApiSuggestionSourceJSON } from 'flavours/glitch/api_types/suggestions';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { Badge, VerifiedBadge } from 'flavours/glitch/components/badge';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { FollowButton } from 'flavours/glitch/components/follow_button';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
|
||||
import { domain } from 'flavours/glitch/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
@ -110,13 +110,12 @@ const Source: React.FC<{ id: ApiSuggestionSourceJSON }> = ({ id }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<Badge
|
||||
className='inline-follow-suggestions__body__scrollable__card__text-stack__source'
|
||||
title={hint}
|
||||
>
|
||||
<Icon id='' icon={InfoIcon} />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
label={label}
|
||||
icon={<InfoIcon />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
apiRemoveAccountFromList,
|
||||
} from 'flavours/glitch/api/lists';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { VerifiedBadge } from 'flavours/glitch/components/badge';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { Column } from 'flavours/glitch/components/column';
|
||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||
@ -27,7 +28,6 @@ import { FollowersCounter } from 'flavours/glitch/components/counters';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
|
||||
import { useSearchAccounts } from 'flavours/glitch/hooks/useSearchAccounts';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
@ -9022,20 +9022,6 @@ noscript {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__familiar-followers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-block: 16px;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
a:any-link {
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account__contents {
|
||||
@ -9045,6 +9031,7 @@ noscript {
|
||||
.account__details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 1em;
|
||||
}
|
||||
|
||||
@ -11020,11 +11007,6 @@ noscript {
|
||||
}
|
||||
|
||||
&__source {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
color: var(--color-text-tertiary);
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
cursor: help;
|
||||
@ -11033,11 +11015,6 @@ noscript {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11063,11 +11040,6 @@ noscript {
|
||||
}
|
||||
}
|
||||
|
||||
.verified-badge {
|
||||
font-size: 14px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user