Merge commit '66fdd3ae65e13349fbc0b2818d4b91a389075553' into glitch-soc/main

This commit is contained in:
Claire committed 2026-04-08 19:32:32 +02:00
commit b9014ec220
184 files changed
+1237 -1059

No files matched your search

@@ -19,17 +19,17 @@ import type { Account } from '@/mastodon/models/account';
import { getAccountHidden } from '@/mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
import { FamiliarFollowers } from '../../../components/familiar_followers';
import { AccountName } from './account_name';
import { AccountSubscriptionForm } from './account_subscription_form';
import { AccountBadges } from './badges';
import { AccountButtons } from './buttons';
import { FamiliarFollowers } from './familiar_followers';
import { AccountHeaderFields } from './fields';
import { MemorialNote } from './memorial_note';
import { MovedNote } from './moved_note';
import { AccountNote as AccountNoteRedesign } from './note';
import { AccountNote } from './note';
import { AccountNumberFields } from './number_fields';
import redesignClasses from './redesign.module.scss';
import classes from './styles.module.scss';
import { AccountTabs } from './tabs';
const titleFromAccount = (account: Account) => {
@@ -111,12 +111,7 @@ export const AccountHeader: React.FC<{
<FollowRequestNoteContainer account={account} />
)}
<div
className={classNames(
'account__header__image',
redesignClasses.header,
)}
>
<div className={classNames('account__header__image', classes.header)}>
{!suspendedOrHidden && (
<img
src={autoPlayGif ? account.header : account.header_static}
@@ -126,16 +121,11 @@ export const AccountHeader: React.FC<{
)}
</div>
<div
className={classNames(
'account__header__bar',
redesignClasses.barWrapper,
)}
>
<div className={classNames('account__header__bar', classes.barWrapper)}>
<div
className={classNames(
'account__header__tabs',
redesignClasses.avatarWrapper,
classes.avatarWrapper,
)}
>
<a
@@ -156,31 +146,32 @@ export const AccountHeader: React.FC<{
<div
className={classNames(
'account__header__tabs__name',
redesignClasses.displayNameWrapper,
classes.displayNameWrapper,
)}
>
<AccountName accountId={accountId} />
<AccountButtons
accountId={accountId}
className={redesignClasses.buttonsDesktop}
className={classes.buttonsDesktop}
noShare={!isMe || 'share' in navigator}
forceMenu={'share' in navigator}
/>
</div>
<AccountBadges accountId={accountId} />
<AccountNumberFields accountId={accountId} />
{!isMe && !suspendedOrHidden && (
<FamiliarFollowers accountId={accountId} />
<FamiliarFollowers
accountId={accountId}
className={classes.familiarFollowers}
/>
)}
{!suspendedOrHidden && (
<div className='account__header__extra'>
<div className='account__header__bio'>
{me && account.id !== me && (
<AccountNoteRedesign accountId={accountId} />
<AccountNote accountId={accountId} />
)}
<AccountBio
@@ -188,7 +179,7 @@ export const AccountHeader: React.FC<{
accountId={accountId}
className={classNames(
'account__header__content',
redesignClasses.bio,
classes.bio,
)}
/>
@@ -203,8 +194,8 @@ export const AccountHeader: React.FC<{
<AccountButtons
className={classNames(
redesignClasses.buttonsMobile,
!isIntersecting && redesignClasses.buttonsMobileIsStuck,
classes.buttonsMobile,
!isIntersecting && classes.buttonsMobileIsStuck,
)}
accountId={accountId}
noShare
@@ -21,7 +21,8 @@ import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import HelpIcon from '@/material-icons/400-24px/help.svg?react';
import DomainIcon from '@/material-icons/400-24px/language.svg?react';
import classes from './redesign.module.scss';
import { AccountBadges } from './badges';
import classes from './styles.module.scss';
const messages = defineMessages({
lockedInfo: {
@@ -77,6 +78,8 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
domain={domain}
isSelf={account.id === me}
/>
<AccountBadges accountId={accountId} />
</div>
);
};
@@ -3,6 +3,7 @@ import { useState, useCallback, useId } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import type { IntlShape } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { AxiosError } from 'axios';
@@ -18,7 +19,7 @@ import type { FieldStatus } from 'mastodon/components/form_fields';
import { TextInputField } from 'mastodon/components/form_fields/text_input_field';
import { useAppSelector } from 'mastodon/store';
import classes from './redesign.module.scss';
import classes from './styles.module.scss';
const messages = defineMessages({
emailInvalid: {
@@ -127,7 +128,9 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
if (submitted) {
return (
<div className={classes.bannerBaseCentered}>
<div
className={classNames(classes.bannerBase, classes.bannerBaseCentered)}
>
<div className={classes.bannerTextAndActions}>
<h2>
<FormattedMessage
@@ -16,6 +16,8 @@ import { useAccount } from '@/mastodon/hooks/useAccount';
import type { AccountRole } from '@/mastodon/models/account';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import classes from './styles.module.scss';
export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
const account = useAccount(accountId);
const localDomain = useAppSelector(
@@ -101,7 +103,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
return null;
}
return <div className={'account__header__badges'}>{badges}</div>;
return <div className={classes.badges}>{badges}</div>;
};
function isAdminBadge(role: AccountRole) {
@@ -1,77 +0,0 @@
import { FormattedMessage } from 'react-intl';
import { Avatar } from '@/mastodon/components/avatar';
import { AvatarGroup } from '@/mastodon/components/avatar_group';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
import type { Account } from '@/mastodon/models/account';
import { useFetchFamiliarFollowers } from '../hooks/familiar_followers';
const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({
familiarFollowers,
}) => {
const messageData = {
name1: (
<LinkedDisplayName
displayProps={{ account: familiarFollowers.at(0), variant: 'simple' }}
/>
),
name2: (
<LinkedDisplayName
displayProps={{ account: familiarFollowers.at(1), variant: 'simple' }}
/>
),
othersCount: familiarFollowers.length - 2,
};
if (familiarFollowers.length === 1) {
return (
<FormattedMessage
id='account.familiar_followers_one'
defaultMessage='Followed by {name1}'
values={messageData}
/>
);
} else if (familiarFollowers.length === 2) {
return (
<FormattedMessage
id='account.familiar_followers_two'
defaultMessage='Followed by {name1} and {name2}'
values={messageData}
/>
);
} else {
return (
<FormattedMessage
id='account.familiar_followers_many'
defaultMessage='Followed by {name1}, {name2}, and {othersCount, plural, one {one other you know} other {# others you know}}'
values={messageData}
/>
);
}
};
export const FamiliarFollowers: React.FC<{ accountId: string }> = ({
accountId,
}) => {
const { familiarFollowers, isLoading } = useFetchFamiliarFollowers({
accountId,
});
if (isLoading || familiarFollowers.length === 0) {
return null;
}
return (
<div className='account__header__familiar-followers'>
<AvatarGroup compact>
{familiarFollowers.slice(0, 3).map((account) => (
<Avatar withLink key={account.id} account={account} size={28} />
))}
</AvatarGroup>
<span>
<FamiliarFollowersReadout familiarFollowers={familiarFollowers} />
</span>
</div>
);
};
@@ -23,7 +23,7 @@ import { cleanExtraEmojis } from '../../emoji/normalize';
import type { AccountField } from '../common';
import { useFieldHtml } from '../hooks/useFieldHtml';
import classes from './redesign.module.scss';
import classes from './styles.module.scss';
const verifyMessage = defineMessage({
id: 'account.link_verified_on',
@@ -285,11 +285,14 @@ function useColumnWrap() {
if (!item) {
break;
}
const { ele, span } = item;
if (i < row.length - 1) {
ele.dataset.cols = span.toString();
remainingRowSpan -= span;
} else if (
row.length > 1 &&
row.length === halfColSpan &&
span === 1 &&
remainingRowSpan > 1
@@ -4,7 +4,6 @@ import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import {
blockAccount,
followAccount,
pinAccount,
unblockAccount,
@@ -13,6 +12,7 @@ import {
} from '@/mastodon/actions/accounts';
import { removeAccountFromFollowers } from '@/mastodon/actions/accounts_typed';
import { showAlert } from '@/mastodon/actions/alerts';
import { initBlockModal } from '@/mastodon/actions/blocks';
import { directCompose, mentionCompose } from '@/mastodon/actions/compose';
import {
initDomainBlockModal,
@@ -40,7 +40,7 @@ import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react'
import ReportIcon from '@/material-icons/400-24px/report.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import classes from './redesign.module.scss';
import classes from './styles.module.scss';
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl();
@@ -61,7 +61,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
return [];
}
return redesignMenuItems({
return getMenuItems({
account,
signedIn: !isMe && signedIn,
permissions,
@@ -223,7 +223,7 @@ const redesignMessages = defineMessages({
},
});
function redesignMenuItems({
function getMenuItems({
account,
signedIn,
permissions,
@@ -435,7 +435,7 @@ function redesignMenuItems({
if (relationship?.blocking) {
dispatch(unblockAccount(account.id));
} else {
dispatch(blockAccount(account.id));
dispatch(initBlockModal(account));
}
},
dangerous: true,
@@ -10,7 +10,7 @@ import { IconButton } from '@/mastodon/components/icon_button';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import EditIcon from '@/material-icons/400-24px/edit_square.svg?react';
import classes from './redesign.module.scss';
import classes from './styles.module.scss';
const messages = defineMessages({
title: {
@@ -28,14 +28,12 @@
}
.name {
display: flex;
gap: 2px;
align-items: baseline;
> h1 {
display: inline;
font-size: 22px;
line-height: normal;
white-space: initial;
margin-inline-end: 4px;
}
}
@@ -43,6 +41,10 @@
color: var(--color-text-secondary);
}
.badges {
margin-top: 8px;
}
.handleHelpButton {
display: flex;
gap: 2px;
@@ -191,6 +193,10 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
font-size: 15px;
}
.familiarFollowers {
margin-top: 16px;
}
.note {
margin-bottom: 16px;
}
@@ -406,6 +412,15 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
margin: 16px 0;
}
.bannerBaseCentered {
min-height: 146px;
align-items: center;
.bannerTextAndActions {
text-align: center;
}
}
.bannerTextAndActions {
display: flex;
flex-direction: column;
@@ -426,16 +441,6 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
}
}
.bannerBaseCentered {
composes: bannerBase;
min-height: 146px;
align-items: center;
.bannerTextAndActions {
text-align: center;
}
}
.bannerInputButton {
display: flex;
gap: 8px;
@@ -10,7 +10,7 @@ import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { areCollectionsEnabled } from '../../collections/utils';
import classes from './redesign.module.scss';
import classes from './styles.module.scss';
const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
match?.url === location.pathname ||
@@ -1,32 +0,0 @@
import { useEffect } from 'react';
import { fetchAccountsFamiliarFollowers } from '@/mastodon/actions/accounts_familiar_followers';
import { useIdentity } from '@/mastodon/identity_context';
import { getAccountFamiliarFollowers } from '@/mastodon/selectors/accounts';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { me } from 'mastodon/initial_state';
export const useFetchFamiliarFollowers = ({
accountId,
}: {
accountId?: string;
}) => {
const dispatch = useAppDispatch();
const familiarFollowers = useAppSelector((state) =>
accountId ? getAccountFamiliarFollowers(state, accountId) : null,
);
const { signedIn } = useIdentity();
const hasNoData = familiarFollowers === null;
useEffect(() => {
if (hasNoData && signedIn && accountId && accountId !== me) {
void dispatch(fetchAccountsFamiliarFollowers({ id: accountId }));
}
}, [dispatch, accountId, hasNoData, signedIn]);
return {
familiarFollowers: hasNoData ? [] : familiarFollowers,
isLoading: hasNoData,
};
};
@@ -3,22 +3,17 @@ import { useCallback, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Account } from 'mastodon/components/account';
import type { RenderButtonOptions } from 'mastodon/components/account_list_item';
import {
AccountListItem,
AccountListItemFollowButton,
} from 'mastodon/components/account_list_item';
import { Button } from 'mastodon/components/button';
import { Callout } from 'mastodon/components/callout';
import { FollowButton } from 'mastodon/components/follow_button';
import {
NumberFields,
NumberFieldsItem,
} from 'mastodon/components/number_fields';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import {
Article,
ItemList,
} from 'mastodon/components/scrollable_list/components';
import { ShortNumber } from 'mastodon/components/short_number';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useRelationship } from 'mastodon/hooks/useRelationship';
import { me } from 'mastodon/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 '@/mastodon/components/display_name/default';
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 'mastodon/api_types/collections';
import { Badge } from 'mastodon/components/badge';
import { Callout } from 'mastodon/components/callout';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/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 { useMemo } from 'react';
import type { FC, ReactNode } from 'react';
import { Account } from '@/mastodon/components/account';
import { AccountListItem } from '@/mastodon/components/account_list_item';
import { Column } from '@/mastodon/components/column';
import { ColumnBackButton } from '@/mastodon/components/column_back_button';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
@@ -53,12 +53,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 'mastodon/actions/suggestions';
import type { ApiSuggestionSourceJSON } from 'mastodon/api_types/suggestions';
import { Avatar } from 'mastodon/components/avatar';
import { Badge, VerifiedBadge } from 'mastodon/components/badge';
import { DisplayName } from 'mastodon/components/display_name';
import { FollowButton } from 'mastodon/components/follow_button';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { domain } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/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 'mastodon/api/lists';
import { Avatar } from 'mastodon/components/avatar';
import { VerifiedBadge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
@@ -27,7 +28,6 @@ import { FollowersCounter } from 'mastodon/components/counters';
import { DisplayName } from 'mastodon/components/display_name';
import ScrollableList from 'mastodon/components/scrollable_list';
import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts';
import { me } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';