mirror of
https://github.com/mastodon/mastodon
synced 2025-04-12 00:56:38 +02:00
Adds featured tab to web (#34405)
This commit is contained in:
parent
678c8dfeec
commit
d43bfa95aa
@ -102,7 +102,7 @@ export interface HashtagProps {
|
||||
description?: React.ReactNode;
|
||||
history?: number[];
|
||||
name: string;
|
||||
people: number;
|
||||
people?: number;
|
||||
to: string;
|
||||
uses?: number;
|
||||
withGraph?: boolean;
|
||||
|
@ -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 ? <Trends /> : null);
|
||||
|
||||
export const NavigationPortal: React.FC = () => (
|
||||
<div className='navigation-panel__portal'>
|
||||
<Switch>
|
||||
<Route path='/@:acct' exact component={AccountNavigation} />
|
||||
<Route
|
||||
path='/@:acct/tagged/:tagged?'
|
||||
exact
|
||||
component={AccountNavigation}
|
||||
/>
|
||||
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/followers' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/following' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/media' exact component={AccountNavigation} />
|
||||
<Route component={DefaultNavigation} />
|
||||
</Switch>
|
||||
</div>
|
||||
<div className='navigation-panel__portal'>{showTrends && <Trends />}</div>
|
||||
);
|
||||
|
43
app/javascript/mastodon/components/remote_hint.tsx
Normal file
43
app/javascript/mastodon/components/remote_hint.tsx
Normal file
@ -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<RemoteHintProps> = ({ 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 (
|
||||
<TimelineHint
|
||||
url={account.url}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id='hints.profiles.posts_may_be_missing'
|
||||
defaultMessage='Some posts from this profile may be missing.'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='hints.profiles.see_more_posts'
|
||||
defaultMessage='See more posts on {domain}'
|
||||
values={{ domain: <strong>{domain}</strong> }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<div className='getting-started__trends'>
|
||||
<h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4>
|
||||
|
||||
{featuredTags.take(3).map(featuredTag => (
|
||||
<Hashtag
|
||||
key={featuredTag.get('name')}
|
||||
name={featuredTag.get('name')}
|
||||
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
|
||||
uses={featuredTag.get('statuses_count') * 1}
|
||||
withGraph={false}
|
||||
description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(FeaturedTags);
|
@ -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);
|
@ -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 (
|
||||
<FeaturedTags accountId={accountId} tagged={tagged} />
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AccountNavigation);
|
@ -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<EmptyMessageProps> = ({
|
||||
accountId,
|
||||
suspended,
|
||||
hidden,
|
||||
blockedBy,
|
||||
}) => {
|
||||
if (!accountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let message: React.ReactNode = null;
|
||||
|
||||
if (suspended) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_suspended'
|
||||
defaultMessage='Account suspended'
|
||||
/>
|
||||
);
|
||||
} else if (hidden) {
|
||||
message = <LimitedAccountHint accountId={accountId} />;
|
||||
} else if (blockedBy) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_unavailable'
|
||||
defaultMessage='Profile unavailable'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured'
|
||||
defaultMessage='This list is empty'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className='empty-column-indicator'>{message}</div>;
|
||||
};
|
@ -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<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',
|
||||
}),
|
||||
})
|
||||
: intl.formatMessage(messages.empty)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
156
app/javascript/mastodon/features/account_featured/index.tsx
Normal file
156
app/javascript/mastodon/features/account_featured/index.tsx
Normal file
@ -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<Params>();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId) {
|
||||
void dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
dispatch(fetchFeaturedTags(accountId));
|
||||
}
|
||||
}, [accountId, dispatch]);
|
||||
|
||||
const isLoading = useAppSelector(
|
||||
(state) =>
|
||||
!accountId ||
|
||||
!!(state.timelines as ImmutableMap<string, unknown>).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<TagMap>,
|
||||
);
|
||||
const featuredStatusIds = useAppSelector(
|
||||
(state) =>
|
||||
(state.timelines as ImmutableMap<string, unknown>).getIn(
|
||||
[`account:${accountId}:pinned`, 'items'],
|
||||
ImmutableList(),
|
||||
) as ImmutableList<string>,
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<AccountFeaturedWrapper accountId={accountId}>
|
||||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
</AccountFeaturedWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (featuredStatusIds.isEmpty() && featuredTags.isEmpty()) {
|
||||
return (
|
||||
<AccountFeaturedWrapper accountId={accountId}>
|
||||
<EmptyMessage
|
||||
blockedBy={blockedBy}
|
||||
hidden={hidden}
|
||||
suspended={suspended}
|
||||
accountId={accountId}
|
||||
/>
|
||||
<RemoteHint accountId={accountId} />
|
||||
</AccountFeaturedWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
|
||||
<div className='scrollable scrollable--flex'>
|
||||
{accountId && (
|
||||
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
|
||||
)}
|
||||
{!featuredTags.isEmpty() && (
|
||||
<>
|
||||
<h4 className='column-subheading'>
|
||||
<FormattedMessage
|
||||
id='account.featured.hashtags'
|
||||
defaultMessage='Hashtags'
|
||||
/>
|
||||
</h4>
|
||||
{featuredTags.map((tag) => (
|
||||
<FeaturedTag key={tag.get('id')} tag={tag} account={acct} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{!featuredStatusIds.isEmpty() && (
|
||||
<>
|
||||
<h4 className='column-subheading'>
|
||||
<FormattedMessage
|
||||
id='account.featured.posts'
|
||||
defaultMessage='Posts'
|
||||
/>
|
||||
</h4>
|
||||
{featuredStatusIds.map((statusId) => (
|
||||
<StatusContainer
|
||||
key={`f-${statusId}`}
|
||||
// @ts-expect-error inferred props are wrong
|
||||
id={statusId}
|
||||
contextType='account'
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<RemoteHint accountId={accountId} />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountFeaturedWrapper = ({
|
||||
children,
|
||||
accountId,
|
||||
}: React.PropsWithChildren<{ accountId?: string }>) => {
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
<div className='scrollable scrollable--flex'>
|
||||
{accountId && <AccountHeader accountId={accountId} />}
|
||||
{children}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AccountFeatured;
|
@ -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 (
|
||||
<TimelineHint
|
||||
url={url}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id='hints.profiles.posts_may_be_missing'
|
||||
defaultMessage='Some posts from this profile may be missing.'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='hints.profiles.see_more_posts'
|
||||
defaultMessage='See more posts on {domain}'
|
||||
values={{ domain: <strong>{domain}</strong> }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountGallery: React.FC<{
|
||||
multiColumn: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const { acct, id } = useParams<Params>();
|
||||
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 = <RemoteHint accountId={accountId} />;
|
||||
} else {
|
||||
emptyMessage = (
|
||||
@ -259,7 +196,7 @@ export const AccountGallery: React.FC<{
|
||||
)
|
||||
}
|
||||
alwaysPrepend
|
||||
append={remote && accountId && <RemoteHint accountId={accountId} />}
|
||||
append={accountId && <RemoteHint accountId={accountId} />}
|
||||
scrollKey='account_gallery'
|
||||
isLoading={isLoading}
|
||||
hasMore={!forceEmptyState && hasMore}
|
||||
|
@ -956,6 +956,9 @@ export const AccountHeader: React.FC<{
|
||||
|
||||
{!(hideTabs || hidden) && (
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to={`/@${account.acct}/featured`}>
|
||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||
</NavLink>
|
||||
<NavLink exact to={`/@${account.acct}`}>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||
</NavLink>
|
||||
|
@ -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 (
|
||||
<TimelineHint
|
||||
url={url}
|
||||
message={<FormattedMessage id='hints.profiles.posts_may_be_missing' defaultMessage='Some posts from this profile may be missing.' />}
|
||||
label={<FormattedMessage id='hints.profiles.see_more_posts' defaultMessage='See more posts on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
|
||||
}
|
||||
|
||||
const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
@ -200,10 +173,9 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
<StatusList
|
||||
prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
||||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
append={<RemoteHint accountId={accountId} />}
|
||||
scrollKey='account_timeline'
|
||||
statusIds={forceEmptyState ? emptyList : statusIds}
|
||||
featuredStatusIds={featuredStatusIds}
|
||||
isLoading={isLoading}
|
||||
hasMore={!forceEmptyState && hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
|
@ -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 {
|
||||
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
||||
|
||||
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
|
||||
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />
|
||||
<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} />
|
||||
|
@ -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');
|
||||
}
|
||||
|
37
app/javascript/mastodon/hooks/useAccountId.ts
Normal file
37
app/javascript/mastodon/hooks/useAccountId.ts
Normal file
@ -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<Params>();
|
||||
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;
|
||||
}
|
20
app/javascript/mastodon/hooks/useAccountVisibility.ts
Normal file
20
app/javascript/mastodon/hooks/useAccountVisibility.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
@ -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!",
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user