[Glitch] Display public collections on profile "Featured tab"

Port 440466c246 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion
2026-02-25 15:14:55 +01:00
committed by Claire
parent 83b4a7845c
commit ae6dbbcc53
7 changed files with 114 additions and 24 deletions

View File

@@ -17,8 +17,15 @@ import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_col
import Column from 'flavours/glitch/features/ui/components/column';
import { useAccountId } from 'flavours/glitch/hooks/useAccountId';
import { useAccountVisibility } from 'flavours/glitch/hooks/useAccountVisibility';
import {
fetchAccountCollections,
selectAccountCollections,
} from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { CollectionListItem } from '../collections/detail/collection_list_item';
import { areCollectionsEnabled } from '../collections/utils';
import { EmptyMessage } from './components/empty_message';
import { FeaturedTag } from './components/featured_tag';
import type { TagMap } from './components/featured_tag';
@@ -42,6 +49,9 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
if (accountId) {
void dispatch(fetchFeaturedTags({ accountId }));
void dispatch(fetchEndorsedAccounts({ accountId }));
if (areCollectionsEnabled()) {
void dispatch(fetchAccountCollections({ accountId }));
}
}
}, [accountId, dispatch]);
@@ -64,6 +74,14 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
ImmutableList(),
) as ImmutableList<string>,
);
const { collections, status } = useAppSelector((state) =>
selectAccountCollections(state, accountId ?? null),
);
const publicCollections = collections.filter(
// This filter only applies when viewing your own profile, where the endpoint
// returns all collections, but we hide unlisted ones here to avoid confusion
(item) => item.discoverable,
);
if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
@@ -101,6 +119,25 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
{accountId && (
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
)}
{publicCollections.length > 0 && status === 'idle' && (
<>
<h4 className='column-subheading'>
<FormattedMessage
id='account.featured.collections'
defaultMessage='Collections'
/>
</h4>
<section>
{publicCollections.map((item, index) => (
<CollectionListItem
key={item.id}
collection={item}
withoutBorder={index === publicCollections.length - 1}
/>
))}
</section>
</>
)}
{!featuredTags.isEmpty() && (
<>
<h4 className='column-subheading'>

View File

@@ -2,15 +2,17 @@
display: flex;
align-items: center;
gap: 16px;
margin-inline: 10px;
padding-inline-end: 5px;
border-bottom: 1px solid var(--color-border-primary);
padding-inline: 16px;
&:not(.wrapperWithoutBorder) {
border-bottom: 1px solid var(--color-border-primary);
}
}
.content {
position: relative;
flex-grow: 1;
padding: 15px 5px;
padding-block: 15px;
}
.link {

View File

@@ -67,13 +67,18 @@ export const CollectionMetaData: React.FC<{
export const CollectionListItem: React.FC<{
collection: ApiCollectionJSON;
}> = ({ collection }) => {
withoutBorder?: boolean;
}> = ({ collection, withoutBorder }) => {
const { id, name } = collection;
const linkId = useId();
return (
<article
className={classNames(classes.wrapper, 'focusable')}
className={classNames(
classes.wrapper,
'focusable',
withoutBorder && classes.wrapperWithoutBorder,
)}
tabIndex={-1}
aria-labelledby={linkId}
>

View File

@@ -2,6 +2,8 @@ import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { matchPath } from 'react-router';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
@@ -9,6 +11,7 @@ import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Dropdown } from 'flavours/glitch/components/dropdown_menu';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { me } from 'flavours/glitch/initial_state';
import type { MenuItem } from 'flavours/glitch/models/dropdown_menu';
import { useAppDispatch } from 'flavours/glitch/store';
import { messages as editorMessages } from '../editor';
@@ -70,7 +73,7 @@ export const CollectionMenu: React.FC<{
const menu = useMemo(() => {
if (isOwnCollection) {
const commonItems = [
const commonItems: MenuItem[] = [
{
text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`,
@@ -97,17 +100,31 @@ export const CollectionMenu: React.FC<{
return commonItems;
}
} else if (ownerAccount) {
return [
{
text: intl.formatMessage(messages.viewOtherCollections),
to: `/@${ownerAccount.acct}/featured`,
},
null,
const items: MenuItem[] = [
{
text: intl.formatMessage(messages.report),
action: openReportModal,
},
];
const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`;
// Don't show menu link to featured collections while on that very page
if (
!matchPath(location.pathname, {
path: featuredCollectionsPath,
exact: true,
})
) {
items.unshift(
...[
{
text: intl.formatMessage(messages.viewOtherCollections),
to: featuredCollectionsPath,
},
null,
],
);
}
return items;
} else {
return [];
}

View File

@@ -5,6 +5,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useParams } from 'react-router';
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { showAlert } from 'flavours/glitch/actions/alerts';
@@ -123,6 +124,28 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
);
};
const CollectionAccountItem: React.FC<{
accountId: string | undefined;
collectionOwnerId: string;
}> = ({ accountId, collectionOwnerId }) => {
const relationship = useRelationship(accountId);
if (!accountId) {
return null;
}
// When viewing your own collection, only show the Follow button
// for accounts you're not following (anymore).
// Otherwise, always show the follow button in its various states.
const withoutButton =
accountId === me ||
!relationship ||
(collectionOwnerId === me &&
(relationship.following || relationship.requested));
return <Account minimal={withoutButton} withMenu={false} id={accountId} />;
};
export const CollectionDetailPage: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
@@ -163,11 +186,13 @@ export const CollectionDetailPage: React.FC<{
collection ? <CollectionHeader collection={collection} /> : null
}
>
{collection?.items.map(({ account_id }) =>
account_id ? (
<Account key={account_id} minimal id={account_id} />
) : null,
)}
{collection?.items.map(({ account_id }) => (
<CollectionAccountItem
key={account_id}
accountId={account_id}
collectionOwnerId={collection.account_id}
/>
))}
</ScrollableList>
<Helmet>

View File

@@ -14,7 +14,7 @@ import { Icon } from 'flavours/glitch/components/icon';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import {
fetchAccountCollections,
selectMyCollections,
selectAccountCollections,
} from 'flavours/glitch/reducers/slices/collections';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
@@ -31,7 +31,9 @@ export const Collections: React.FC<{
const dispatch = useAppDispatch();
const intl = useIntl();
const me = useAppSelector((state) => state.meta.get('me') as string);
const { collections, status } = useAppSelector(selectMyCollections);
const { collections, status } = useAppSelector((state) =>
selectAccountCollections(state, me),
);
useEffect(() => {
void dispatch(fetchAccountCollections({ accountId: me }));

View File

@@ -229,14 +229,16 @@ interface AccountCollectionQuery {
collections: ApiCollectionJSON[];
}
export const selectMyCollections = createAppSelector(
export const selectAccountCollections = createAppSelector(
[
(state) => state.meta.get('me') as string,
(_, accountId: string | null) => accountId,
(state) => state.collections.accountCollections,
(state) => state.collections.collections,
],
(me, collectionsByAccountId, collectionsMap) => {
const myCollectionsQuery = collectionsByAccountId[me];
(accountId, collectionsByAccountId, collectionsMap) => {
const myCollectionsQuery = accountId
? collectionsByAccountId[accountId]
: null;
if (!myCollectionsQuery) {
return {