Profile redesign: Profile tab settings (#38309)

This commit is contained in:
Echo 2026-03-20 14:38:10 +01:00 committed by GitHub
parent 00bcb014df
commit 6507a61d30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 129 additions and 72 deletions

View File

@ -158,7 +158,7 @@ export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } =
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia });
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandAccountMediaTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}:media${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40, exclude_replies: !withReplies });
export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId });
export const expandLinkTimeline = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId });
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}) => {

View File

@ -53,6 +53,9 @@ export interface BaseApiAccountJSON {
id: string;
last_status_at: string;
locked: boolean;
show_media: boolean;
show_media_replies: boolean;
show_featured: boolean;
noindex?: boolean;
note: string;
roles?: ApiAccountJSON[];

View File

@ -2,10 +2,12 @@ import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router';
import { useHistory } from 'react-router';
import { List as ImmutableList } from 'immutable';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
import { Account } from 'mastodon/components/account';
@ -35,21 +37,27 @@ 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: React.FC<{ multiColumn: boolean }> = ({
multiColumn,
}) => {
const accountId = useAccountId();
const account = useAccount(accountId);
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
const forceEmptyState = suspended || blockedBy || hidden;
const { acct = '' } = useParams<Params>();
const dispatch = useAppDispatch();
const history = useHistory();
useEffect(() => {
if (
account &&
!account.show_featured &&
isServerFeatureEnabled('profile_redesign')
) {
history.push(`/@${account.acct}`);
}
}, [account, history]);
useEffect(() => {
if (accountId) {
void dispatch(fetchFeaturedTags({ accountId }));
@ -166,7 +174,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
aria-posinset={index + 1}
aria-setsize={featuredTags.size}
>
<FeaturedTag tag={tag} account={acct} />
<FeaturedTag tag={tag} account={account?.acct ?? ''} />
</Article>
))}
</ItemList>

View File

@ -2,10 +2,9 @@ import { useEffect, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit';
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import { List as ImmutableList, isList } from 'immutable';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { openModal } from 'mastodon/actions/modal';
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
@ -18,38 +17,69 @@ 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 type { RootState } from 'mastodon/store';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import {
useAppSelector,
useAppDispatch,
createAppSelector,
} from 'mastodon/store';
import { MediaItem } from './components/media_item';
const getAccountGallery = createSelector(
const emptyList = ImmutableList<MediaAttachment>();
const redesignEnabled = isServerFeatureEnabled('profile_redesign');
const selectGalleryTimeline = createAppSelector(
[
(state: RootState, accountId: string) =>
(state.timelines as ImmutableMap<string, unknown>).getIn(
[`account:${accountId}:media`, 'items'],
ImmutableList(),
) as ImmutableList<string>,
(state: RootState) => state.statuses,
(_state, accountId?: string | null) => accountId,
(state) => state.timelines,
(state) => state.accounts,
(state) => state.statuses,
],
(statusIds, statuses) => {
let items = ImmutableList<MediaAttachment>();
(accountId, timelines, accounts, statuses) => {
if (!accountId) {
return null;
}
const account = accounts.get(accountId);
if (!account) {
return null;
}
statusIds.forEach((statusId) => {
const status = statuses.get(statusId) as
| ImmutableMap<string, unknown>
| undefined;
let items = emptyList;
const { show_media, show_media_replies } = account;
// If the account disabled showing media, don't display anything.
if (!show_media && redesignEnabled) {
return {
items,
hasMore: false,
isLoading: false,
showingReplies: false,
};
}
if (status) {
const showingReplies = show_media_replies && redesignEnabled;
const timeline = timelines.get(
`account:${accountId}:media${showingReplies ? ':with_replies' : ''}`,
);
const statusIds = timeline?.get('items');
if (isList(statusIds)) {
for (const statusId of statusIds) {
const status = statuses.get(statusId);
items = items.concat(
(
status.get('media_attachments') as ImmutableList<MediaAttachment>
status?.get('media_attachments') as ImmutableList<MediaAttachment>
).map((media) => media.set('status', status)),
);
}
});
}
return items;
return {
items,
hasMore: !!timeline?.get('hasMore'),
isLoading: !!timeline?.get('isLoading'),
showingReplies,
};
},
);
@ -58,27 +88,12 @@ export const AccountGallery: React.FC<{
}> = ({ multiColumn }) => {
const dispatch = useAppDispatch();
const accountId = useAccountId();
const attachments = useAppSelector((state) =>
accountId
? getAccountGallery(state, accountId)
: ImmutableList<MediaAttachment>(),
);
const isLoading = useAppSelector((state) =>
(state.timelines as ImmutableMap<string, unknown>).getIn([
`account:${accountId}:media`,
'isLoading',
]),
);
const hasMore = useAppSelector((state) =>
(state.timelines as ImmutableMap<string, unknown>).getIn([
`account:${accountId}:media`,
'hasMore',
]),
);
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const isAccount = !!account;
const {
isLoading = true,
hasMore = false,
items: attachments = emptyList,
showingReplies: withReplies = false,
} = useAppSelector((state) => selectGalleryTimeline(state, accountId)) ?? {};
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
@ -87,16 +102,18 @@ export const AccountGallery: React.FC<{
| undefined;
useEffect(() => {
if (accountId && isAccount) {
void dispatch(expandAccountMediaTimeline(accountId));
if (accountId) {
void dispatch(expandAccountMediaTimeline(accountId, { withReplies }));
}
}, [dispatch, accountId, isAccount]);
}, [dispatch, accountId, withReplies]);
const handleLoadMore = useCallback(() => {
if (maxId) {
void dispatch(expandAccountMediaTimeline(accountId, { maxId }));
void dispatch(
expandAccountMediaTimeline(accountId, { maxId, withReplies }),
);
}
}, [dispatch, accountId, maxId]);
}, [maxId, dispatch, accountId, withReplies]);
const handleOpenMedia = useCallback(
(attachment: MediaAttachment) => {

View File

@ -5,25 +5,16 @@ import { FormattedMessage } from 'react-intl';
import type { NavLinkProps } from 'react-router-dom';
import { NavLink } from 'react-router-dom';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss';
export const AccountTabs: FC<{ acct: string }> = ({ acct }) => {
if (isRedesignEnabled()) {
return (
<div className={classes.tabs}>
<NavLink isActive={isActive} to={`/@${acct}`}>
<FormattedMessage id='account.activity' defaultMessage='Activity' />
</NavLink>
<NavLink exact to={`/@${acct}/media`}>
<FormattedMessage id='account.media' defaultMessage='Media' />
</NavLink>
<NavLink exact to={`/@${acct}/featured`}>
<FormattedMessage id='account.featured' defaultMessage='Featured' />
</NavLink>
</div>
);
return <RedesignTabs />;
}
return (
<div className='account__section-headline'>
@ -49,3 +40,32 @@ export const AccountTabs: FC<{ acct: string }> = ({ acct }) => {
const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
match?.url === location.pathname ||
(!!match?.url && location.pathname.startsWith(`${match.url}/tagged/`));
const RedesignTabs: FC = () => {
const accountId = useAccountId();
const account = useAccount(accountId);
if (!account) {
return null;
}
const { acct, show_featured, show_media } = account;
return (
<div className={classes.tabs}>
<NavLink isActive={isActive} to={`/@${acct}`}>
<FormattedMessage id='account.activity' defaultMessage='Activity' />
</NavLink>
{show_media && (
<NavLink exact to={`/@${acct}/media`}>
<FormattedMessage id='account.media' defaultMessage='Media' />
</NavLink>
)}
{show_featured && (
<NavLink exact to={`/@${acct}/featured`}>
<FormattedMessage id='account.featured' defaultMessage='Featured' />
</NavLink>
)}
</div>
);
};

View File

@ -21,8 +21,7 @@ export const LinkTimeline: React.FC<{
const columnRef = useRef<ColumnRef>(null);
const firstStatusId = useAppSelector((state) =>
decodedUrl
? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
(state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
? (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
: undefined,
);
const story = useAppSelector((state) =>

View File

@ -82,6 +82,9 @@ export const accountDefaultValues: AccountShape = {
last_status_at: '',
locked: false,
noindex: false,
show_featured: true,
show_media: true,
show_media_replies: true,
note: '',
note_emojified: '',
note_plain: 'string',

View File

@ -28,6 +28,7 @@ import {
} from '../actions/timelines_typed';
import { compareId } from '../compare_id';
/** @type {ImmutableMap<string, typeof initialTimeline>} */
const initialState = ImmutableMap();
const initialTimeline = ImmutableMap({
@ -36,7 +37,9 @@ const initialTimeline = ImmutableMap({
top: true,
isLoading: false,
hasMore: true,
/** @type {ImmutableList<string>} */
pendingItems: ImmutableList(),
/** @type {ImmutableList<string>} */
items: ImmutableList(),
});
@ -197,6 +200,7 @@ const reconnectTimeline = (state, usePendingItems) => {
});
};
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
export default function timelines(state = initialState, action) {
switch(action.type) {
case TIMELINE_LOAD_PENDING:

View File

@ -45,6 +45,9 @@ export const accountFactory: FactoryFunction<ApiAccountJSON> = ({
indexable: true,
last_status_at: '2023-01-01',
locked: false,
show_featured: true,
show_media: true,
show_media_replies: true,
mute_expires_at: null,
note: 'This is a test user account.',
statuses_count: 0,