[Glitch] Allow viewing unlisted collections on your own Profile's Featured tab
Port 4835c3b7b481000476f0a34d335b0d7503b11680 to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
25f4e44580
commit
f18834d86e
@ -0,0 +1,107 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { Article } from '@/flavours/glitch/components/scrollable_list/components';
|
||||||
|
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
|
||||||
|
import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react';
|
||||||
|
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
import type { IconProp } from '../icon';
|
||||||
|
import { ListItemButton, ListItemWrapper } from '../list_item';
|
||||||
|
|
||||||
|
export interface TruncatedListItemInfo<TListItem> {
|
||||||
|
item: TListItem;
|
||||||
|
index: number;
|
||||||
|
totalListLength: number;
|
||||||
|
isLastElement: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToggleButtonOptions {
|
||||||
|
title: NonNullable<React.ReactNode>;
|
||||||
|
subtitle?: React.ReactNode;
|
||||||
|
icon?: IconProp;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TruncatedListProps<TListItem> {
|
||||||
|
visibleItems: TListItem[];
|
||||||
|
truncatedItems: TListItem[];
|
||||||
|
renderListItem: (
|
||||||
|
itemInfo: TruncatedListItemInfo<TListItem>,
|
||||||
|
) => React.ReactElement;
|
||||||
|
toggleButton: ToggleButtonOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate the children of an `ItemList` component with this helper
|
||||||
|
* component.
|
||||||
|
* It handles rendering the children with correct indexes for accessibility,
|
||||||
|
* and has a configurable toggle button.
|
||||||
|
*/
|
||||||
|
export const TruncatedListItems = <TListItem,>({
|
||||||
|
visibleItems,
|
||||||
|
truncatedItems,
|
||||||
|
toggleButton,
|
||||||
|
renderListItem,
|
||||||
|
}: TruncatedListProps<TListItem>) => {
|
||||||
|
const [showTruncatedItems, setShowTruncatedItems] = useState(false);
|
||||||
|
const toggleTruncatedItems = useCallback(() => {
|
||||||
|
setShowTruncatedItems((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hasHiddenAccounts = truncatedItems.length > 0;
|
||||||
|
// Add the toggle button's item to the list size when needed
|
||||||
|
const initialListSize = visibleItems.length + (hasHiddenAccounts ? 1 : 0);
|
||||||
|
const totalListLength =
|
||||||
|
initialListSize + (showTruncatedItems ? truncatedItems.length : 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{visibleItems.map((item, index) => {
|
||||||
|
return renderListItem({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
totalListLength,
|
||||||
|
isLastElement:
|
||||||
|
index === visibleItems.length - 1 && !hasHiddenAccounts,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
{hasHiddenAccounts && (
|
||||||
|
<Article aria-posinset={initialListSize} aria-setsize={totalListLength}>
|
||||||
|
<ListItemWrapper
|
||||||
|
icon={
|
||||||
|
toggleButton.icon && (
|
||||||
|
<Icon id='toggle-icon' icon={toggleButton.icon} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconEnd={
|
||||||
|
<Icon
|
||||||
|
id='open-status'
|
||||||
|
icon={
|
||||||
|
showTruncatedItems
|
||||||
|
? KeyboardArrowUpIcon
|
||||||
|
: KeyboardArrowDownIcon
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemButton
|
||||||
|
aria-expanded={showTruncatedItems}
|
||||||
|
onClick={toggleTruncatedItems}
|
||||||
|
subtitle={toggleButton.subtitle}
|
||||||
|
>
|
||||||
|
{toggleButton.title}
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItemWrapper>
|
||||||
|
</Article>
|
||||||
|
)}
|
||||||
|
{showTruncatedItems &&
|
||||||
|
truncatedItems.map((item, index) => {
|
||||||
|
return renderListItem({
|
||||||
|
item,
|
||||||
|
index: initialListSize + index + 1,
|
||||||
|
totalListLength,
|
||||||
|
isLastElement: index === truncatedItems.length - 1,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
@ -6,10 +6,9 @@ import { useHistory } from 'react-router';
|
|||||||
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { AccountListItem } from '@/flavours/glitch/components/account_list_item';
|
|
||||||
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||||
import { fetchEndorsedAccounts } from 'flavours/glitch/actions/accounts';
|
import { fetchEndorsedAccounts } from 'flavours/glitch/actions/accounts';
|
||||||
|
import { AccountListItem } from 'flavours/glitch/components/account_list_item';
|
||||||
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
|
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
import { RemoteHint } from 'flavours/glitch/components/remote_hint';
|
import { RemoteHint } from 'flavours/glitch/components/remote_hint';
|
||||||
@ -18,9 +17,12 @@ import {
|
|||||||
ItemList,
|
ItemList,
|
||||||
Scrollable,
|
Scrollable,
|
||||||
} from 'flavours/glitch/components/scrollable_list/components';
|
} from 'flavours/glitch/components/scrollable_list/components';
|
||||||
|
import type { TruncatedListItemInfo } from 'flavours/glitch/components/truncated_list';
|
||||||
|
import { TruncatedListItems } from 'flavours/glitch/components/truncated_list';
|
||||||
import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header';
|
import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header';
|
||||||
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
|
||||||
import Column from 'flavours/glitch/features/ui/components/column';
|
import Column from 'flavours/glitch/features/ui/components/column';
|
||||||
|
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
||||||
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 {
|
import {
|
||||||
@ -74,11 +76,32 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
const { collections, status: collectionsLoadStatus } = useAppSelector(
|
const { collections, status: collectionsLoadStatus } = useAppSelector(
|
||||||
(state) => selectAccountCollections(state, accountId ?? null),
|
(state) => selectAccountCollections(state, accountId ?? null),
|
||||||
);
|
);
|
||||||
const listedCollections = collections.filter(
|
|
||||||
// Hide unlisted and empty collections to avoid confusion
|
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
|
||||||
// (Unlisted collections will only be part of the payload
|
collections,
|
||||||
// when viewing your own profile.)
|
(item) =>
|
||||||
(item) => item.discoverable && !!item.item_count,
|
item.discoverable && !!item.item_count
|
||||||
|
? 'listedCollections'
|
||||||
|
: 'unlistedCollections',
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderListItem = useCallback(
|
||||||
|
({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
totalListLength,
|
||||||
|
isLastElement,
|
||||||
|
}: TruncatedListItemInfo<(typeof listedCollections)[number]>) => (
|
||||||
|
<CollectionListItem
|
||||||
|
key={item.id}
|
||||||
|
collection={item}
|
||||||
|
withoutBorder={isLastElement}
|
||||||
|
withAuthorHandle={false}
|
||||||
|
positionInList={index}
|
||||||
|
listSize={totalListLength}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasCollections =
|
const hasCollections =
|
||||||
@ -167,16 +190,26 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
</Subheading>
|
</Subheading>
|
||||||
{hasCollections ? (
|
{hasCollections ? (
|
||||||
<ItemList>
|
<ItemList>
|
||||||
{listedCollections.map((item, index) => (
|
<TruncatedListItems
|
||||||
<CollectionListItem
|
visibleItems={listedCollections}
|
||||||
key={item.id}
|
truncatedItems={unlistedCollections}
|
||||||
collection={item}
|
toggleButton={{
|
||||||
withoutBorder={index === listedCollections.length - 1}
|
title: (
|
||||||
withAuthorHandle={false}
|
<FormattedMessage
|
||||||
positionInList={index + 1}
|
id='collections.unlisted_collections_with_count'
|
||||||
listSize={listedCollections.length}
|
defaultMessage='Unlisted collections ({count})'
|
||||||
/>
|
values={{ count: unlistedCollections.length }}
|
||||||
))}
|
/>
|
||||||
|
),
|
||||||
|
subtitle: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.unlisted_collections_description'
|
||||||
|
defaultMessage='These don’t appear on your profile to others. Anyone with the link can discover them.'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
renderListItem={renderListItem}
|
||||||
|
/>
|
||||||
</ItemList>
|
</ItemList>
|
||||||
) : (
|
) : (
|
||||||
<EmptyMessage
|
<EmptyMessage
|
||||||
|
|||||||
@ -2,13 +2,6 @@ import { useCallback, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import {
|
|
||||||
ListItemButton,
|
|
||||||
ListItemWrapper,
|
|
||||||
} from '@/flavours/glitch/components/list_item';
|
|
||||||
import { createAppSelector, useAppSelector } from '@/flavours/glitch/store';
|
|
||||||
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
|
|
||||||
import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react';
|
|
||||||
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
|
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
|
||||||
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
||||||
import type { RenderButtonOptions } from 'flavours/glitch/components/account_list_item';
|
import type { RenderButtonOptions } from 'flavours/glitch/components/account_list_item';
|
||||||
@ -18,13 +11,15 @@ import {
|
|||||||
} from 'flavours/glitch/components/account_list_item';
|
} from 'flavours/glitch/components/account_list_item';
|
||||||
import { Button } from 'flavours/glitch/components/button';
|
import { Button } from 'flavours/glitch/components/button';
|
||||||
import { Callout } from 'flavours/glitch/components/callout';
|
import { Callout } from 'flavours/glitch/components/callout';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
import {
|
import {
|
||||||
Article,
|
Article,
|
||||||
ItemList,
|
ItemList,
|
||||||
} from 'flavours/glitch/components/scrollable_list/components';
|
} from 'flavours/glitch/components/scrollable_list/components';
|
||||||
|
import type { TruncatedListItemInfo } from 'flavours/glitch/components/truncated_list';
|
||||||
|
import { TruncatedListItems } from 'flavours/glitch/components/truncated_list';
|
||||||
import { me } from 'flavours/glitch/initial_state';
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
import type { Account } from 'flavours/glitch/models/account';
|
import type { Account } from 'flavours/glitch/models/account';
|
||||||
|
import { createAppSelector, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import { useConfirmRevoke } from './revoke_collection_inclusion_modal';
|
import { useConfirmRevoke } from './revoke_collection_inclusion_modal';
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
@ -99,13 +94,9 @@ export const CollectionAccountsList: React.FC<{
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const confirmRevoke = useConfirmRevoke(collection);
|
const confirmRevoke = useConfirmRevoke(collection);
|
||||||
const listHeadingRef = useRef<HTMLHeadingElement>(null);
|
const listHeadingRef = useRef<HTMLHeadingElement>(null);
|
||||||
const [canShowHiddenAccounts, setCanShowHiddenAccounts] = useState(false);
|
|
||||||
const toggleHiddenAccounts = useCallback(() => {
|
|
||||||
setCanShowHiddenAccounts((prev) => !prev);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isOwnCollection = collection?.account_id === me;
|
const isOwnCollection = collection?.account_id === me;
|
||||||
const { items = [], account_id: collectionOwnerId, id } = collection ?? {};
|
const { account_id: collectionOwnerId, id } = collection ?? {};
|
||||||
|
|
||||||
const relationships = useAppSelector((state) => state.relationships);
|
const relationships = useAppSelector((state) => state.relationships);
|
||||||
const collectionAccounts = useAppSelector((state) =>
|
const collectionAccounts = useAppSelector((state) =>
|
||||||
@ -135,9 +126,6 @@ export const CollectionAccountsList: React.FC<{
|
|||||||
return { visibleAccounts, hiddenAccounts };
|
return { visibleAccounts, hiddenAccounts };
|
||||||
}, [collectionAccounts, relationships]);
|
}, [collectionAccounts, relationships]);
|
||||||
|
|
||||||
const hasHiddenAccounts = hiddenAccounts.length > 0;
|
|
||||||
const initialListSize = visibleAccounts.length + (hasHiddenAccounts ? 1 : 0);
|
|
||||||
|
|
||||||
const renderAccountItemButton = useCallback(
|
const renderAccountItemButton = useCallback(
|
||||||
({ relationship, accountId }: RenderButtonOptions) => {
|
({ relationship, accountId }: RenderButtonOptions) => {
|
||||||
// When viewing your own collection, only show the Follow button
|
// When viewing your own collection, only show the Follow button
|
||||||
@ -165,6 +153,28 @@ export const CollectionAccountsList: React.FC<{
|
|||||||
[collectionOwnerId, confirmRevoke],
|
[collectionOwnerId, confirmRevoke],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderListItem = useCallback(
|
||||||
|
({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
totalListLength,
|
||||||
|
isLastElement,
|
||||||
|
}: TruncatedListItemInfo<Account>) => (
|
||||||
|
<Article
|
||||||
|
key={item.id}
|
||||||
|
aria-posinset={index + 1}
|
||||||
|
aria-setsize={totalListLength}
|
||||||
|
>
|
||||||
|
<AccountListItem
|
||||||
|
accountId={item.id}
|
||||||
|
withBorder={!isLastElement}
|
||||||
|
renderButton={renderAccountItemButton}
|
||||||
|
/>
|
||||||
|
</Article>
|
||||||
|
),
|
||||||
|
[renderAccountItemButton],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3
|
<h3
|
||||||
@ -194,71 +204,28 @@ export const CollectionAccountsList: React.FC<{
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
emptyMessage={intl.formatMessage(messages.empty)}
|
emptyMessage={intl.formatMessage(messages.empty)}
|
||||||
>
|
>
|
||||||
{visibleAccounts.map(({ id }, index) => (
|
<TruncatedListItems
|
||||||
<Article
|
visibleItems={visibleAccounts}
|
||||||
key={id}
|
truncatedItems={hiddenAccounts}
|
||||||
aria-posinset={index + 1}
|
toggleButton={{
|
||||||
aria-setsize={initialListSize}
|
icon: VisibilityOffIcon,
|
||||||
>
|
title: (
|
||||||
<AccountListItem
|
<FormattedMessage
|
||||||
accountId={id}
|
id='collections.hidden_accounts_link'
|
||||||
withBorder={index !== items.length - 1 || hasHiddenAccounts}
|
defaultMessage='{count, plural, one {# hidden account} other {# hidden accounts}}'
|
||||||
renderButton={renderAccountItemButton}
|
values={{ count: hiddenAccounts.length }}
|
||||||
/>
|
|
||||||
</Article>
|
|
||||||
))}
|
|
||||||
{hasHiddenAccounts && (
|
|
||||||
<Article
|
|
||||||
aria-posinset={initialListSize}
|
|
||||||
aria-setsize={initialListSize}
|
|
||||||
>
|
|
||||||
<ListItemWrapper
|
|
||||||
icon={<Icon id='visibility-off' icon={VisibilityOffIcon} />}
|
|
||||||
iconEnd={
|
|
||||||
<Icon
|
|
||||||
id='open-status'
|
|
||||||
icon={
|
|
||||||
canShowHiddenAccounts
|
|
||||||
? KeyboardArrowUpIcon
|
|
||||||
: KeyboardArrowDownIcon
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListItemButton
|
|
||||||
aria-expanded={canShowHiddenAccounts}
|
|
||||||
onClick={toggleHiddenAccounts}
|
|
||||||
subtitle={
|
|
||||||
<FormattedMessage
|
|
||||||
id='collections.hidden_accounts_description'
|
|
||||||
defaultMessage='You’ve blocked or muted {count, plural, one {this user} other {these users}}'
|
|
||||||
values={{ count: hiddenAccounts.length }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='collections.hidden_accounts_link'
|
|
||||||
defaultMessage='{count, plural, one {# hidden account} other {# hidden accounts}}'
|
|
||||||
values={{ count: hiddenAccounts.length }}
|
|
||||||
/>
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItemWrapper>
|
|
||||||
</Article>
|
|
||||||
)}
|
|
||||||
{canShowHiddenAccounts &&
|
|
||||||
hiddenAccounts.map(({ id }, index) => (
|
|
||||||
<Article
|
|
||||||
key={id}
|
|
||||||
aria-posinset={initialListSize + index + 1}
|
|
||||||
aria-setsize={initialListSize + hiddenAccounts.length}
|
|
||||||
>
|
|
||||||
<AccountListItem
|
|
||||||
accountId={id}
|
|
||||||
withBorder={index !== hiddenAccounts.length - 1}
|
|
||||||
renderButton={renderAccountItemButton}
|
|
||||||
/>
|
/>
|
||||||
</Article>
|
),
|
||||||
))}
|
subtitle: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.hidden_accounts_description'
|
||||||
|
defaultMessage='You’ve blocked or muted {count, plural, one {this user} other {these users}}'
|
||||||
|
values={{ count: hiddenAccounts.length }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
renderListItem={renderListItem}
|
||||||
|
/>
|
||||||
</ItemList>
|
</ItemList>
|
||||||
</SensitiveScreen>
|
</SensitiveScreen>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user