Profile redesign: Account featured tab (#38529)
Co-authored-by: diondiondion <mail@diondiondion.com>
This commit is contained in:
parent
8a5d6dab5f
commit
8da2fae6cc
1
app/javascript/images/elephant_ui_dark.svg
Normal file
1
app/javascript/images/elephant_ui_dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
1
app/javascript/images/elephant_ui_light.svg
Normal file
1
app/javascript/images/elephant_ui_light.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
@ -10,6 +10,13 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
svg,
|
||||
img {
|
||||
width: 240px;
|
||||
max-width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
|
||||
@ -9,10 +9,12 @@ import classes from './empty_state.module.scss';
|
||||
*/
|
||||
|
||||
export const EmptyState: React.FC<{
|
||||
title?: string | React.ReactElement;
|
||||
message?: string | React.ReactElement;
|
||||
image?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
message?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}> = ({
|
||||
image,
|
||||
title = (
|
||||
<FormattedMessage id='empty_state.no_results' defaultMessage='No results' />
|
||||
),
|
||||
@ -21,10 +23,13 @@ export const EmptyState: React.FC<{
|
||||
}) => {
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.content}>
|
||||
<h3>{title}</h3>
|
||||
{!!message && <p>{message}</p>}
|
||||
</div>
|
||||
{(title || message || image) && (
|
||||
<div className={classes.content}>
|
||||
{image}
|
||||
{!!title && <h3>{title}</h3>}
|
||||
{!!message && <p>{message}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import ElephantDarkImage from '@/images/elephant_ui_dark.svg?react';
|
||||
import ElephantLightImage from '@/images/elephant_ui_light.svg?react';
|
||||
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';
|
||||
|
||||
interface EmptyMessageProps {
|
||||
suspended: boolean;
|
||||
@ -19,21 +25,59 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
blockedBy,
|
||||
}) => {
|
||||
const { acct } = useParams<{ acct?: string }>();
|
||||
const me = useCurrentAccountId();
|
||||
const theme = useTheme();
|
||||
const ElephantImage =
|
||||
theme === 'dark' ? ElephantDarkImage : ElephantLightImage;
|
||||
|
||||
if (!accountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let title: React.ReactNode = null;
|
||||
let message: React.ReactNode = null;
|
||||
|
||||
const hasCollections = areCollectionsEnabled();
|
||||
|
||||
const image = <ElephantImage />;
|
||||
|
||||
if (me === accountId) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured.me'
|
||||
defaultMessage='You have not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?'
|
||||
/>
|
||||
);
|
||||
if (hasCollections) {
|
||||
// Return only here to insert the "Create a collection" button as the action for the empty state.
|
||||
return (
|
||||
<EmptyState
|
||||
image={image}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_self.no_collections'
|
||||
defaultMessage='No collections yet'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Link to='/collections/new' className='button'>
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_self.no_collections_button'
|
||||
defaultMessage='Create a collection'
|
||||
/>
|
||||
</Link>
|
||||
</EmptyState>
|
||||
);
|
||||
} else {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_self.pre_collections'
|
||||
defaultMessage='Stay tuned for Collections'
|
||||
/>
|
||||
);
|
||||
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.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (suspended) {
|
||||
message = (
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_suspended'
|
||||
defaultMessage='Account suspended'
|
||||
@ -42,32 +86,56 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
} else if (hidden) {
|
||||
message = <LimitedAccountHint accountId={accountId} />;
|
||||
} else if (blockedBy) {
|
||||
message = (
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_unavailable'
|
||||
defaultMessage='Profile unavailable'
|
||||
/>
|
||||
);
|
||||
} else if (acct) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured.other'
|
||||
defaultMessage='{acct} has not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?'
|
||||
values={{ acct }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
// Standard other account empty state.
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_other.unknown'
|
||||
defaultMessage='This account has not featured anything yet.'
|
||||
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.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
} 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.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='empty-column-indicator'>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
);
|
||||
return <EmptyState title={title} message={message} image={image} />;
|
||||
};
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { Hashtag } from 'mastodon/components/hashtag';
|
||||
|
||||
export type TagMap = ImmutableMap<
|
||||
'id' | 'name' | 'url' | 'statuses_count' | 'last_status_at' | 'accountId',
|
||||
string | null
|
||||
>;
|
||||
|
||||
interface FeaturedTagProps {
|
||||
tag: TagMap;
|
||||
account: string;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
lastStatusAt: {
|
||||
id: 'account.featured_tags.last_status_at',
|
||||
defaultMessage: 'Last post on {date}',
|
||||
},
|
||||
empty: {
|
||||
id: 'account.featured_tags.last_status_never',
|
||||
defaultMessage: 'No posts',
|
||||
},
|
||||
});
|
||||
|
||||
export const FeaturedTag: React.FC<FeaturedTagProps> = ({ tag, account }) => {
|
||||
const intl = useIntl();
|
||||
const name = tag.get('name') ?? '';
|
||||
const count = Number.parseInt(tag.get('statuses_count') ?? '');
|
||||
return (
|
||||
<Hashtag
|
||||
key={name}
|
||||
name={name}
|
||||
to={`/@${account}/tagged/${name}`}
|
||||
uses={count}
|
||||
withGraph={false}
|
||||
description={
|
||||
count > 0
|
||||
? intl.formatMessage(messages.lastStatusAt, {
|
||||
date: intl.formatDate(tag.get('last_status_at') ?? '', {
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
}),
|
||||
})
|
||||
: intl.formatMessage(messages.empty)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -8,7 +8,6 @@ import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
|
||||
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
@ -33,8 +32,6 @@ import { CollectionListItem } from '../collections/detail/collection_list_item';
|
||||
import { areCollectionsEnabled } from '../collections/utils';
|
||||
|
||||
import { EmptyMessage } from './components/empty_message';
|
||||
import { FeaturedTag } from './components/featured_tag';
|
||||
import type { TagMap } from './components/featured_tag';
|
||||
|
||||
const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
multiColumn,
|
||||
@ -55,26 +52,15 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId) {
|
||||
void dispatch(fetchFeaturedTags({ accountId }));
|
||||
void dispatch(fetchEndorsedAccounts({ accountId }));
|
||||
|
||||
if (areCollectionsEnabled()) {
|
||||
void dispatch(fetchAccountCollections({ accountId }));
|
||||
}
|
||||
}
|
||||
}, [accountId, dispatch]);
|
||||
|
||||
const isLoading = useAppSelector(
|
||||
(state) =>
|
||||
!accountId ||
|
||||
!!state.user_lists.getIn(['featured_tags', accountId, 'isLoading']),
|
||||
);
|
||||
const featuredTags = useAppSelector(
|
||||
(state) =>
|
||||
state.user_lists.getIn(
|
||||
['featured_tags', accountId, 'items'],
|
||||
ImmutableList(),
|
||||
) as ImmutableList<TagMap>,
|
||||
);
|
||||
const isLoading = !accountId;
|
||||
const featuredAccountIds = useAppSelector(
|
||||
(state) =>
|
||||
state.user_lists.getIn(
|
||||
@ -106,13 +92,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const noTags = featuredTags.isEmpty();
|
||||
|
||||
if (
|
||||
noTags &&
|
||||
featuredAccountIds.isEmpty() &&
|
||||
listedCollections.length === 0
|
||||
) {
|
||||
if (featuredAccountIds.isEmpty() && listedCollections.length === 0) {
|
||||
return (
|
||||
<AccountFeaturedWrapper accountId={accountId}>
|
||||
<EmptyMessage
|
||||
@ -156,28 +136,6 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
</ItemList>
|
||||
</>
|
||||
)}
|
||||
{!noTags && (
|
||||
<>
|
||||
<h4 className='column-subheading'>
|
||||
<FormattedMessage
|
||||
id='account.featured.hashtags'
|
||||
defaultMessage='Hashtags'
|
||||
/>
|
||||
</h4>
|
||||
<ItemList>
|
||||
{featuredTags.map((tag, index) => (
|
||||
<Article
|
||||
focusable
|
||||
key={tag.get('id')}
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={featuredTags.size}
|
||||
>
|
||||
<FeaturedTag tag={tag} account={account?.acct ?? ''} />
|
||||
</Article>
|
||||
))}
|
||||
</ItemList>
|
||||
</>
|
||||
)}
|
||||
{!featuredAccountIds.isEmpty() && (
|
||||
<>
|
||||
<h4 className='column-subheading'>
|
||||
|
||||
@ -8,6 +8,8 @@ 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 './redesign.module.scss';
|
||||
|
||||
const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
|
||||
@ -39,7 +41,14 @@ export const AccountTabs: FC = () => {
|
||||
)}
|
||||
{show_featured && (
|
||||
<NavLink exact to={`/@${acct}/featured`}>
|
||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||
{areCollectionsEnabled() ? (
|
||||
<FormattedMessage
|
||||
id='account.featured.collections'
|
||||
defaultMessage='Collections'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||
)}
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
|
||||
23
app/javascript/mastodon/hooks/useTheme.ts
Normal file
23
app/javascript/mastodon/hooks/useTheme.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { isDarkMode } from '../utils/theme';
|
||||
|
||||
export function useTheme() {
|
||||
const [darkMode, setDarkMode] = useState(() => isDarkMode());
|
||||
|
||||
useEffect(() => {
|
||||
const mutationObserver = new MutationObserver(() => {
|
||||
setDarkMode(isDarkMode());
|
||||
});
|
||||
mutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-color-scheme'],
|
||||
});
|
||||
|
||||
return () => {
|
||||
mutationObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return darkMode ? 'dark' : 'light';
|
||||
}
|
||||
@ -45,9 +45,6 @@
|
||||
"account.featured": "Featured",
|
||||
"account.featured.accounts": "Profiles",
|
||||
"account.featured.collections": "Collections",
|
||||
"account.featured.hashtags": "Hashtags",
|
||||
"account.featured_tags.last_status_at": "Last post on {date}",
|
||||
"account.featured_tags.last_status_never": "No posts",
|
||||
"account.field_overflow": "Show full content",
|
||||
"account.filters.all": "All activity",
|
||||
"account.filters.boosts_toggle": "Show boosts",
|
||||
@ -606,9 +603,15 @@
|
||||
"emoji_button.search_results": "Search results",
|
||||
"emoji_button.symbols": "Symbols",
|
||||
"emoji_button.travel": "Travel & Places",
|
||||
"empty_column.account_featured.me": "You have not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?",
|
||||
"empty_column.account_featured.other": "{acct} has not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?",
|
||||
"empty_column.account_featured_other.unknown": "This account has not featured anything yet.",
|
||||
"empty_column.account_featured_other.no_collections_desc": "{acct} hasn’t created any collections yet.",
|
||||
"empty_column.account_featured_other.title": "Nothing to see here",
|
||||
"empty_column.account_featured_self.no_collections": "No collections yet",
|
||||
"empty_column.account_featured_self.no_collections_button": "Create a collection",
|
||||
"empty_column.account_featured_self.pre_collections": "Stay tuned for Collections",
|
||||
"empty_column.account_featured_self.pre_collections_desc": "Collections (coming in Mastodon 4.6) allows you to create your own curated lists of accounts to recommend to others.",
|
||||
"empty_column.account_featured_unknown.no_collections_desc": "This account hasn’t created any collections yet.",
|
||||
"empty_column.account_featured_unknown.other": "This account hasn’t featured anything yet.",
|
||||
"empty_column.account_hides_collections": "This user has chosen to not make this information available",
|
||||
"empty_column.account_suspended": "Account suspended",
|
||||
"empty_column.account_timeline": "No posts here!",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user