diff --git a/app/javascript/mastodon/components/hashtag.tsx b/app/javascript/mastodon/components/hashtag.tsx index 1fe41e1e8b9..346c95183f9 100644 --- a/app/javascript/mastodon/components/hashtag.tsx +++ b/app/javascript/mastodon/components/hashtag.tsx @@ -102,7 +102,7 @@ export interface HashtagProps { description?: React.ReactNode; history?: number[]; name: string; - people: number; + people?: number; to: string; uses?: number; withGraph?: boolean; diff --git a/app/javascript/mastodon/components/navigation_portal.tsx b/app/javascript/mastodon/components/navigation_portal.tsx index 08f91ce18aa..d3ac8baa6e3 100644 --- a/app/javascript/mastodon/components/navigation_portal.tsx +++ b/app/javascript/mastodon/components/navigation_portal.tsx @@ -1,25 +1,6 @@ -import { Switch, Route } from 'react-router-dom'; - -import AccountNavigation from 'mastodon/features/account/navigation'; import Trends from 'mastodon/features/getting_started/containers/trends_container'; import { showTrends } from 'mastodon/initial_state'; -const DefaultNavigation: React.FC = () => (showTrends ? : null); - export const NavigationPortal: React.FC = () => ( -
- - - - - - - - - -
+
{showTrends && }
); diff --git a/app/javascript/mastodon/components/remote_hint.tsx b/app/javascript/mastodon/components/remote_hint.tsx new file mode 100644 index 00000000000..772aa805db6 --- /dev/null +++ b/app/javascript/mastodon/components/remote_hint.tsx @@ -0,0 +1,43 @@ +import { FormattedMessage } from 'react-intl'; + +import { useAppSelector } from 'mastodon/store'; + +import { TimelineHint } from './timeline_hint'; + +interface RemoteHintProps { + accountId?: string; +} + +export const RemoteHint: React.FC = ({ accountId }) => { + const account = useAppSelector((state) => + accountId ? state.accounts.get(accountId) : undefined, + ); + const domain = account?.acct ? account.acct.split('@')[1] : undefined; + if ( + !account || + !account.url || + account.acct !== account.username || + !domain + ) { + return null; + } + + return ( + + } + label={ + {domain} }} + /> + } + /> + ); +}; diff --git a/app/javascript/mastodon/features/account/components/featured_tags.jsx b/app/javascript/mastodon/features/account/components/featured_tags.jsx deleted file mode 100644 index 56a9efac022..00000000000 --- a/app/javascript/mastodon/features/account/components/featured_tags.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import { Hashtag } from 'mastodon/components/hashtag'; - -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' }, -}); - -class FeaturedTags extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.record, - featuredTags: ImmutablePropTypes.list, - tagged: PropTypes.string, - intl: PropTypes.object.isRequired, - }; - - render () { - const { account, featuredTags, intl } = this.props; - - if (!account || account.get('suspended') || featuredTags.isEmpty()) { - return null; - } - - return ( -
-

}} />

- - {featuredTags.take(3).map(featuredTag => ( - 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)} - /> - ))} -
- ); - } - -} - -export default injectIntl(FeaturedTags); diff --git a/app/javascript/mastodon/features/account/containers/featured_tags_container.js b/app/javascript/mastodon/features/account/containers/featured_tags_container.js deleted file mode 100644 index 726c805f782..00000000000 --- a/app/javascript/mastodon/features/account/containers/featured_tags_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { List as ImmutableList } from 'immutable'; -import { connect } from 'react-redux'; - -import { makeGetAccount } from 'mastodon/selectors'; - -import FeaturedTags from '../components/featured_tags'; - -const mapStateToProps = () => { - const getAccount = makeGetAccount(); - - return (state, { accountId }) => ({ - account: getAccount(state, accountId), - featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()), - }); -}; - -export default connect(mapStateToProps)(FeaturedTags); diff --git a/app/javascript/mastodon/features/account/navigation.jsx b/app/javascript/mastodon/features/account/navigation.jsx deleted file mode 100644 index aa78135de24..00000000000 --- a/app/javascript/mastodon/features/account/navigation.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { connect } from 'react-redux'; - -import FeaturedTags from 'mastodon/features/account/containers/featured_tags_container'; -import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; - -const mapStateToProps = (state, { match: { params: { acct } } }) => { - const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]); - - if (!accountId) { - return { - isLoading: true, - }; - } - - return { - accountId, - isLoading: false, - }; -}; - -class AccountNavigation extends PureComponent { - - static propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape({ - acct: PropTypes.string, - tagged: PropTypes.string, - }).isRequired, - }).isRequired, - - accountId: PropTypes.string, - isLoading: PropTypes.bool, - }; - - render () { - const { accountId, isLoading, match: { params: { tagged } } } = this.props; - - if (isLoading) { - return null; - } - - return ( - - ); - } - -} - -export default connect(mapStateToProps)(AccountNavigation); diff --git a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx new file mode 100644 index 00000000000..9dd8ffdfe07 --- /dev/null +++ b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx @@ -0,0 +1,50 @@ +import { FormattedMessage } from 'react-intl'; + +import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint'; + +interface EmptyMessageProps { + suspended: boolean; + hidden: boolean; + blockedBy: boolean; + accountId?: string; +} + +export const EmptyMessage: React.FC = ({ + accountId, + suspended, + hidden, + blockedBy, +}) => { + if (!accountId) { + return null; + } + + let message: React.ReactNode = null; + + if (suspended) { + message = ( + + ); + } else if (hidden) { + message = ; + } else if (blockedBy) { + message = ( + + ); + } else { + message = ( + + ); + } + + return
{message}
; +}; diff --git a/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx b/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx new file mode 100644 index 00000000000..7b476ba01d8 --- /dev/null +++ b/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx @@ -0,0 +1,51 @@ +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 = ({ tag, account }) => { + const intl = useIntl(); + const name = tag.get('name') ?? ''; + const count = Number.parseInt(tag.get('statuses_count') ?? ''); + return ( + 0 + ? intl.formatMessage(messages.lastStatusAt, { + date: intl.formatDate(tag.get('last_status_at') ?? '', { + month: 'short', + day: '2-digit', + }), + }) + : intl.formatMessage(messages.empty) + } + /> + ); +}; diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx new file mode 100644 index 00000000000..70e411f61a3 --- /dev/null +++ b/app/javascript/mastodon/features/account_featured/index.tsx @@ -0,0 +1,156 @@ +import { useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { useParams } from 'react-router'; + +import type { Map as ImmutableMap } from 'immutable'; +import { List as ImmutableList } from 'immutable'; + +import { fetchFeaturedTags } from 'mastodon/actions/featured_tags'; +import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines'; +import { ColumnBackButton } from 'mastodon/components/column_back_button'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; +import { RemoteHint } from 'mastodon/components/remote_hint'; +import StatusContainer from 'mastodon/containers/status_container'; +import { useAccountId } from 'mastodon/hooks/useAccountId'; +import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +import { AccountHeader } from '../account_timeline/components/account_header'; +import Column from '../ui/components/column'; + +import { EmptyMessage } from './components/empty_message'; +import { FeaturedTag } from './components/featured_tag'; +import type { TagMap } from './components/featured_tag'; + +interface Params { + acct?: string; + id?: string; +} + +const AccountFeatured = () => { + const accountId = useAccountId(); + const { suspended, blockedBy, hidden } = useAccountVisibility(accountId); + const forceEmptyState = suspended || blockedBy || hidden; + const { acct = '' } = useParams(); + + const dispatch = useAppDispatch(); + + useEffect(() => { + if (accountId) { + void dispatch(expandAccountFeaturedTimeline(accountId)); + dispatch(fetchFeaturedTags(accountId)); + } + }, [accountId, dispatch]); + + const isLoading = useAppSelector( + (state) => + !accountId || + !!(state.timelines as ImmutableMap).getIn([ + `account:${accountId}:pinned`, + 'isLoading', + ]) || + !!state.user_lists.getIn(['featured_tags', accountId, 'isLoading']), + ); + const featuredTags = useAppSelector( + (state) => + state.user_lists.getIn( + ['featured_tags', accountId, 'items'], + ImmutableList(), + ) as ImmutableList, + ); + const featuredStatusIds = useAppSelector( + (state) => + (state.timelines as ImmutableMap).getIn( + [`account:${accountId}:pinned`, 'items'], + ImmutableList(), + ) as ImmutableList, + ); + + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + if (featuredStatusIds.isEmpty() && featuredTags.isEmpty()) { + return ( + + + ); + } + + return ( + + + +
+ {accountId && ( + + )} + {!featuredTags.isEmpty() && ( + <> +

+ +

+ {featuredTags.map((tag) => ( + + ))} + + )} + {!featuredStatusIds.isEmpty() && ( + <> +

+ +

+ {featuredStatusIds.map((statusId) => ( + + ))} + + )} + +
+
+ ); +}; + +const AccountFeaturedWrapper = ({ + children, + accountId, +}: React.PropsWithChildren<{ accountId?: string }>) => { + return ( + + +
+ {accountId && } + {children} +
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default AccountFeatured; diff --git a/app/javascript/mastodon/features/account_gallery/index.tsx b/app/javascript/mastodon/features/account_gallery/index.tsx index 60afdadc813..0027329c93d 100644 --- a/app/javascript/mastodon/features/account_gallery/index.tsx +++ b/app/javascript/mastodon/features/account_gallery/index.tsx @@ -2,25 +2,22 @@ import { useEffect, useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useParams } from 'react-router-dom'; - import { createSelector } from '@reduxjs/toolkit'; import type { Map as ImmutableMap } from 'immutable'; import { List as ImmutableList } from 'immutable'; -import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; import { expandAccountMediaTimeline } from 'mastodon/actions/timelines'; import { ColumnBackButton } from 'mastodon/components/column_back_button'; +import { RemoteHint } from 'mastodon/components/remote_hint'; import ScrollableList from 'mastodon/components/scrollable_list'; -import { TimelineHint } from 'mastodon/components/timeline_hint'; import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header'; import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import Column from 'mastodon/features/ui/components/column'; +import { useAccountId } from 'mastodon/hooks/useAccountId'; +import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility'; import type { MediaAttachment } from 'mastodon/models/media_attachment'; -import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; -import { getAccountHidden } from 'mastodon/selectors/accounts'; import type { RootState } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; @@ -56,53 +53,11 @@ const getAccountGallery = createSelector( }, ); -interface Params { - acct?: string; - id?: string; -} - -const RemoteHint: React.FC<{ - accountId: string; -}> = ({ accountId }) => { - const account = useAppSelector((state) => state.accounts.get(accountId)); - const acct = account?.acct; - const url = account?.url; - const domain = acct ? acct.split('@')[1] : undefined; - - if (!url) { - return null; - } - - return ( - - } - label={ - {domain} }} - /> - } - /> - ); -}; - export const AccountGallery: React.FC<{ multiColumn: boolean; }> = ({ multiColumn }) => { - const { acct, id } = useParams(); const dispatch = useAppDispatch(); - const accountId = useAppSelector( - (state) => - id ?? - (state.accounts_map.get(normalizeForLookup(acct)) as string | undefined), - ); + const accountId = useAccountId(); const attachments = useAppSelector((state) => accountId ? getAccountGallery(state, accountId) @@ -123,33 +78,15 @@ export const AccountGallery: React.FC<{ const account = useAppSelector((state) => accountId ? state.accounts.get(accountId) : undefined, ); - const blockedBy = useAppSelector( - (state) => - state.relationships.getIn([accountId, 'blocked_by'], false) as boolean, - ); - const suspended = useAppSelector( - (state) => state.accounts.getIn([accountId, 'suspended'], false) as boolean, - ); const isAccount = !!account; - const remote = account?.acct !== account?.username; - const hidden = useAppSelector((state) => - accountId ? getAccountHidden(state, accountId) : false, - ); + + const { suspended, blockedBy, hidden } = useAccountVisibility(accountId); + const maxId = attachments.last()?.getIn(['status', 'id']) as | string | undefined; useEffect(() => { - if (!accountId) { - dispatch(lookupAccount(acct)); - } - }, [dispatch, accountId, acct]); - - useEffect(() => { - if (accountId && !isAccount) { - dispatch(fetchAccount(accountId)); - } - if (accountId && isAccount) { void dispatch(expandAccountMediaTimeline(accountId)); } @@ -233,7 +170,7 @@ export const AccountGallery: React.FC<{ defaultMessage='Profile unavailable' /> ); - } else if (remote && attachments.isEmpty()) { + } else if (attachments.isEmpty()) { emptyMessage = ; } else { emptyMessage = ( @@ -259,7 +196,7 @@ export const AccountGallery: React.FC<{ ) } alwaysPrepend - append={remote && accountId && } + append={accountId && } scrollKey='account_gallery' isLoading={isLoading} hasMore={!forceEmptyState && hasMore} diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index ca12834528d..c8fb3d2ae76 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -956,6 +956,9 @@ export const AccountHeader: React.FC<{ {!(hideTabs || hidden) && (
+ + + diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx index 886191e668d..a5223275b36 100644 --- a/app/javascript/mastodon/features/account_timeline/index.jsx +++ b/app/javascript/mastodon/features/account_timeline/index.jsx @@ -7,12 +7,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import { TimelineHint } from 'mastodon/components/timeline_hint'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import { me } from 'mastodon/initial_state'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { getAccountHidden } from 'mastodon/selectors/accounts'; -import { useAppSelector } from 'mastodon/store'; import { lookupAccount, fetchAccount } from '../../actions/accounts'; import { fetchFeaturedTags } from '../../actions/featured_tags'; @@ -21,6 +19,7 @@ import { ColumnBackButton } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import StatusList from '../../components/status_list'; import Column from '../ui/components/column'; +import { RemoteHint } from 'mastodon/components/remote_hint'; import { AccountHeader } from './components/account_header'; import { LimitedAccountHint } from './components/limited_account_hint'; @@ -47,11 +46,8 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa return { accountId, - remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])), - remoteUrl: state.getIn(['accounts', accountId, 'url']), isAccount: !!state.getIn(['accounts', accountId]), statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList), - featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], emptyList), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), suspended: state.getIn(['accounts', accountId, 'suspended'], false), @@ -60,24 +56,6 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa }; }; -const RemoteHint = ({ accountId, url }) => { - const acct = useAppSelector(state => state.accounts.get(accountId)?.acct); - const domain = acct ? acct.split('@')[1] : undefined; - - return ( - } - label={{domain} }} />} - /> - ); -}; - -RemoteHint.propTypes = { - url: PropTypes.string.isRequired, - accountId: PropTypes.string.isRequired, -}; - class AccountTimeline extends ImmutablePureComponent { static propTypes = { @@ -89,7 +67,6 @@ class AccountTimeline extends ImmutablePureComponent { accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, statusIds: ImmutablePropTypes.list, - featuredStatusIds: ImmutablePropTypes.list, isLoading: PropTypes.bool, hasMore: PropTypes.bool, withReplies: PropTypes.bool, @@ -97,8 +74,6 @@ class AccountTimeline extends ImmutablePureComponent { isAccount: PropTypes.bool, suspended: PropTypes.bool, hidden: PropTypes.bool, - remote: PropTypes.bool, - remoteUrl: PropTypes.string, multiColumn: PropTypes.bool, }; @@ -161,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent { }; render () { - const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props; + const { accountId, statusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props; if (isLoading && statusIds.isEmpty()) { return ( @@ -191,8 +166,6 @@ class AccountTimeline extends ImmutablePureComponent { emptyMessage = ; } - const remoteMessage = remote ? : null; - return ( @@ -200,10 +173,9 @@ class AccountTimeline extends ImmutablePureComponent { } alwaysPrepend - append={remoteMessage} + append={} scrollKey='account_timeline' statusIds={forceEmptyState ? emptyList : statusIds} - featuredStatusIds={featuredStatusIds} isLoading={isLoading} hasMore={!forceEmptyState && hasMore} onLoadMore={this.handleLoadMore} diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index a1cb8212d23..bb9720c17f6 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -73,6 +73,7 @@ import { About, PrivacyPolicy, TermsOfService, + AccountFeatured, } from './util/async-components'; import { ColumnsContextProvider } from './util/columns_context'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; @@ -236,6 +237,7 @@ class SwitchingColumnsArea extends PureComponent { + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 8c3b3427785..ec493ae283e 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -66,6 +66,10 @@ export function AccountGallery () { return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery'); } +export function AccountFeatured() { + return import(/* webpackChunkName: "features/account_featured" */'../../account_featured'); +} + export function Followers () { return import(/* webpackChunkName: "features/followers" */'../../followers'); } diff --git a/app/javascript/mastodon/hooks/useAccountId.ts b/app/javascript/mastodon/hooks/useAccountId.ts new file mode 100644 index 00000000000..1cc819ca592 --- /dev/null +++ b/app/javascript/mastodon/hooks/useAccountId.ts @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; + +import { useParams } from 'react-router'; + +import { fetchAccount, lookupAccount } from 'mastodon/actions/accounts'; +import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +interface Params { + acct?: string; + id?: string; +} + +export function useAccountId() { + const { acct, id } = useParams(); + const accountId = useAppSelector( + (state) => + id ?? + (state.accounts_map.get(normalizeForLookup(acct)) as string | undefined), + ); + + const account = useAppSelector((state) => + accountId ? state.accounts.get(accountId) : undefined, + ); + const isAccount = !!account; + + const dispatch = useAppDispatch(); + useEffect(() => { + if (!accountId) { + dispatch(lookupAccount(acct)); + } else if (!isAccount) { + dispatch(fetchAccount(accountId)); + } + }, [dispatch, accountId, acct, isAccount]); + + return accountId; +} diff --git a/app/javascript/mastodon/hooks/useAccountVisibility.ts b/app/javascript/mastodon/hooks/useAccountVisibility.ts new file mode 100644 index 00000000000..55651af5a0f --- /dev/null +++ b/app/javascript/mastodon/hooks/useAccountVisibility.ts @@ -0,0 +1,20 @@ +import { getAccountHidden } from 'mastodon/selectors/accounts'; +import { useAppSelector } from 'mastodon/store'; + +export function useAccountVisibility(accountId?: string) { + const blockedBy = useAppSelector( + (state) => !!state.relationships.getIn([accountId, 'blocked_by'], false), + ); + const suspended = useAppSelector( + (state) => !!state.accounts.getIn([accountId, 'suspended'], false), + ); + const hidden = useAppSelector((state) => + accountId ? Boolean(getAccountHidden(state, accountId)) : false, + ); + + return { + blockedBy, + suspended, + hidden, + }; +} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index ebd5412cf24..0a0f043b4d5 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -27,9 +27,11 @@ "account.edit_profile": "Edit profile", "account.enable_notifications": "Notify me when @{name} posts", "account.endorse": "Feature on profile", + "account.featured": "Featured", + "account.featured.hashtags": "Hashtags", + "account.featured.posts": "Posts", "account.featured_tags.last_status_at": "Last post on {date}", "account.featured_tags.last_status_never": "No posts", - "account.featured_tags.title": "{name}'s featured hashtags", "account.follow": "Follow", "account.follow_back": "Follow back", "account.followers": "Followers", @@ -294,6 +296,7 @@ "emoji_button.search_results": "Search results", "emoji_button.symbols": "Symbols", "emoji_button.travel": "Travel & Places", + "empty_column.account_featured": "This list is empty", "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!", diff --git a/config/routes.rb b/config/routes.rb index 5b130c517bd..2fff44851e0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -129,6 +129,7 @@ Rails.application.routes.draw do constraints(username: %r{[^@/.]+}) do with_options to: 'accounts#show' do get '/@:username', as: :short_account + get '/@:username/featured' get '/@:username/with_replies', as: :short_account_with_replies get '/@:username/media', as: :short_account_media get '/@:username/tagged/:tag', as: :short_account_tag