[Glitch] Add "Featuring you" tab to Collections page

Port b076808fd2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion authored and Claire committed 2026-04-30 20:11:22 +02:00
1 parent d33d25121b
commit 0ec0d84c67
12 files changed
+398 -170

No files matched your search

@@ -27,8 +27,8 @@ import { useAccountVisibility } from '@/flavours/glitch/hooks/useAccountVisibili
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { useAccountCollections } from '../collections';
import { CollectionListItem } from '../collections/components/collection_list_item';
import { useCollectionsCreatedBy } from '../collections/overview/created_by_you';
import { areCollectionsEnabled } from '../collections/utils';
import { EmptyMessage } from './components/empty_message';
@@ -67,7 +67,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
) as ImmutableList<string>,
);
const { collections, status: collectionsLoadStatus } =
useAccountCollections(accountId);
useCollectionsCreatedBy(accountId);
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
collections,
@@ -114,7 +114,7 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
sideContent={sideContent}
>
<ListItemLink
as='h2'
as='h3'
to={getCollectionPath(id)}
subtitle={collectionInfo}
>
@@ -25,7 +25,7 @@ import {
} from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { useAccountCollections } from '..';
import { useCollectionsCreatedBy } from '../overview/created_by_you';
import { CollectionAccounts } from './accounts';
import { CollectionDetails } from './details';
@@ -86,7 +86,7 @@ export const CollectionEditorPage: React.FC<{
// When creating a new collection, we load the current account's collections
// to determine if they're allowed to create more.
const { collections: collectionList, status: collectionListStatus } =
useAccountCollections(isEditMode ? null : accountId);
useCollectionsCreatedBy(isEditMode ? null : accountId);
const isLoading =
(isEditMode && !collection) ||
@@ -1,41 +1,22 @@
import { useEffect } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Route, Switch, useRouteMatch } from 'react-router-dom';
import { EmptyState } from '@/flavours/glitch/components/empty_state';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import { TabLink, TabList } from '@/flavours/glitch/components/tab_list';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { DisplayNameSimple } from 'flavours/glitch/components/display_name/simple';
import { Icon } from 'flavours/glitch/components/icon';
import {
ItemList,
Scrollable,
} from 'flavours/glitch/components/scrollable_list/components';
import { Scrollable } from 'flavours/glitch/components/scrollable_list/components';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import {
useAccountId,
useCurrentAccountId,
} from 'flavours/glitch/hooks/useAccountId';
import {
fetchAccountCollections,
selectAccountCollections,
} from 'flavours/glitch/reducers/slices/collections';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { CollectionListItem } from './components/collection_list_item';
import {
messages as editorMessages,
MaxCollectionsCallout,
userCollectionLimit,
} from './editor';
import { CollectionsCreatedByYou } from './overview/created_by_you';
import { CollectionsFeaturingYou } from './overview/featuring_you';
import classes from './styles.module.scss';
import { areCollectionsEnabled } from './utils';
const messages = defineMessages({
headingMe: {
@@ -60,25 +41,6 @@ const messages = defineMessages({
},
});
const CreateButton: React.FC = () => (
<Link to='/collections/new' className='button button--compact'>
<Icon id='plus' icon={AddIcon} />
<FormattedMessage {...editorMessages.newCollection} />
</Link>
);
export function useAccountCollections(accountId: string | null | undefined) {
const dispatch = useAppDispatch();
useEffect(() => {
if (accountId && areCollectionsEnabled()) {
void dispatch(fetchAccountCollections({ accountId }));
}
}, [dispatch, accountId]);
return useAppSelector((state) => selectAccountCollections(state, accountId));
}
export const Collections: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
@@ -86,15 +48,11 @@ export const Collections: React.FC<{
const me = useCurrentAccountId();
const accountId = useAccountId();
const account = useAccount(accountId);
const { path } = useRouteMatch();
const { collections, status } = useAccountCollections(accountId);
const isOwnCollectionsPage = accountId === me;
const canCreateMoreCollections = collections.length < userCollectionLimit;
const isOwnCollection = accountId === me;
const showCreateButton =
isOwnCollection && status === 'idle' && canCreateMoreCollections;
const titleMessage = isOwnCollection
const titleMessage = isOwnCollectionsPage
? messages.headingMe
: messages.headingOther;
@@ -105,18 +63,10 @@ export const Collections: React.FC<{
name: <DisplayNameSimple account={account} />,
});
const tabMessage = isOwnCollection
const createdByTabMessage = isOwnCollectionsPage
? messages.createdByYou
: messages.createdByAuthor;
const errorMessage = (status === 'error' || !accountId) && (
<FormattedMessage
id='collections.error_loading_collections'
defaultMessage='There was an error when trying to load your collections.'
tagName='span'
/>
);
return (
<Column bindToDocument={!multiColumn} label={pageTitle}>
<ColumnHeader showBackButton multiColumn={multiColumn} />
@@ -126,64 +76,28 @@ export const Collections: React.FC<{
<h1 className={classes.heading}>{pageTitleHtml}</h1>
<TabList plain>
<TabLink exact to={`/@${account?.acct}/collections`}>
{intl.formatMessage(tabMessage, {
{intl.formatMessage(createdByTabMessage, {
name: <DisplayNameSimple account={account} />,
})}
</TabLink>
{isOwnCollectionsPage && (
<TabLink
exact
to={`/@${account?.acct}/collections/featuring-you`}
>
{intl.formatMessage(messages.featuringYou)}
</TabLink>
)}
</TabList>
</header>
{status === 'loading' && <LoadingIndicator />}
{status === 'idle' &&
(collections.length > 0 ? (
<>
<div className={classes.listHeader}>
<h2 className={classes.subHeading}>
<FormattedMessage
id='collections.list.collections_with_count'
defaultMessage='{count, plural, one {# Collection} other {# Collections}}'
values={{
count: collections.length,
}}
/>
</h2>
{showCreateButton && <CreateButton />}
</div>
<ItemList emptyMessage={errorMessage}>
{!canCreateMoreCollections && (
<MaxCollectionsCallout
className={classes.maxCollectionsError}
/>
)}
{collections.map((item, index) => (
<CollectionListItem
withTimestamp
withAuthorHandle={false}
key={item.id}
collection={item}
positionInList={index + 1}
listSize={collections.length}
/>
))}
</ItemList>
</>
) : (
<EmptyState
title={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
defaultMessage='Showcase your favorite accounts'
/>
}
message={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts_desc'
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
/>
}
>
<CreateButton />
</EmptyState>
))}
<Switch>
<Route exact path={path} component={CollectionsCreatedByYou} />
<Route
exact
path={`${path}/featuring-you`}
component={CollectionsFeaturingYou}
/>
</Switch>
</Scrollable>
<Helmet>
@@ -0,0 +1,136 @@
import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { EmptyState } from 'flavours/glitch/components/empty_state';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { ItemList } from 'flavours/glitch/components/scrollable_list/components';
import {
useAccountId,
useCurrentAccountId,
} from 'flavours/glitch/hooks/useAccountId';
import {
fetchCollectionsCreatedByAccount,
selectAccountCollections,
} from 'flavours/glitch/reducers/slices/collections';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { CollectionListItem } from '../components/collection_list_item';
import {
messages as editorMessages,
MaxCollectionsCallout,
userCollectionLimit,
} from '../editor';
import classes from '../styles.module.scss';
import { areCollectionsEnabled } from '../utils';
const CreateButton: React.FC = () => (
<Link to='/collections/new' className='button button--compact'>
<Icon id='plus' icon={AddIcon} />
<FormattedMessage {...editorMessages.newCollection} />
</Link>
);
export const CollectionListError: React.FC = () => (
<EmptyState
image={null}
className={classes.error}
message={
<FormattedMessage
id='collections.error_loading_collections'
defaultMessage='There was an error when trying to load these collections.'
/>
}
/>
);
export function useCollectionsCreatedBy(accountId: string | null | undefined) {
const dispatch = useAppDispatch();
useEffect(() => {
if (accountId && areCollectionsEnabled()) {
void dispatch(fetchCollectionsCreatedByAccount({ accountId }));
}
}, [dispatch, accountId]);
return useAppSelector((state) =>
selectAccountCollections(state, accountId, 'createdBy'),
);
}
export const CollectionsCreatedByYou: React.FC = () => {
const me = useCurrentAccountId();
const accountId = useAccountId();
const { collections, status } = useCollectionsCreatedBy(accountId);
const canCreateMoreCollections = collections.length < userCollectionLimit;
const isOwnCollectionPage = accountId === me;
const showCreateButton =
isOwnCollectionPage && status === 'idle' && canCreateMoreCollections;
if (status === 'error' || !accountId) {
return <CollectionListError />;
}
if (status === 'loading') {
return <LoadingIndicator />;
}
if (collections.length === 0) {
return (
<EmptyState
title={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
defaultMessage='Showcase your favorite accounts'
/>
}
message={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts_desc'
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
/>
}
>
<CreateButton />
</EmptyState>
);
}
return (
<>
<div className={classes.listHeader}>
<h2 className={classes.subHeading}>
<FormattedMessage
id='collections.list.collections_with_count'
defaultMessage='{count, plural, one {# Collection} other {# Collections}}'
values={{
count: collections.length,
}}
/>
</h2>
{showCreateButton && <CreateButton />}
</div>
<ItemList>
{!canCreateMoreCollections && (
<MaxCollectionsCallout className={classes.maxCollectionsError} />
)}
{collections.map((item, index) => (
<CollectionListItem
withTimestamp
withAuthorHandle={false}
key={item.id}
collection={item}
positionInList={index + 1}
listSize={collections.length}
/>
))}
</ItemList>
</>
);
};
@@ -0,0 +1,87 @@
import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { EmptyState } from 'flavours/glitch/components/empty_state';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { ItemList } from 'flavours/glitch/components/scrollable_list/components';
import { useAccountId } from 'flavours/glitch/hooks/useAccountId';
import {
fetchCollectionsFeaturingAccount,
selectAccountCollections,
} from 'flavours/glitch/reducers/slices/collections';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { CollectionListItem } from '../components/collection_list_item';
import classes from '../styles.module.scss';
import { areCollectionsEnabled } from '../utils';
import { CollectionListError } from './created_by_you';
function useCollectionsFeaturing(accountId: string | null | undefined) {
const dispatch = useAppDispatch();
useEffect(() => {
if (accountId && areCollectionsEnabled()) {
void dispatch(fetchCollectionsFeaturingAccount({ accountId }));
}
}, [dispatch, accountId]);
return useAppSelector((state) =>
selectAccountCollections(state, accountId, 'featuring'),
);
}
export const CollectionsFeaturingYou: React.FC = () => {
const accountId = useAccountId();
const { collections, status } = useCollectionsFeaturing(accountId);
if (status === 'error' || !accountId) {
return <CollectionListError />;
}
if (status === 'loading') {
return <LoadingIndicator />;
}
if (collections.length === 0) {
return (
<EmptyState
message={
<FormattedMessage
id='empty_column.collections.featured_in'
defaultMessage='You have not been added to any collections yet.'
/>
}
/>
);
}
return (
<>
<div className={classes.listHeader}>
<h2 className={classes.subHeading}>
<FormattedMessage
id='collections.list.collections_with_count'
defaultMessage='{count, plural, one {# Collection} other {# Collections}}'
values={{
count: collections.length,
}}
/>
</h2>
</div>
<ItemList>
{collections.map((item, index) => (
<CollectionListItem
withAuthorHandle
key={item.id}
collection={item}
positionInList={index + 1}
listSize={collections.length}
/>
))}
</ItemList>
</>
);
};
@@ -33,3 +33,7 @@
.maxCollectionsError {
margin: 8px 16px;
}
.error {
flex-grow: 1;
}