Add "My collections" page (#37552)
This commit is contained in:
parent
d05df5c197
commit
a1acf8f4bc
39
app/javascript/mastodon/api/collections.ts
Normal file
39
app/javascript/mastodon/api/collections.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
apiRequestPost,
|
||||||
|
apiRequestPut,
|
||||||
|
apiRequestGet,
|
||||||
|
apiRequestDelete,
|
||||||
|
} from 'mastodon/api';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiWrappedCollectionJSON,
|
||||||
|
ApiCollectionWithAccountsJSON,
|
||||||
|
ApiCreateCollectionPayload,
|
||||||
|
ApiPatchCollectionPayload,
|
||||||
|
ApiCollectionsJSON,
|
||||||
|
} from '../api_types/collections';
|
||||||
|
|
||||||
|
export const apiCreateCollection = (collection: ApiCreateCollectionPayload) =>
|
||||||
|
apiRequestPost<ApiWrappedCollectionJSON>('v1_alpha/collections', collection);
|
||||||
|
|
||||||
|
export const apiUpdateCollection = ({
|
||||||
|
id,
|
||||||
|
...collection
|
||||||
|
}: ApiPatchCollectionPayload) =>
|
||||||
|
apiRequestPut<ApiWrappedCollectionJSON>(
|
||||||
|
`v1_alpha/collections/${id}`,
|
||||||
|
collection,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const apiDeleteCollection = (collectionId: string) =>
|
||||||
|
apiRequestDelete(`v1_alpha/collections/${collectionId}`);
|
||||||
|
|
||||||
|
export const apiGetCollection = (collectionId: string) =>
|
||||||
|
apiRequestGet<ApiCollectionWithAccountsJSON[]>(
|
||||||
|
`v1_alpha/collections/${collectionId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const apiGetAccountCollections = (accountId: string) =>
|
||||||
|
apiRequestGet<ApiCollectionsJSON>(
|
||||||
|
`v1_alpha/accounts/${accountId}/collections`,
|
||||||
|
);
|
||||||
82
app/javascript/mastodon/api_types/collections.ts
Normal file
82
app/javascript/mastodon/api_types/collections.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// See app/serializers/rest/base_collection_serializer.rb
|
||||||
|
|
||||||
|
import type { ApiAccountJSON } from './accounts';
|
||||||
|
import type { ApiTagJSON } from './statuses';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned when fetching all collections for an account,
|
||||||
|
* doesn't contain account and item data
|
||||||
|
*/
|
||||||
|
export interface ApiCollectionJSON {
|
||||||
|
account_id: string;
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
uri: string;
|
||||||
|
local: boolean;
|
||||||
|
item_count: number;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
tag?: ApiTagJSON;
|
||||||
|
language: string;
|
||||||
|
sensitive: boolean;
|
||||||
|
discoverable: boolean;
|
||||||
|
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
|
||||||
|
items: CollectionAccountItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned when fetching all collections for an account
|
||||||
|
*/
|
||||||
|
export interface ApiCollectionsJSON {
|
||||||
|
collections: ApiCollectionJSON[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned when creating, updating, and adding to a collection
|
||||||
|
*/
|
||||||
|
export interface ApiWrappedCollectionJSON {
|
||||||
|
collection: ApiCollectionJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned when fetching a single collection
|
||||||
|
*/
|
||||||
|
export interface ApiCollectionWithAccountsJSON extends ApiWrappedCollectionJSON {
|
||||||
|
accounts: ApiAccountJSON[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nested account item
|
||||||
|
*/
|
||||||
|
interface CollectionAccountItem {
|
||||||
|
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
||||||
|
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WrappedCollectionAccountItem {
|
||||||
|
collection_item: CollectionAccountItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload types
|
||||||
|
*/
|
||||||
|
|
||||||
|
type CommonPayloadFields = Pick<
|
||||||
|
ApiCollectionJSON,
|
||||||
|
'name' | 'description' | 'sensitive' | 'discoverable'
|
||||||
|
> & {
|
||||||
|
tag?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ApiPatchCollectionPayload extends Partial<CommonPayloadFields> {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiCreateCollectionPayload extends CommonPayloadFields {
|
||||||
|
account_ids?: string[];
|
||||||
|
}
|
||||||
179
app/javascript/mastodon/features/collections/index.tsx
Normal file
179
app/javascript/mastodon/features/collections/index.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { useEffect, useMemo, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||||
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { Column } from 'mastodon/components/column';
|
||||||
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
|
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
|
import {
|
||||||
|
createCollection,
|
||||||
|
fetchAccountCollections,
|
||||||
|
selectMyCollections,
|
||||||
|
} from 'mastodon/reducers/slices/collections';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.collections', defaultMessage: 'My collections' },
|
||||||
|
create: {
|
||||||
|
id: 'collections.create_collection',
|
||||||
|
defaultMessage: 'Create collection',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
id: 'collections.view_collection',
|
||||||
|
defaultMessage: 'View collection',
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
id: 'collections.delete_collection',
|
||||||
|
defaultMessage: 'Delete collection',
|
||||||
|
},
|
||||||
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListItem: React.FC<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}> = ({ id, name }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleDeleteClick = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'CONFIRM_DELETE_LIST',
|
||||||
|
modalProps: {
|
||||||
|
listId: id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const menu = useMemo(
|
||||||
|
() => [
|
||||||
|
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
|
||||||
|
{ text: intl.formatMessage(messages.delete), action: handleDeleteClick },
|
||||||
|
],
|
||||||
|
[intl, id, handleDeleteClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='lists__item'>
|
||||||
|
<Link to={`/collections/${id}`} className='lists__item__title'>
|
||||||
|
<span>{name}</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
scrollKey='collections'
|
||||||
|
items={menu}
|
||||||
|
icon='ellipsis-h'
|
||||||
|
iconComponent={MoreHorizIcon}
|
||||||
|
title={intl.formatMessage(messages.more)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Collections: React.FC<{
|
||||||
|
multiColumn?: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
const me = useAppSelector((state) => state.meta.get('me') as string);
|
||||||
|
const { collections, status } = useAppSelector(selectMyCollections);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void dispatch(fetchAccountCollections({ accountId: me }));
|
||||||
|
}, [dispatch, me]);
|
||||||
|
|
||||||
|
const addDummyCollection = useCallback(
|
||||||
|
(event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
void dispatch(
|
||||||
|
createCollection({
|
||||||
|
payload: {
|
||||||
|
name: 'Test Collection',
|
||||||
|
description: 'A useful test collection',
|
||||||
|
discoverable: true,
|
||||||
|
sensitive: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const emptyMessage =
|
||||||
|
status === 'error' ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.error_loading_collections'
|
||||||
|
defaultMessage='There was an error when trying to load your collections.'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.no_collections_yet'
|
||||||
|
defaultMessage='No collections yet.'
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.create_a_collection_hint'
|
||||||
|
defaultMessage='Create a collection to recommend or share your favourite accounts with others.'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
label={intl.formatMessage(messages.heading)}
|
||||||
|
>
|
||||||
|
<ColumnHeader
|
||||||
|
title={intl.formatMessage(messages.heading)}
|
||||||
|
icon='list-ul'
|
||||||
|
iconComponent={ListAltIcon}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
extraButton={
|
||||||
|
<Link
|
||||||
|
to='/collections/new'
|
||||||
|
className='column-header__button'
|
||||||
|
title={intl.formatMessage(messages.create)}
|
||||||
|
aria-label={intl.formatMessage(messages.create)}
|
||||||
|
onClick={addDummyCollection}
|
||||||
|
>
|
||||||
|
<Icon id='plus' icon={AddIcon} />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='collections'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
isLoading={status === 'loading'}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
>
|
||||||
|
{collections.map((item) => (
|
||||||
|
<ListItem key={item.id} id={item.id} name={item.name} />
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.heading)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
11
app/javascript/mastodon/features/collections/utils.ts
Normal file
11
app/javascript/mastodon/features/collections/utils.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {
|
||||||
|
isClientFeatureEnabled,
|
||||||
|
isServerFeatureEnabled,
|
||||||
|
} from '@/mastodon/utils/environment';
|
||||||
|
|
||||||
|
export function areCollectionsEnabled() {
|
||||||
|
return (
|
||||||
|
isClientFeatureEnabled('collections') &&
|
||||||
|
isServerFeatureEnabled('collections')
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -63,6 +63,7 @@ import {
|
|||||||
Lists,
|
Lists,
|
||||||
ListEdit,
|
ListEdit,
|
||||||
ListMembers,
|
ListMembers,
|
||||||
|
Collections,
|
||||||
Blocks,
|
Blocks,
|
||||||
DomainBlocks,
|
DomainBlocks,
|
||||||
Mutes,
|
Mutes,
|
||||||
@ -85,6 +86,7 @@ import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
|||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
import '../../components/status';
|
import '../../components/status';
|
||||||
|
import { areCollectionsEnabled } from '../collections/utils';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
|
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
|
||||||
@ -227,6 +229,9 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
||||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||||
|
{areCollectionsEnabled() &&
|
||||||
|
<WrappedRoute path='/collections' component={Collections} content={children} />
|
||||||
|
}
|
||||||
|
|
||||||
<Route component={BundleColumnError} />
|
<Route component={BundleColumnError} />
|
||||||
</WrappedSwitch>
|
</WrappedSwitch>
|
||||||
|
|||||||
@ -42,6 +42,12 @@ export function Lists () {
|
|||||||
return import('../../lists');
|
return import('../../lists');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Collections () {
|
||||||
|
return import('../../collections').then(
|
||||||
|
module => ({default: module.Collections})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Status () {
|
export function Status () {
|
||||||
return import('../../status');
|
return import('../../status');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -194,9 +194,16 @@
|
|||||||
"closed_registrations_modal.find_another_server": "Find another server",
|
"closed_registrations_modal.find_another_server": "Find another server",
|
||||||
"closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
|
"closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
|
||||||
"closed_registrations_modal.title": "Signing up on Mastodon",
|
"closed_registrations_modal.title": "Signing up on Mastodon",
|
||||||
|
"collections.create_a_collection_hint": "Create a collection to recommend or share your favourite accounts with others.",
|
||||||
|
"collections.create_collection": "Create collection",
|
||||||
|
"collections.delete_collection": "Delete collection",
|
||||||
|
"collections.error_loading_collections": "There was an error when trying to load your collections.",
|
||||||
|
"collections.no_collections_yet": "No collections yet.",
|
||||||
|
"collections.view_collection": "View collection",
|
||||||
"column.about": "About",
|
"column.about": "About",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
"column.bookmarks": "Bookmarks",
|
"column.bookmarks": "Bookmarks",
|
||||||
|
"column.collections": "My collections",
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
"column.create_list": "Create list",
|
"column.create_list": "Create list",
|
||||||
"column.direct": "Private mentions",
|
"column.direct": "Private mentions",
|
||||||
|
|||||||
158
app/javascript/mastodon/reducers/slices/collections.ts
Normal file
158
app/javascript/mastodon/reducers/slices/collections.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
apiCreateCollection,
|
||||||
|
apiGetAccountCollections,
|
||||||
|
// apiGetCollection,
|
||||||
|
} from '@/mastodon/api/collections';
|
||||||
|
import type {
|
||||||
|
ApiCollectionJSON,
|
||||||
|
ApiCreateCollectionPayload,
|
||||||
|
} from '@/mastodon/api_types/collections';
|
||||||
|
import {
|
||||||
|
createAppSelector,
|
||||||
|
createDataLoadingThunk,
|
||||||
|
} from '@/mastodon/store/typed_functions';
|
||||||
|
|
||||||
|
type QueryStatus = 'idle' | 'loading' | 'error';
|
||||||
|
|
||||||
|
interface CollectionState {
|
||||||
|
// Collections mapped by collection id
|
||||||
|
collections: Record<string, ApiCollectionJSON>;
|
||||||
|
// Lists of collection ids mapped by account id
|
||||||
|
accountCollections: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
collectionIds: string[];
|
||||||
|
status: QueryStatus;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: CollectionState = {
|
||||||
|
collections: {},
|
||||||
|
accountCollections: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectionSlice = createSlice({
|
||||||
|
name: 'collections',
|
||||||
|
initialState,
|
||||||
|
reducers: {},
|
||||||
|
extraReducers(builder) {
|
||||||
|
/**
|
||||||
|
* Fetching account collections
|
||||||
|
*/
|
||||||
|
builder.addCase(fetchAccountCollections.pending, (state, action) => {
|
||||||
|
const { accountId } = action.meta.arg;
|
||||||
|
state.accountCollections[accountId] ??= {
|
||||||
|
status: 'loading',
|
||||||
|
collectionIds: [],
|
||||||
|
};
|
||||||
|
state.accountCollections[accountId].status = 'loading';
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(fetchAccountCollections.rejected, (state, action) => {
|
||||||
|
const { accountId } = action.meta.arg;
|
||||||
|
state.accountCollections[accountId] = {
|
||||||
|
status: 'error',
|
||||||
|
collectionIds: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(fetchAccountCollections.fulfilled, (state, actions) => {
|
||||||
|
const { collections } = actions.payload;
|
||||||
|
|
||||||
|
const collectionsMap: Record<string, ApiCollectionJSON> = {};
|
||||||
|
const collectionIds: string[] = [];
|
||||||
|
|
||||||
|
collections.forEach((collection) => {
|
||||||
|
const { id } = collection;
|
||||||
|
collectionsMap[id] = collection;
|
||||||
|
collectionIds.push(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
state.collections = collectionsMap;
|
||||||
|
state.accountCollections[actions.meta.arg.accountId] = {
|
||||||
|
collectionIds,
|
||||||
|
status: 'idle',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creating a collection
|
||||||
|
*/
|
||||||
|
|
||||||
|
builder.addCase(createCollection.fulfilled, (state, actions) => {
|
||||||
|
const { collection } = actions.payload;
|
||||||
|
|
||||||
|
state.collections[collection.id] = collection;
|
||||||
|
if (state.accountCollections[collection.account_id]) {
|
||||||
|
state.accountCollections[collection.account_id]?.collectionIds.unshift(
|
||||||
|
collection.id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
state.accountCollections[collection.account_id] = {
|
||||||
|
collectionIds: [collection.id],
|
||||||
|
status: 'idle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchAccountCollections = createDataLoadingThunk(
|
||||||
|
`${collectionSlice.name}/fetchAccountCollections`,
|
||||||
|
({ accountId }: { accountId: string }) => apiGetAccountCollections(accountId),
|
||||||
|
);
|
||||||
|
|
||||||
|
// To be added soon…
|
||||||
|
//
|
||||||
|
// export const fetchCollection = createDataLoadingThunk(
|
||||||
|
// `${collectionSlice.name}/fetchCollection`,
|
||||||
|
// ({ collectionId }: { collectionId: string }) =>
|
||||||
|
// apiGetCollection(collectionId),
|
||||||
|
// );
|
||||||
|
|
||||||
|
export const createCollection = createDataLoadingThunk(
|
||||||
|
`${collectionSlice.name}/createCollection`,
|
||||||
|
({ payload }: { payload: ApiCreateCollectionPayload }) =>
|
||||||
|
apiCreateCollection(payload),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const collections = collectionSlice.reducer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selectors
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface AccountCollectionQuery {
|
||||||
|
status: QueryStatus;
|
||||||
|
collections: ApiCollectionJSON[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectMyCollections = createAppSelector(
|
||||||
|
[
|
||||||
|
(state) => state.meta.get('me') as string,
|
||||||
|
(state) => state.collections.accountCollections,
|
||||||
|
(state) => state.collections.collections,
|
||||||
|
],
|
||||||
|
(me, collectionsByAccountId, collectionsById) => {
|
||||||
|
const myCollectionsQuery = collectionsByAccountId[me];
|
||||||
|
|
||||||
|
if (!myCollectionsQuery) {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
collections: [] as ApiCollectionJSON[],
|
||||||
|
} satisfies AccountCollectionQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, collectionIds } = myCollectionsQuery;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
collections: collectionIds
|
||||||
|
.map((id) => collectionsById[id])
|
||||||
|
.filter((c) => !!c),
|
||||||
|
} satisfies AccountCollectionQuery;
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { annualReport } from './annual_report';
|
import { annualReport } from './annual_report';
|
||||||
|
import { collections } from './collections';
|
||||||
|
|
||||||
export const sliceReducers = {
|
export const sliceReducers = {
|
||||||
annualReport,
|
annualReport,
|
||||||
|
collections,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,13 +12,13 @@ export function isProduction() {
|
|||||||
else return import.meta.env.PROD;
|
else return import.meta.env.PROD;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerFeatures = 'fasp';
|
export type ServerFeatures = 'fasp' | 'collections';
|
||||||
|
|
||||||
export function isServerFeatureEnabled(feature: ServerFeatures) {
|
export function isServerFeatureEnabled(feature: ServerFeatures) {
|
||||||
return initialState?.features.includes(feature) ?? false;
|
return initialState?.features.includes(feature) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientFeatures = 'profile_redesign';
|
type ClientFeatures = 'profile_redesign' | 'collections';
|
||||||
|
|
||||||
export function isClientFeatureEnabled(feature: ClientFeatures) {
|
export function isClientFeatureEnabled(feature: ClientFeatures) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
%w(
|
%w(
|
||||||
/blocks
|
/blocks
|
||||||
/bookmarks
|
/bookmarks
|
||||||
|
/collections/(*any)
|
||||||
/conversations
|
/conversations
|
||||||
/deck/(*any)
|
/deck/(*any)
|
||||||
/directory
|
/directory
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user