Merge commit '73fc8d34d98a80cfb32d3c7c5dc4794fad5470d5' into glitch-soc/merge-upstream
This commit is contained in:
commit
1eac058989
174 files changed
+1677
-860
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useParams } from 'react-router';
|
||||
@@ -5,17 +7,21 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import ElephantDarkImage from '@/images/elephant_ui_dark.svg?react';
|
||||
import ElephantLightImage from '@/images/elephant_ui_light.svg?react';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import { EmptyState } from '@/mastodon/components/empty_state';
|
||||
import { LimitedAccountHint } from '@/mastodon/features/account_timeline/components/limited_account_hint';
|
||||
import { areCollectionsEnabled } from '@/mastodon/features/collections/utils';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { useTheme } from '@/mastodon/hooks/useTheme';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
interface EmptyMessageProps {
|
||||
suspended: boolean;
|
||||
hidden: boolean;
|
||||
blockedBy: boolean;
|
||||
accountId?: string;
|
||||
withoutAddCollectionButton?: boolean;
|
||||
}
|
||||
|
||||
export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
@@ -23,6 +29,7 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
suspended,
|
||||
hidden,
|
||||
blockedBy,
|
||||
withoutAddCollectionButton,
|
||||
}) => {
|
||||
const { acct } = useParams<{ acct?: string }>();
|
||||
const me = useCurrentAccountId();
|
||||
@@ -30,6 +37,17 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
const ElephantImage =
|
||||
theme === 'dark' ? ElephantDarkImage : ElephantLightImage;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const confirmHideFeaturedTab = useCallback(() => {
|
||||
void dispatch(
|
||||
openModal({
|
||||
modalType: 'ACCOUNT_HIDE_FEATURED_TAB',
|
||||
modalProps: {},
|
||||
}),
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
if (!accountId) {
|
||||
return null;
|
||||
}
|
||||
@@ -49,17 +67,31 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
image={image}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_self.no_collections'
|
||||
defaultMessage='No collections yet'
|
||||
id='empty_column.account_featured_self.showcase_accounts'
|
||||
defaultMessage='Showcase your favorite accounts'
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_self.showcase_accounts_desc'
|
||||
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Link to='/collections/new' className='button'>
|
||||
{!withoutAddCollectionButton && (
|
||||
<Link to='/collections/new' className='button'>
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_self.no_collections_button'
|
||||
defaultMessage='Create a collection'
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
<Button secondary onClick={confirmHideFeaturedTab}>
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_self.no_collections_button'
|
||||
defaultMessage='Create a collection'
|
||||
id='empty_column.account_featured_self.no_collections_hide_tab'
|
||||
defaultMessage='Hide this tab instead'
|
||||
/>
|
||||
</Link>
|
||||
</Button>
|
||||
</EmptyState>
|
||||
);
|
||||
} else {
|
||||
@@ -72,7 +104,7 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_self.pre_collections_desc'
|
||||
defaultMessage='Collections (coming in Mastodon 4.6) allows you to create your own curated lists of accounts to recommend to others.'
|
||||
defaultMessage='Collections (coming in Mastodon 4.6) allow you to create your own curated lists of accounts to recommend to others.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -93,47 +125,21 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Standard other account empty state.
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_other.title'
|
||||
defaultMessage='Nothing to see here'
|
||||
/>
|
||||
);
|
||||
if (hasCollections) {
|
||||
if (acct) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_other.no_collections_desc'
|
||||
defaultMessage='{acct} hasn’t created any collections yet.'
|
||||
values={{ acct }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_unknown.no_collections_desc'
|
||||
defaultMessage='This account hasn’t created any collections yet.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (acct) {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured.other'
|
||||
defaultMessage='{acct} has not featured anything yet.'
|
||||
values={{ acct }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
if (acct) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured.other'
|
||||
defaultMessage='{acct} hasn’t featured anything yet.'
|
||||
values={{ acct }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_unknown.other'
|
||||
defaultMessage='This account hasn’t featured anything yet.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_unknown.other'
|
||||
defaultMessage='This account has not featured anything yet.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
.subheading {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 52px;
|
||||
padding: 12px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
padding-block: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-transform: none;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--outline-focus-default);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
:global(.icon) {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { IconProp } from '@/mastodon/components/icon';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||
|
||||
import classes from './subheading.module.scss';
|
||||
|
||||
export const Subheading = polymorphicForwardRef<'h2'>(
|
||||
({ as: Component = 'h2', children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
className={classNames(classes.subheading, className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
interface SubheadingLinkProps extends ComponentPropsWithoutRef<typeof Link> {
|
||||
icon: IconProp;
|
||||
}
|
||||
|
||||
export const SubheadingLink: React.FC<SubheadingLinkProps> = ({
|
||||
icon,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Link className={classNames(classes.link, className)} {...props}>
|
||||
<Icon id='subheading-icon' icon={icon} />
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -6,9 +6,10 @@ import { useHistory } from 'react-router';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { AccountListItem } from '@/mastodon/components/account_list_item';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { RemoteHint } from 'mastodon/components/remote_hint';
|
||||
@@ -28,10 +29,13 @@ import {
|
||||
} from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { CollectionListItem } from '../collections/detail/collection_list_item';
|
||||
import { CollectionListItem } from '../collections/components/collection_list_item';
|
||||
import { areCollectionsEnabled } from '../collections/utils';
|
||||
|
||||
import { EmptyMessage } from './components/empty_message';
|
||||
import { Subheading, SubheadingLink } from './components/subheading';
|
||||
|
||||
const collectionsEnabled = areCollectionsEnabled();
|
||||
|
||||
const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
multiColumn,
|
||||
@@ -54,13 +58,12 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
if (accountId) {
|
||||
void dispatch(fetchEndorsedAccounts({ accountId }));
|
||||
|
||||
if (areCollectionsEnabled()) {
|
||||
if (collectionsEnabled) {
|
||||
void dispatch(fetchAccountCollections({ accountId }));
|
||||
}
|
||||
}
|
||||
}, [accountId, dispatch]);
|
||||
|
||||
const isLoading = !accountId;
|
||||
const featuredAccountIds = useAppSelector(
|
||||
(state) =>
|
||||
state.user_lists.getIn(
|
||||
@@ -68,8 +71,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
ImmutableList(),
|
||||
) as ImmutableList<string>,
|
||||
);
|
||||
const { collections, status } = useAppSelector((state) =>
|
||||
selectAccountCollections(state, accountId ?? null),
|
||||
const { collections, status: collectionsLoadStatus } = useAppSelector(
|
||||
(state) => selectAccountCollections(state, accountId ?? null),
|
||||
);
|
||||
const listedCollections = collections.filter(
|
||||
// Hide unlisted and empty collections to avoid confusion
|
||||
@@ -78,6 +81,15 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
(item) => item.discoverable && !!item.item_count,
|
||||
);
|
||||
|
||||
const hasCollections =
|
||||
collectionsEnabled &&
|
||||
collectionsLoadStatus === 'idle' &&
|
||||
listedCollections.length > 0;
|
||||
|
||||
const hasFeaturedAccounts = !featuredAccountIds.isEmpty();
|
||||
|
||||
const isLoading = !accountId || collectionsLoadStatus !== 'idle';
|
||||
|
||||
if (accountId === null) {
|
||||
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
||||
}
|
||||
@@ -92,7 +104,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (featuredAccountIds.isEmpty() && listedCollections.length === 0) {
|
||||
if (!hasFeaturedAccounts && !hasCollections) {
|
||||
return (
|
||||
<AccountFeaturedWrapper accountId={accountId}>
|
||||
<EmptyMessage
|
||||
@@ -114,36 +126,14 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
{accountId && (
|
||||
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
|
||||
)}
|
||||
{listedCollections.length > 0 && status === 'idle' && (
|
||||
<>
|
||||
<h4 className='column-subheading'>
|
||||
<FormattedMessage
|
||||
id='account.featured.collections'
|
||||
defaultMessage='Collections'
|
||||
/>
|
||||
</h4>
|
||||
<ItemList>
|
||||
{listedCollections.map((item, index) => (
|
||||
<CollectionListItem
|
||||
key={item.id}
|
||||
collection={item}
|
||||
withoutBorder={index === listedCollections.length - 1}
|
||||
withAuthorHandle={false}
|
||||
positionInList={index + 1}
|
||||
listSize={listedCollections.length}
|
||||
/>
|
||||
))}
|
||||
</ItemList>
|
||||
</>
|
||||
)}
|
||||
{!featuredAccountIds.isEmpty() && (
|
||||
<>
|
||||
<h4 className='column-subheading'>
|
||||
<Subheading as='h2'>
|
||||
<FormattedMessage
|
||||
id='account.featured.accounts'
|
||||
defaultMessage='Profiles'
|
||||
/>
|
||||
</h4>
|
||||
</Subheading>
|
||||
<ItemList>
|
||||
{featuredAccountIds.map((featuredAccountId, index) => (
|
||||
<Article
|
||||
@@ -152,12 +142,52 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={featuredAccountIds.size}
|
||||
>
|
||||
<Account id={featuredAccountId} />
|
||||
<AccountListItem accountId={featuredAccountId} />
|
||||
</Article>
|
||||
))}
|
||||
</ItemList>
|
||||
</>
|
||||
)}
|
||||
{collectionsEnabled && (
|
||||
<>
|
||||
<Subheading as='header'>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='account.featured.collections'
|
||||
defaultMessage='Collections'
|
||||
/>
|
||||
</h2>
|
||||
<SubheadingLink to='/collections/new' icon={AddIcon}>
|
||||
<FormattedMessage
|
||||
id='account.featured.new_collection'
|
||||
defaultMessage='New collection'
|
||||
/>
|
||||
</SubheadingLink>
|
||||
</Subheading>
|
||||
{hasCollections ? (
|
||||
<ItemList>
|
||||
{listedCollections.map((item, index) => (
|
||||
<CollectionListItem
|
||||
key={item.id}
|
||||
collection={item}
|
||||
withoutBorder={index === listedCollections.length - 1}
|
||||
withAuthorHandle={false}
|
||||
positionInList={index + 1}
|
||||
listSize={listedCollections.length}
|
||||
/>
|
||||
))}
|
||||
</ItemList>
|
||||
) : (
|
||||
<EmptyMessage
|
||||
withoutAddCollectionButton
|
||||
blockedBy={blockedBy}
|
||||
hidden={hidden}
|
||||
suspended={suspended}
|
||||
accountId={accountId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<RemoteHint accountId={accountId} />
|
||||
</Scrollable>
|
||||
</Column>
|
||||
|
||||
@@ -8,8 +8,6 @@ import { NavLink } from 'react-router-dom';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
|
||||
import { areCollectionsEnabled } from '../../collections/utils';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
|
||||
@@ -41,14 +39,7 @@ export const AccountTabs: FC = () => {
|
||||
)}
|
||||
{show_featured && (
|
||||
<NavLink exact to={`/@${acct}/featured`}>
|
||||
{areCollectionsEnabled() ? (
|
||||
<FormattedMessage
|
||||
id='account.featured.collections'
|
||||
defaultMessage='Collections'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||
)}
|
||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
padding: 12px 16px;
|
||||
gap: 16px;
|
||||
|
||||
&:not(.wrapperWithoutBorder) {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
padding: 4px;
|
||||
margin-top: -2px;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useId } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Article } from 'mastodon/components/scrollable_list/components';
|
||||
import type { CollectionLockupProps } from 'mastodon/features/collections/components/collection_lockup';
|
||||
import { CollectionLockup } from 'mastodon/features/collections/components/collection_lockup';
|
||||
import { CollectionMenu } from 'mastodon/features/collections/components/collection_menu';
|
||||
|
||||
import classes from './collection_list_item.module.scss';
|
||||
|
||||
interface CollectionListItemProps extends CollectionLockupProps {
|
||||
withoutBorder?: boolean;
|
||||
positionInList: number;
|
||||
listSize: number;
|
||||
}
|
||||
|
||||
export const CollectionListItem: React.FC<CollectionListItemProps> = ({
|
||||
collection,
|
||||
withoutBorder,
|
||||
positionInList,
|
||||
listSize,
|
||||
...otherProps
|
||||
}) => {
|
||||
const uniqueId = useId();
|
||||
const linkId = `${uniqueId}-link`;
|
||||
const infoId = `${uniqueId}-info`;
|
||||
|
||||
return (
|
||||
<Article
|
||||
focusable
|
||||
className={classNames(
|
||||
classes.wrapper,
|
||||
withoutBorder && classes.wrapperWithoutBorder,
|
||||
)}
|
||||
aria-labelledby={linkId}
|
||||
aria-describedby={infoId}
|
||||
aria-posinset={positionInList}
|
||||
aria-setsize={listSize}
|
||||
>
|
||||
<CollectionLockup collection={collection} {...otherProps} />
|
||||
|
||||
<CollectionMenu
|
||||
context='list'
|
||||
collection={collection}
|
||||
className={classes.menuButton}
|
||||
/>
|
||||
</Article>
|
||||
);
|
||||
};
|
||||
+2
-22
@@ -1,21 +1,11 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
margin-inline: 24px;
|
||||
padding-block: 12px;
|
||||
gap: 16px;
|
||||
|
||||
&:not(.wrapperWithoutBorder) {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.avatarGrid {
|
||||
@@ -70,13 +60,3 @@
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
padding: 4px;
|
||||
margin-top: -2px;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useId } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { AvatarById } from 'mastodon/components/avatar';
|
||||
import { useAccountHandle } from 'mastodon/components/display_name/default';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
|
||||
import classes from './collection_lockup.module.scss';
|
||||
|
||||
export const AvatarGrid: React.FC<{
|
||||
accountIds: (string | undefined)[];
|
||||
sensitive?: boolean;
|
||||
}> = ({ accountIds: ids, sensitive }) => {
|
||||
const avatarIds = [ids[0], ids[1], ids[2], ids[3]];
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
classes.avatarGrid,
|
||||
sensitive ? classes.avatarGridSensitive : null,
|
||||
)}
|
||||
>
|
||||
{avatarIds.map((id) => (
|
||||
<AvatarById
|
||||
animate={false}
|
||||
key={id}
|
||||
accountId={id}
|
||||
className={classes.avatar}
|
||||
size={25}
|
||||
/>
|
||||
))}
|
||||
{sensitive && <WarningIcon className={classes.avatarSensitiveBadge} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface CollectionLockupProps {
|
||||
collection: ApiCollectionJSON;
|
||||
withAuthorHandle?: boolean;
|
||||
withTimestamp?: boolean;
|
||||
}
|
||||
|
||||
export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
||||
collection,
|
||||
withAuthorHandle = true,
|
||||
withTimestamp,
|
||||
}) => {
|
||||
const { id, name } = collection;
|
||||
const uniqueId = useId();
|
||||
const linkId = `${uniqueId}-link`;
|
||||
const infoId = `${uniqueId}-info`;
|
||||
const authorAccount = useAccount(collection.account_id);
|
||||
const authorHandle = useAccountHandle(authorAccount, domain);
|
||||
|
||||
return (
|
||||
<div className={classes.content}>
|
||||
<AvatarGrid
|
||||
accountIds={collection.items.map((item) => item.account_id)}
|
||||
sensitive={collection.sensitive}
|
||||
/>
|
||||
<div>
|
||||
<h2 id={linkId}>
|
||||
<Link to={`/collections/${id}`} className={classes.link}>
|
||||
{name}
|
||||
</Link>
|
||||
</h2>
|
||||
<ul className={classes.info} id={infoId}>
|
||||
{collection.sensitive && (
|
||||
<li className='sr-only'>
|
||||
<FormattedMessage
|
||||
id='collections.sensitive'
|
||||
defaultMessage='Sensitive'
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
{withAuthorHandle && authorAccount && (
|
||||
<FormattedMessage
|
||||
id='collections.by_account'
|
||||
defaultMessage='by {account_handle}'
|
||||
values={{
|
||||
account_handle: authorHandle,
|
||||
}}
|
||||
tagName='li'
|
||||
/>
|
||||
)}
|
||||
<FormattedMessage
|
||||
id='collections.account_count'
|
||||
defaultMessage='{count, plural, one {# account} other {# accounts}}'
|
||||
values={{ count: collection.item_count }}
|
||||
tagName='li'
|
||||
/>
|
||||
{withTimestamp && (
|
||||
<FormattedMessage
|
||||
id='collections.last_updated_at'
|
||||
defaultMessage='Last updated: {date}'
|
||||
values={{
|
||||
date: (
|
||||
<RelativeTimestamp timestamp={collection.updated_at} long />
|
||||
),
|
||||
}}
|
||||
tagName='li'
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
File renamed without changes.
+8
@@ -0,0 +1,8 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { CollectionLockupProps } from 'mastodon/features/collections/components/collection_lockup';
|
||||
import { CollectionLockup } from 'mastodon/features/collections/components/collection_lockup';
|
||||
|
||||
import classes from './collection_preview_card.module.scss';
|
||||
|
||||
interface CollectionPreviewCardProps extends CollectionLockupProps {
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
export const CollectionPreviewCard: React.FC<CollectionPreviewCardProps> = ({
|
||||
collection,
|
||||
onRemove,
|
||||
...otherProps
|
||||
}) => {
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<CollectionLockup collection={collection} {...otherProps} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-20
@@ -5,27 +5,7 @@
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap-reverse;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
color: var(--color-text-primary);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--color-bg-brand-soft),
|
||||
var(--color-bg-primary)
|
||||
);
|
||||
border: 1px solid var(--color-bg-brand-base);
|
||||
}
|
||||
|
||||
.previewHeading {
|
||||
font-size: 22px;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
+3
-16
@@ -8,8 +8,6 @@ import { me } from '@/mastodon/initial_state';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { changeCompose, focusCompose } from 'mastodon/actions/compose';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { AvatarById } from 'mastodon/components/avatar';
|
||||
import { AvatarGroup } from 'mastodon/components/avatar_group';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CopyLinkField } from 'mastodon/components/form_fields';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
@@ -20,7 +18,7 @@ import {
|
||||
} from 'mastodon/components/modal_shell';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { AuthorNote } from '.';
|
||||
import { CollectionPreviewCard } from './collection_preview_card';
|
||||
import classes from './share_modal.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -95,24 +93,13 @@ export const CollectionShareModal: React.FC<{
|
||||
/>
|
||||
|
||||
<div className={classes.preview}>
|
||||
<div>
|
||||
<h2 className={classes.previewHeading}>{collection.name}</h2>
|
||||
<AuthorNote id={collection.account_id} />
|
||||
</div>
|
||||
<AvatarGroup>
|
||||
{collection.items.slice(0, 5).map(({ account_id }) => {
|
||||
if (!account_id) return;
|
||||
return (
|
||||
<AvatarById key={account_id} accountId={account_id} size={28} />
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
<CollectionPreviewCard collection={collection} />
|
||||
</div>
|
||||
|
||||
<CopyLinkField
|
||||
label={intl.formatMessage({
|
||||
id: 'collection.share_modal.share_link_label',
|
||||
defaultMessage: 'Invite share link',
|
||||
defaultMessage: 'Share link',
|
||||
})}
|
||||
value={collectionLink}
|
||||
/>
|
||||
@@ -146,7 +146,6 @@ export const CollectionAccountsList: React.FC<{
|
||||
<AccountListItem
|
||||
accountId={account_id}
|
||||
withBorder={index !== items.length - 1}
|
||||
stats={['followers', 'last-active']}
|
||||
renderButton={renderAccountItemButton}
|
||||
/>
|
||||
</Article>
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { useId } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { AvatarById } from 'mastodon/components/avatar';
|
||||
import { useAccountHandle } from 'mastodon/components/display_name/default';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
import { Article } from 'mastodon/components/scrollable_list/components';
|
||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
|
||||
import classes from './collection_list_item.module.scss';
|
||||
import { CollectionMenu } from './collection_menu';
|
||||
|
||||
export const AvatarGrid: React.FC<{
|
||||
accountIds: (string | undefined)[];
|
||||
sensitive?: boolean;
|
||||
}> = ({ accountIds: ids, sensitive }) => {
|
||||
const avatarIds = [ids[0], ids[1], ids[2], ids[3]];
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
classes.avatarGrid,
|
||||
sensitive ? classes.avatarGridSensitive : null,
|
||||
)}
|
||||
>
|
||||
{avatarIds.map((id) => (
|
||||
<AvatarById
|
||||
animate={false}
|
||||
key={id}
|
||||
accountId={id}
|
||||
className={classes.avatar}
|
||||
size={25}
|
||||
/>
|
||||
))}
|
||||
{sensitive && <WarningIcon className={classes.avatarSensitiveBadge} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionListItem: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
withoutBorder?: boolean;
|
||||
withAuthorHandle?: boolean;
|
||||
withTimestamp?: boolean;
|
||||
positionInList: number;
|
||||
listSize: number;
|
||||
}> = ({
|
||||
collection,
|
||||
withoutBorder,
|
||||
withAuthorHandle = true,
|
||||
withTimestamp,
|
||||
positionInList,
|
||||
listSize,
|
||||
}) => {
|
||||
const { id, name } = collection;
|
||||
const uniqueId = useId();
|
||||
const linkId = `${uniqueId}-link`;
|
||||
const infoId = `${uniqueId}-info`;
|
||||
const authorAccount = useAccount(collection.account_id);
|
||||
const authorHandle = useAccountHandle(authorAccount, domain);
|
||||
|
||||
return (
|
||||
<Article
|
||||
focusable
|
||||
className={classNames(
|
||||
classes.wrapper,
|
||||
withoutBorder && classes.wrapperWithoutBorder,
|
||||
)}
|
||||
aria-labelledby={linkId}
|
||||
aria-describedby={infoId}
|
||||
aria-posinset={positionInList}
|
||||
aria-setsize={listSize}
|
||||
>
|
||||
<div className={classes.content}>
|
||||
<AvatarGrid
|
||||
accountIds={collection.items.map((item) => item.account_id)}
|
||||
sensitive={collection.sensitive}
|
||||
/>
|
||||
<div>
|
||||
<h2 id={linkId}>
|
||||
<Link to={`/collections/${id}`} className={classes.link}>
|
||||
{name}
|
||||
</Link>
|
||||
</h2>
|
||||
<ul className={classes.info} id={infoId}>
|
||||
{collection.sensitive && (
|
||||
<li className='sr-only'>
|
||||
<FormattedMessage
|
||||
id='collections.sensitive'
|
||||
defaultMessage='Sensitive'
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
{withAuthorHandle && authorAccount && (
|
||||
<FormattedMessage
|
||||
id='collections.by_account'
|
||||
defaultMessage='by {account_handle}'
|
||||
values={{
|
||||
account_handle: authorHandle,
|
||||
}}
|
||||
tagName='li'
|
||||
/>
|
||||
)}
|
||||
<FormattedMessage
|
||||
id='collections.account_count'
|
||||
defaultMessage='{count, plural, one {# account} other {# accounts}}'
|
||||
values={{ count: collection.item_count }}
|
||||
tagName='li'
|
||||
/>
|
||||
{withTimestamp && (
|
||||
<FormattedMessage
|
||||
id='collections.last_updated_at'
|
||||
defaultMessage='Last updated: {date}'
|
||||
values={{
|
||||
date: (
|
||||
<RelativeTimestamp timestamp={collection.updated_at} long />
|
||||
),
|
||||
}}
|
||||
tagName='li'
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollectionMenu
|
||||
context='list'
|
||||
collection={collection}
|
||||
className={classes.menuButton}
|
||||
/>
|
||||
</Article>
|
||||
);
|
||||
};
|
||||
@@ -23,8 +23,9 @@ import { domain, me } from 'mastodon/initial_state';
|
||||
import { fetchCollection } from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { CollectionMenu } from '../components/collection_menu';
|
||||
|
||||
import { CollectionAccountsList } from './accounts_list';
|
||||
import { CollectionMenu } from './collection_menu';
|
||||
import { useConfirmRevoke } from './revoke_collection_inclusion_modal';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
@@ -66,7 +67,7 @@ export const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const RevokeControls: React.FC<{
|
||||
const RevokeControls: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
}> = ({ collection }) => {
|
||||
const authorAccount = useAccount(collection.account_id);
|
||||
|
||||
@@ -61,9 +61,11 @@ const AddedAccountItem: React.FC<{
|
||||
onRemove(accountId);
|
||||
}, [accountId, onRemove]);
|
||||
|
||||
const lastStatusAt = account?.last_status_at;
|
||||
|
||||
const lastPostHint = useMemo(
|
||||
() =>
|
||||
isOlderThanAWeek(account?.last_status_at) && (
|
||||
(!lastStatusAt || isOlderThanAWeek(lastStatusAt)) && (
|
||||
<Badge
|
||||
label={
|
||||
<FormattedMessage
|
||||
@@ -75,7 +77,7 @@ const AddedAccountItem: React.FC<{
|
||||
className={classes.accountBadge}
|
||||
/>
|
||||
),
|
||||
[account?.last_status_at],
|
||||
[lastStatusAt],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,22 +10,29 @@ import CollectionsFilledIcon from '@/material-icons/400-24px/category-fill.svg?r
|
||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { DisplayNameSimple } from 'mastodon/components/display_name/simple';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import {
|
||||
ItemList,
|
||||
Scrollable,
|
||||
} from 'mastodon/components/scrollable_list/components';
|
||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||
import { useAccountId, useCurrentAccountId } from 'mastodon/hooks/useAccountId';
|
||||
import {
|
||||
fetchAccountCollections,
|
||||
selectAccountCollections,
|
||||
} from 'mastodon/reducers/slices/collections';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { CollectionListItem } from './detail/collection_list_item';
|
||||
import { CollectionListItem } from './components/collection_list_item';
|
||||
import { messages as editorMessages } from './editor';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.collections', defaultMessage: 'My collections' },
|
||||
headingMe: { id: 'column.my_collections', defaultMessage: 'My collections' },
|
||||
headingOther: {
|
||||
id: 'column.other_collections',
|
||||
defaultMessage: 'Collections by {name}',
|
||||
},
|
||||
});
|
||||
|
||||
export const Collections: React.FC<{
|
||||
@@ -33,17 +40,22 @@ export const Collections: React.FC<{
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const me = useAppSelector((state) => state.meta.get('me') as string);
|
||||
const me = useCurrentAccountId();
|
||||
const accountId = useAccountId();
|
||||
const account = useAccount(accountId);
|
||||
|
||||
const { collections, status } = useAppSelector((state) =>
|
||||
selectAccountCollections(state, me),
|
||||
selectAccountCollections(state, accountId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void dispatch(fetchAccountCollections({ accountId: me }));
|
||||
}, [dispatch, me]);
|
||||
if (accountId) {
|
||||
void dispatch(fetchAccountCollections({ accountId }));
|
||||
}
|
||||
}, [dispatch, accountId]);
|
||||
|
||||
const emptyMessage =
|
||||
status === 'error' ? (
|
||||
status === 'error' || !accountId ? (
|
||||
<FormattedMessage
|
||||
id='collections.error_loading_collections'
|
||||
defaultMessage='There was an error when trying to load your collections.'
|
||||
@@ -67,25 +79,36 @@ export const Collections: React.FC<{
|
||||
</>
|
||||
);
|
||||
|
||||
const isOwnCollection = accountId === me;
|
||||
const titleMessage = isOwnCollection
|
||||
? messages.headingMe
|
||||
: messages.headingOther;
|
||||
|
||||
const pageTitle = intl.formatMessage(titleMessage, {
|
||||
name: account?.get('display_name'),
|
||||
});
|
||||
const pageTitleHtml = intl.formatMessage(titleMessage, {
|
||||
name: <DisplayNameSimple account={account} />,
|
||||
});
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
>
|
||||
<Column bindToDocument={!multiColumn} label={pageTitle}>
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
title={pageTitleHtml}
|
||||
icon='collections'
|
||||
iconComponent={CollectionsFilledIcon}
|
||||
multiColumn={multiColumn}
|
||||
extraButton={
|
||||
<Link
|
||||
to='/collections/new'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(editorMessages.create)}
|
||||
aria-label={intl.formatMessage(editorMessages.create)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</Link>
|
||||
isOwnCollection && (
|
||||
<Link
|
||||
to='/collections/new'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(editorMessages.create)}
|
||||
aria-label={intl.formatMessage(editorMessages.create)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -105,7 +128,7 @@ export const Collections: React.FC<{
|
||||
</Scrollable>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
|
||||
@@ -53,12 +53,20 @@ export const AccountList: FC<AccountListProps> = ({
|
||||
}
|
||||
const children =
|
||||
list?.items.map((followerId) => (
|
||||
<AccountListItem key={followerId} accountId={followerId} />
|
||||
<AccountListItem
|
||||
key={followerId}
|
||||
accountId={followerId}
|
||||
withBio={false}
|
||||
/>
|
||||
)) ?? [];
|
||||
|
||||
if (prependAccountId) {
|
||||
children.unshift(
|
||||
<AccountListItem key={prependAccountId} accountId={prependAccountId} />,
|
||||
<AccountListItem
|
||||
key={prependAccountId}
|
||||
accountId={prependAccountId}
|
||||
withBio={false}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return children;
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Map as ImmutableMap } from 'immutable';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import BookmarksActiveIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
|
||||
@@ -212,6 +213,7 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
|
||||
const { signedIn, permissions, disabledAccountId } = useIdentity();
|
||||
const location = useLocation();
|
||||
const showSearch = useBreakpoint('full') && !multiColumn;
|
||||
const account = useAccount(me);
|
||||
|
||||
let banner: React.ReactNode;
|
||||
|
||||
@@ -335,7 +337,7 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
|
||||
{areCollectionsEnabled() && (
|
||||
<ColumnLink
|
||||
transparent
|
||||
to='/collections'
|
||||
to={`/@${account?.acct}/collections`}
|
||||
icon='collections'
|
||||
iconComponent={CollectionsIcon}
|
||||
activeIconComponent={CollectionsActiveIcon}
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { DisplayNameSimple } from '@/mastodon/components/display_name/simple';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import CollectionsFilledIcon from '@/material-icons/400-24px/category-fill.svg?react';
|
||||
import type {
|
||||
NotificationGroupAddedToCollection,
|
||||
NotificationGroupCollectionUpdate,
|
||||
} from 'mastodon/models/notification_group';
|
||||
|
||||
import { CollectionPreviewCard } from '../../collections/components/collection_preview_card';
|
||||
|
||||
export const NotificationCollection: React.FC<{
|
||||
notification:
|
||||
| NotificationGroupAddedToCollection
|
||||
| NotificationGroupCollectionUpdate;
|
||||
unread: boolean;
|
||||
}> = ({ notification, unread }) => {
|
||||
const { collection, type } = notification;
|
||||
const collectionCreatorAccount = useAccount(collection.account_id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'notification-group',
|
||||
`notification-group--${type}`,
|
||||
{ 'notification-group--unread': unread },
|
||||
)}
|
||||
>
|
||||
<div className='notification-group__icon'>
|
||||
<Icon id='collection' icon={CollectionsFilledIcon} />
|
||||
</div>
|
||||
|
||||
<div className='notification-group__main'>
|
||||
<div className='notification-group__main__header'>
|
||||
<div className='notification-group__main__header__label'>
|
||||
{type === 'added_to_collection' && (
|
||||
<FormattedMessage
|
||||
id='notification.added_to_collection'
|
||||
defaultMessage='{name} added you to a collection'
|
||||
values={{
|
||||
name: (
|
||||
<DisplayNameSimple account={collectionCreatorAccount} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === 'collection_update' && (
|
||||
<FormattedMessage
|
||||
id='notification.collection_update'
|
||||
defaultMessage='{name} edited a collection you’re in'
|
||||
values={{
|
||||
name: (
|
||||
<DisplayNameSimple account={collectionCreatorAccount} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollectionPreviewCard collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
import { NotificationAdminReport } from './notification_admin_report';
|
||||
import { NotificationAdminSignUp } from './notification_admin_sign_up';
|
||||
import { NotificationAnnualReport } from './notification_annual_report';
|
||||
import { NotificationCollection } from './notification_collection';
|
||||
import { NotificationFavourite } from './notification_favourite';
|
||||
import { NotificationFollow } from './notification_follow';
|
||||
import { NotificationFollowRequest } from './notification_follow_request';
|
||||
@@ -156,6 +157,15 @@ export const NotificationGroup: React.FC<{
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'added_to_collection':
|
||||
case 'collection_update':
|
||||
content = (
|
||||
<NotificationCollection
|
||||
unread={unread}
|
||||
notification={notificationGroup}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ColumnSubheading = ({ text }) => {
|
||||
return (
|
||||
<div className='column-subheading'>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ColumnSubheading.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ColumnSubheading;
|
||||
+5
-2
@@ -4,6 +4,8 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import { deleteCollection } from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
@@ -34,11 +36,12 @@ export const ConfirmDeleteCollectionModal: React.FC<
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const { acct: currentUserName } = useAccount(me) ?? {};
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
void dispatch(deleteCollection({ collectionId: id }));
|
||||
history.push('/collections');
|
||||
}, [dispatch, history, id]);
|
||||
history.push(`/@${currentUserName}/collections`);
|
||||
}, [dispatch, history, id, currentUserName]);
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { domain } from '@/mastodon/initial_state';
|
||||
import { patchProfile } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import type { BaseConfirmationModalProps } from './confirmation_modal';
|
||||
import { ConfirmationModal } from './confirmation_modal';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'confirmations.hide_featured_tab.title',
|
||||
defaultMessage: 'Hide "Featured" tab?',
|
||||
},
|
||||
intro: {
|
||||
id: 'confirmations.hide_featured_tab.intro',
|
||||
defaultMessage:
|
||||
'You can change this at any time under <i>Edit profile > Profile tab settings</i>.',
|
||||
},
|
||||
message: {
|
||||
id: 'confirmations.hide_featured_tab.message',
|
||||
defaultMessage:
|
||||
'This will hide the tab for users on {serverName} and other servers running the latest version of Mastodon. Other displays may vary.',
|
||||
},
|
||||
confirm: {
|
||||
id: 'confirmations.hide_featured_tab.confirm',
|
||||
defaultMessage: 'Hide tab',
|
||||
},
|
||||
});
|
||||
|
||||
export const ConfirmHideFeaturedTabModal: React.FC<
|
||||
BaseConfirmationModalProps
|
||||
> = ({ onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const currentAccountId = useCurrentAccountId();
|
||||
const { acct: currentUserName } = useAccount(currentAccountId) ?? {};
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
void dispatch(patchProfile({ show_featured: false }));
|
||||
history.push(`/@${currentUserName}`);
|
||||
}, [currentUserName, dispatch, history]);
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
title={intl.formatMessage(messages.title)}
|
||||
extraContent={
|
||||
<div className='prose'>
|
||||
<p>
|
||||
{intl.formatMessage(messages.intro, {
|
||||
i: (words) => <i>{words}</i>,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(messages.message, {
|
||||
serverName: domain,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
confirm={intl.formatMessage(messages.confirm)}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -79,7 +79,7 @@ export const MODAL_COMPONENTS = {
|
||||
'DOMAIN_BLOCK': DomainBlockModal,
|
||||
'REPORT': ReportModal,
|
||||
'REPORT_COLLECTION': ReportCollectionModal,
|
||||
'SHARE_COLLECTION': () => import('@/mastodon/features/collections/detail/share_modal').then(module => ({ default: module.CollectionShareModal })),
|
||||
'SHARE_COLLECTION': () => import('@/mastodon/features/collections/components/share_modal').then(module => ({ default: module.CollectionShareModal })),
|
||||
'REVOKE_COLLECTION_INCLUSION': () => import('@/mastodon/features/collections/detail/revoke_collection_inclusion_modal').then(module => ({ default: module.RevokeCollectionInclusionModal })),
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
@@ -105,6 +105,7 @@ export const MODAL_COMPONENTS = {
|
||||
'ACCOUNT_EDIT_IMAGE_ALT': accountEditModal('ImageAltModal'),
|
||||
'ACCOUNT_EDIT_IMAGE_DELETE': accountEditModal('ImageDeleteModal'),
|
||||
'ACCOUNT_EDIT_IMAGE_UPLOAD': accountEditModal('ImageUploadModal'),
|
||||
'ACCOUNT_HIDE_FEATURED_TAB': () => import('@/mastodon/features/ui/components/confirmation_modals/hide_featured_tab').then(module => ({ default: module.ConfirmHideFeaturedTabModal })),
|
||||
};
|
||||
|
||||
/** @arg {keyof import('@/mastodon/features/account_edit/modals')} type */
|
||||
|
||||
@@ -232,6 +232,13 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
|
||||
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
|
||||
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />
|
||||
{areCollectionsEnabled() &&
|
||||
[
|
||||
<WrappedRoute path={['/@:acct/collections']} component={Collections} content={children} key='collections-list' />,
|
||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} key='collections-editor' />,
|
||||
<WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} key='collections-detail' />,
|
||||
]
|
||||
}
|
||||
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
|
||||
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
||||
<WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
|
||||
@@ -255,13 +262,6 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||
{areCollectionsEnabled() &&
|
||||
[
|
||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} key='collections-editor' />,
|
||||
<WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} key='collections-detail' />,
|
||||
<WrappedRoute path='/collections' component={Collections} content={children} key='collections-list' />
|
||||
]
|
||||
}
|
||||
<Route component={BundleColumnError} />
|
||||
</WrappedSwitch>
|
||||
</ColumnsArea>
|
||||
|
||||
Reference in new issue
Block a user