+
+
+
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