[Glitch] Hide account list in sensitive collections
Port 3557be5d4d to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -74,6 +74,7 @@ interface AccountProps {
|
|||||||
defaultAction?: 'block' | 'mute';
|
defaultAction?: 'block' | 'mute';
|
||||||
withBio?: boolean;
|
withBio?: boolean;
|
||||||
withMenu?: boolean;
|
withMenu?: boolean;
|
||||||
|
withBorder?: boolean;
|
||||||
extraAccountInfo?: React.ReactNode;
|
extraAccountInfo?: React.ReactNode;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -86,6 +87,7 @@ export const Account: React.FC<AccountProps> = ({
|
|||||||
defaultAction,
|
defaultAction,
|
||||||
withBio,
|
withBio,
|
||||||
withMenu = true,
|
withMenu = true,
|
||||||
|
withBorder = true,
|
||||||
extraAccountInfo,
|
extraAccountInfo,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -291,6 +293,7 @@ export const Account: React.FC<AccountProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames('account', {
|
className={classNames('account', {
|
||||||
'account--minimal': minimal,
|
'account--minimal': minimal,
|
||||||
|
'account--without-border': !withBorder,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const ItemList = forwardRef<
|
|||||||
emptyMessage?: React.ReactNode;
|
emptyMessage?: React.ReactNode;
|
||||||
}
|
}
|
||||||
>(({ isLoading, emptyMessage, className, children, ...otherProps }, ref) => {
|
>(({ isLoading, emptyMessage, className, children, ...otherProps }, ref) => {
|
||||||
if (Children.count(children) === 0 && emptyMessage) {
|
if (!isLoading && Children.count(children) === 0 && emptyMessage) {
|
||||||
return <div className='empty-column-indicator'>{emptyMessage}</div>;
|
return <div className='empty-column-indicator'>{emptyMessage}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { Fragment, useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { Button } from '@/flavours/glitch/components/button';
|
||||||
|
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
|
||||||
|
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
||||||
|
import { Account } from 'flavours/glitch/components/account';
|
||||||
|
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||||
|
import {
|
||||||
|
Article,
|
||||||
|
ItemList,
|
||||||
|
} from 'flavours/glitch/components/scrollable_list/components';
|
||||||
|
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
||||||
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
empty: {
|
||||||
|
id: 'collections.accounts.empty_title',
|
||||||
|
defaultMessage: 'This collection is empty',
|
||||||
|
},
|
||||||
|
accounts: {
|
||||||
|
id: 'collections.detail.accounts_heading',
|
||||||
|
defaultMessage: 'Accounts',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const SimpleAuthorName: React.FC<{ id: string }> = ({ id }) => {
|
||||||
|
const account = useAccount(id);
|
||||||
|
return <DisplayName account={account} variant='simple' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountItem: React.FC<{
|
||||||
|
accountId: string | undefined;
|
||||||
|
collectionOwnerId: string;
|
||||||
|
withBorder?: boolean;
|
||||||
|
}> = ({ accountId, withBorder = true, 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}
|
||||||
|
withBorder={withBorder}
|
||||||
|
id={accountId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SensitiveScreen: React.FC<{
|
||||||
|
sensitive: boolean | undefined;
|
||||||
|
focusTargetRef: React.RefObject<HTMLHeadingElement>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ sensitive, focusTargetRef, children }) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(!sensitive);
|
||||||
|
|
||||||
|
const showAnyway = useCallback(() => {
|
||||||
|
setIsVisible(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
focusTargetRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
}, [focusTargetRef]);
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.sensitiveWarning}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.detail.sensitive_note'
|
||||||
|
defaultMessage='This collection contains accounts and content that may be sensitive to some users.'
|
||||||
|
tagName='p'
|
||||||
|
/>
|
||||||
|
<Button onClick={showAnyway}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='content_warning.show'
|
||||||
|
defaultMessage='Show anyway'
|
||||||
|
tagName={Fragment}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the collection's account items. If the current user's account
|
||||||
|
* is part of the collection, it will be returned separately.
|
||||||
|
*/
|
||||||
|
function getCollectionItems(collection: ApiCollectionJSON | undefined) {
|
||||||
|
if (!collection)
|
||||||
|
return {
|
||||||
|
currentUserInCollection: null,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { account_id, items } = collection;
|
||||||
|
|
||||||
|
const isOwnCollection = account_id === me;
|
||||||
|
const currentUserIndex = items.findIndex(
|
||||||
|
(account) => account.account_id === me,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isOwnCollection || currentUserIndex === -1) {
|
||||||
|
return {
|
||||||
|
currentUserInCollection: null,
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
currentUserInCollection: items.at(currentUserIndex) ?? null,
|
||||||
|
items: items.toSpliced(currentUserIndex, 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CollectionAccountsList: React.FC<{
|
||||||
|
collection?: ApiCollectionJSON;
|
||||||
|
isLoading: boolean;
|
||||||
|
}> = ({ collection, isLoading }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const listHeadingRef = useRef<HTMLHeadingElement>(null);
|
||||||
|
|
||||||
|
const isOwnCollection = collection?.account_id === me;
|
||||||
|
const { items, currentUserInCollection } = getCollectionItems(collection);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemList
|
||||||
|
isLoading={isLoading}
|
||||||
|
emptyMessage={intl.formatMessage(messages.empty)}
|
||||||
|
>
|
||||||
|
{collection && currentUserInCollection ? (
|
||||||
|
<>
|
||||||
|
<h3 className={classes.columnSubheading}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.detail.author_added_you'
|
||||||
|
defaultMessage='{author} added you to this collection'
|
||||||
|
values={{
|
||||||
|
author: <SimpleAuthorName id={collection.account_id} />,
|
||||||
|
}}
|
||||||
|
tagName={Fragment}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<Article
|
||||||
|
key={currentUserInCollection.account_id}
|
||||||
|
aria-posinset={1}
|
||||||
|
aria-setsize={items.length}
|
||||||
|
>
|
||||||
|
<AccountItem
|
||||||
|
withBorder={false}
|
||||||
|
accountId={currentUserInCollection.account_id}
|
||||||
|
collectionOwnerId={collection.account_id}
|
||||||
|
/>
|
||||||
|
</Article>
|
||||||
|
<h3
|
||||||
|
className={classes.columnSubheading}
|
||||||
|
tabIndex={-1}
|
||||||
|
ref={listHeadingRef}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.detail.other_accounts_in_collection'
|
||||||
|
defaultMessage='Others in this collection:'
|
||||||
|
tagName={Fragment}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<h3
|
||||||
|
className='column-subheading sr-only'
|
||||||
|
tabIndex={-1}
|
||||||
|
ref={listHeadingRef}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.accounts)}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
{collection && (
|
||||||
|
<SensitiveScreen
|
||||||
|
sensitive={!isOwnCollection && collection.sensitive}
|
||||||
|
focusTargetRef={listHeadingRef}
|
||||||
|
>
|
||||||
|
{items.map(({ account_id }, index, items) => (
|
||||||
|
<Article
|
||||||
|
key={account_id}
|
||||||
|
aria-posinset={index + (currentUserInCollection ? 2 : 1)}
|
||||||
|
aria-setsize={items.length}
|
||||||
|
>
|
||||||
|
<AccountItem
|
||||||
|
accountId={account_id}
|
||||||
|
collectionOwnerId={collection.account_id}
|
||||||
|
/>
|
||||||
|
</Article>
|
||||||
|
))}
|
||||||
|
</SensitiveScreen>
|
||||||
|
)}
|
||||||
|
</ItemList>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,11 +6,9 @@ import { Helmet } from 'react-helmet';
|
|||||||
import { useLocation, useParams } from 'react-router';
|
import { useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
import { openModal } from '@/flavours/glitch/actions/modal';
|
import { openModal } from '@/flavours/glitch/actions/modal';
|
||||||
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
|
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||||
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
||||||
import { Account } from 'flavours/glitch/components/account';
|
|
||||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
import { Column } from 'flavours/glitch/components/column';
|
import { Column } from 'flavours/glitch/components/column';
|
||||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
@@ -19,26 +17,19 @@ import {
|
|||||||
LinkedDisplayName,
|
LinkedDisplayName,
|
||||||
} from 'flavours/glitch/components/display_name';
|
} from 'flavours/glitch/components/display_name';
|
||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
import {
|
import { Scrollable } from 'flavours/glitch/components/scrollable_list/components';
|
||||||
Article,
|
|
||||||
ItemList,
|
|
||||||
Scrollable,
|
|
||||||
} from 'flavours/glitch/components/scrollable_list/components';
|
|
||||||
import { Tag } from 'flavours/glitch/components/tags/tag';
|
import { Tag } from 'flavours/glitch/components/tags/tag';
|
||||||
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
||||||
import { me } from 'flavours/glitch/initial_state';
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
import { fetchCollection } from 'flavours/glitch/reducers/slices/collections';
|
import { fetchCollection } from 'flavours/glitch/reducers/slices/collections';
|
||||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { CollectionAccountsList } from './collection_list';
|
||||||
import { CollectionMetaData } from './collection_list_item';
|
import { CollectionMetaData } from './collection_list_item';
|
||||||
import { CollectionMenu } from './collection_menu';
|
import { CollectionMenu } from './collection_menu';
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
empty: {
|
|
||||||
id: 'collections.accounts.empty_title',
|
|
||||||
defaultMessage: 'This collection is empty',
|
|
||||||
},
|
|
||||||
loading: {
|
loading: {
|
||||||
id: 'collections.detail.loading',
|
id: 'collections.detail.loading',
|
||||||
defaultMessage: 'Loading collection…',
|
defaultMessage: 'Loading collection…',
|
||||||
@@ -47,10 +38,6 @@ const messages = defineMessages({
|
|||||||
id: 'collections.detail.share',
|
id: 'collections.detail.share',
|
||||||
defaultMessage: 'Share this collection',
|
defaultMessage: 'Share this collection',
|
||||||
},
|
},
|
||||||
accounts: {
|
|
||||||
id: 'collections.detail.accounts_heading',
|
|
||||||
defaultMessage: 'Accounts',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AuthorNote: React.FC<{ id: string; previewMode?: boolean }> = ({
|
export const AuthorNote: React.FC<{ id: string; previewMode?: boolean }> = ({
|
||||||
@@ -149,33 +136,10 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
|
|||||||
collection={collection}
|
collection={collection}
|
||||||
className={classes.metaData}
|
className={classes.metaData}
|
||||||
/>
|
/>
|
||||||
<h2 className='sr-only'>{intl.formatMessage(messages.accounts)}</h2>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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<{
|
export const CollectionDetailPage: React.FC<{
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
@@ -185,7 +149,6 @@ export const CollectionDetailPage: React.FC<{
|
|||||||
const collection = useAppSelector((state) =>
|
const collection = useAppSelector((state) =>
|
||||||
id ? state.collections.collections[id] : undefined,
|
id ? state.collections.collections[id] : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLoading = !!id && !collection;
|
const isLoading = !!id && !collection;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -208,24 +171,7 @@ export const CollectionDetailPage: React.FC<{
|
|||||||
|
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
{collection && <CollectionHeader collection={collection} />}
|
{collection && <CollectionHeader collection={collection} />}
|
||||||
<ItemList
|
<CollectionAccountsList collection={collection} isLoading={isLoading} />
|
||||||
isLoading={isLoading}
|
|
||||||
emptyMessage={intl.formatMessage(messages.empty)}
|
|
||||||
>
|
|
||||||
{collection?.items.map(({ account_id }, index, items) => (
|
|
||||||
<Article
|
|
||||||
key={account_id}
|
|
||||||
data-id={account_id}
|
|
||||||
aria-posinset={index + 1}
|
|
||||||
aria-setsize={items.length}
|
|
||||||
>
|
|
||||||
<CollectionAccountItem
|
|
||||||
accountId={account_id}
|
|
||||||
collectionOwnerId={collection.account_id}
|
|
||||||
/>
|
|
||||||
</Article>
|
|
||||||
))}
|
|
||||||
</ItemList>
|
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
@@ -57,6 +57,18 @@
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.columnSubheading {
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.displayNameWithAvatar {
|
.displayNameWithAvatar {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -76,3 +88,18 @@
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sensitiveWarning {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 460px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 60px 30px;
|
||||||
|
gap: 20px;
|
||||||
|
text-align: center;
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Helmet } from 'react-helmet';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import CollectionsFilledIcon from '@/material-icons/400-24px/category-fill.svg?react';
|
||||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||||
import { Column } from 'flavours/glitch/components/column';
|
import { Column } from 'flavours/glitch/components/column';
|
||||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
@@ -73,8 +73,8 @@ export const Collections: React.FC<{
|
|||||||
>
|
>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
title={intl.formatMessage(messages.heading)}
|
title={intl.formatMessage(messages.heading)}
|
||||||
icon='list-ul'
|
icon='collections'
|
||||||
iconComponent={ListAltIcon}
|
iconComponent={CollectionsFilledIcon}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
extraButton={
|
extraButton={
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -2074,7 +2074,10 @@ body > [data-popper-placement] {
|
|||||||
|
|
||||||
.account {
|
.account {
|
||||||
padding: 10px; // glitch: reduced padding
|
padding: 10px; // glitch: reduced padding
|
||||||
border-bottom: 1px solid var(--color-border-primary);
|
|
||||||
|
&:not(&--without-border) {
|
||||||
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.account__display-name {
|
.account__display-name {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user