[Glitch] Profile redesign: Profile tab settings

Port 6507a61d30 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo
2026-03-20 14:38:10 +01:00
committed by Claire
parent 00a423ed2b
commit 781491e643
8 changed files with 126 additions and 72 deletions

View File

@@ -170,7 +170,7 @@ export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expa
export const expandDirectTimeline = ({ maxId } = {}) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }); export const expandDirectTimeline = ({ maxId } = {}) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId });
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 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`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/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 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 expandLinkTimeline = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId });
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}) => { export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}) => {

View File

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

View File

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

View File

@@ -2,10 +2,9 @@ import { useEffect, useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit'; import { List as ImmutableList, isList } from 'immutable';
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
import PersonIcon from '@/material-icons/400-24px/person.svg?react'; import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
@@ -18,8 +17,11 @@ import Column from 'flavours/glitch/features/ui/components/column';
import { useAccountId } from 'flavours/glitch/hooks/useAccountId'; import { useAccountId } from 'flavours/glitch/hooks/useAccountId';
import { useAccountVisibility } from 'flavours/glitch/hooks/useAccountVisibility'; import { useAccountVisibility } from 'flavours/glitch/hooks/useAccountVisibility';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment'; import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import type { RootState } from 'flavours/glitch/store'; import {
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; useAppSelector,
useAppDispatch,
createAppSelector,
} from 'flavours/glitch/store';
import { MediaItem } from './components/media_item'; import { MediaItem } from './components/media_item';
@@ -27,33 +29,61 @@ const messages = defineMessages({
profile: { id: 'column_header.profile', defaultMessage: 'Profile' }, profile: { id: 'column_header.profile', defaultMessage: 'Profile' },
}); });
const getAccountGallery = createSelector( const emptyList = ImmutableList<MediaAttachment>();
const redesignEnabled = isServerFeatureEnabled('profile_redesign');
const selectGalleryTimeline = createAppSelector(
[ [
(state: RootState, accountId: string) => (_state, accountId?: string | null) => accountId,
(state.timelines as ImmutableMap<string, unknown>).getIn( (state) => state.timelines,
[`account:${accountId}:media`, 'items'], (state) => state.accounts,
ImmutableList(), (state) => state.statuses,
) as ImmutableList<string>,
(state: RootState) => state.statuses,
], ],
(statusIds, statuses) => { (accountId, timelines, accounts, statuses) => {
let items = ImmutableList<MediaAttachment>(); if (!accountId) {
return null;
}
const account = accounts.get(accountId);
if (!account) {
return null;
}
statusIds.forEach((statusId) => { let items = emptyList;
const status = statuses.get(statusId) as const { show_media, show_media_replies } = account;
| ImmutableMap<string, unknown> // If the account disabled showing media, don't display anything.
| undefined; 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( items = items.concat(
( (
status.get('media_attachments') as ImmutableList<MediaAttachment> status?.get('media_attachments') as ImmutableList<MediaAttachment>
).map((media) => media.set('status', status)), ).map((media) => media.set('status', status)),
); );
} }
}); }
return items; return {
items,
hasMore: !!timeline?.get('hasMore'),
isLoading: !!timeline?.get('isLoading'),
showingReplies,
};
}, },
); );
@@ -63,27 +93,12 @@ export const AccountGallery: React.FC<{
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const accountId = useAccountId(); const accountId = useAccountId();
const attachments = useAppSelector((state) => const {
accountId isLoading = true,
? getAccountGallery(state, accountId) hasMore = false,
: ImmutableList<MediaAttachment>(), items: attachments = emptyList,
); showingReplies: withReplies = false,
const isLoading = useAppSelector((state) => } = useAppSelector((state) => selectGalleryTimeline(state, accountId)) ?? {};
(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 { suspended, blockedBy, hidden } = useAccountVisibility(accountId); const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
@@ -92,16 +107,18 @@ export const AccountGallery: React.FC<{
| undefined; | undefined;
useEffect(() => { useEffect(() => {
if (accountId && isAccount) { if (accountId) {
void dispatch(expandAccountMediaTimeline(accountId)); void dispatch(expandAccountMediaTimeline(accountId, { withReplies }));
} }
}, [dispatch, accountId, isAccount]); }, [dispatch, accountId, withReplies]);
const handleLoadMore = useCallback(() => { const handleLoadMore = useCallback(() => {
if (maxId) { if (maxId) {
void dispatch(expandAccountMediaTimeline(accountId, { maxId })); void dispatch(
expandAccountMediaTimeline(accountId, { maxId, withReplies }),
);
} }
}, [dispatch, accountId, maxId]); }, [maxId, dispatch, accountId, withReplies]);
const handleOpenMedia = useCallback( const handleOpenMedia = useCallback(
(attachment: MediaAttachment) => { (attachment: MediaAttachment) => {

View File

@@ -5,25 +5,16 @@ import { FormattedMessage } from 'react-intl';
import type { NavLinkProps } from 'react-router-dom'; import type { NavLinkProps } from 'react-router-dom';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useAccountId } from '@/flavours/glitch/hooks/useAccountId';
import { isRedesignEnabled } from '../common'; import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss'; import classes from './redesign.module.scss';
export const AccountTabs: FC<{ acct: string }> = ({ acct }) => { export const AccountTabs: FC<{ acct: string }> = ({ acct }) => {
if (isRedesignEnabled()) { if (isRedesignEnabled()) {
return ( return <RedesignTabs />;
<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 ( return (
<div className='account__section-headline'> <div className='account__section-headline'>
@@ -49,3 +40,32 @@ export const AccountTabs: FC<{ acct: string }> = ({ acct }) => {
const isActive: Required<NavLinkProps>['isActive'] = (match, location) => const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
match?.url === location.pathname || match?.url === location.pathname ||
(!!match?.url && location.pathname.startsWith(`${match.url}/tagged/`)); (!!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 columnRef = useRef<ColumnRef>(null);
const firstStatusId = useAppSelector((state) => const firstStatusId = useAppSelector((state) =>
decodedUrl 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, : undefined,
); );
const story = useAppSelector((state) => const story = useAppSelector((state) =>

View File

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

View File

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