diff --git a/app/javascript/flavours/glitch/features/collections/detail/accounts_list.tsx b/app/javascript/flavours/glitch/features/collections/detail/accounts_list.tsx
index 05789ff85c..b706dc90ba 100644
--- a/app/javascript/flavours/glitch/features/collections/detail/accounts_list.tsx
+++ b/app/javascript/flavours/glitch/features/collections/detail/accounts_list.tsx
@@ -2,26 +2,26 @@ import { useCallback, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-import { Callout } from '@/flavours/glitch/components/callout';
-import { FollowButton } from '@/flavours/glitch/components/follow_button';
-import { openModal } from 'flavours/glitch/actions/modal';
-import type {
- ApiCollectionJSON,
- CollectionAccountItem,
-} from 'flavours/glitch/api_types/collections';
+import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Account } from 'flavours/glitch/components/account';
import { Button } from 'flavours/glitch/components/button';
-import { DisplayName } from 'flavours/glitch/components/display_name';
+import { Callout } from 'flavours/glitch/components/callout';
+import { FollowButton } from 'flavours/glitch/components/follow_button';
+import {
+ NumberFields,
+ NumberFieldsItem,
+} from 'flavours/glitch/components/number_fields';
+import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import {
Article,
ItemList,
} from 'flavours/glitch/components/scrollable_list/components';
+import { ShortNumber } from 'flavours/glitch/components/short_number';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
-import { useDismissible } from 'flavours/glitch/hooks/useDismissible';
import { useRelationship } from 'flavours/glitch/hooks/useRelationship';
import { me } from 'flavours/glitch/initial_state';
-import { useAppDispatch } from 'flavours/glitch/store';
+import { useConfirmRevoke } from './revoke_collection_inclusion_modal';
import classes from './styles.module.scss';
const messages = defineMessages({
@@ -31,28 +31,33 @@ const messages = defineMessages({
},
});
-const SimpleAuthorName: React.FC<{ id: string }> = ({ id }) => {
- const account = useAccount(id);
- return ;
-};
-
const AccountItem: React.FC<{
accountId: string | undefined;
collectionOwnerId: string;
+ onRevoke: () => void;
withBio?: boolean;
withBorder?: boolean;
-}> = ({ accountId, withBio = true, withBorder = true, collectionOwnerId }) => {
+}> = ({
+ accountId,
+ collectionOwnerId,
+ onRevoke,
+ withBio = true,
+ withBorder = true,
+}) => {
+ const intl = useIntl();
+ const account = useAccount(accountId);
const relationship = useRelationship(accountId);
- if (!accountId) {
+ if (!accountId || !account) {
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 isOwnAccount = accountId === me;
const withoutButton =
- accountId === me ||
+ isOwnAccount ||
!relationship ||
(collectionOwnerId === me &&
(relationship.following || relationship.requested));
@@ -66,52 +71,55 @@ const AccountItem: React.FC<{
withBorder={false}
withMenu={false}
className={classes.accountItem}
+ extraAccountInfo={
+
+
+ }
+ hint={intl.formatNumber(account.followers_count)}
+ >
+
+
+
+
+ }
+ hint={intl.formatNumber(account.statuses_count)}
+ >
+
+
+
+
+ }
+ >
+
+
+
+ }
/>
{!withoutButton && }
-
- );
-};
-
-const RevokeControls: React.FC<{
- collectionId: string;
- collectionItem: CollectionAccountItem;
-}> = ({ collectionId, collectionItem }) => {
- const dispatch = useAppDispatch();
-
- const confirmRevoke = useCallback(() => {
- void dispatch(
- openModal({
- modalType: 'REVOKE_COLLECTION_INCLUSION',
- modalProps: {
- collectionId,
- collectionItemId: collectionItem.id,
- },
- }),
- );
- }, [collectionId, collectionItem.id, dispatch]);
-
- const { wasDismissed, dismiss } = useDismissible(
- `collection-revoke-hint-${collectionItem.id}`,
- );
-
- if (wasDismissed) {
- return null;
- }
-
- return (
-
-
-
+ {isOwnAccount && (
+
+ )}
);
};
@@ -159,46 +167,16 @@ const SensitiveScreen: React.FC<{
);
};
-/**
- * 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 confirmRevoke = useConfirmRevoke(collection);
const listHeadingRef = useRef(null);
const isOwnCollection = collection?.account_id === me;
- const { items, currentUserInCollection } = getCollectionItems(collection);
+ const { items = [] } = collection ?? {};
return (
- {collection && currentUserInCollection ? (
- <>
-
- ,
- }}
- />
-
-
-
-
-
-
-
-
- >
- ) : (
-
- {collection ? (
-
- ) : (
-
- )}
-
- )}
+
+ {collection ? (
+
+ ) : (
+
+ )}
+
{collection && (
- {items.map(({ account_id }, index, items) => (
+ {items.map(({ account_id }, index) => (
))}
diff --git a/app/javascript/flavours/glitch/features/collections/detail/index.tsx b/app/javascript/flavours/glitch/features/collections/detail/index.tsx
index 157bcbbc31..28e67837de 100644
--- a/app/javascript/flavours/glitch/features/collections/detail/index.tsx
+++ b/app/javascript/flavours/glitch/features/collections/detail/index.tsx
@@ -11,17 +11,20 @@ import { useAccountHandle } from '@/flavours/glitch/components/display_name/defa
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
+import { Callout } from 'flavours/glitch/components/callout';
import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
+import { DisplayName } from 'flavours/glitch/components/display_name';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { Scrollable } from 'flavours/glitch/components/scrollable_list/components';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
-import { domain } from 'flavours/glitch/initial_state';
+import { domain, me } from 'flavours/glitch/initial_state';
import { fetchCollection } from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { CollectionAccountsList } from './accounts_list';
import { CollectionMenu } from './collection_menu';
+import { useConfirmRevoke } from './revoke_collection_inclusion_modal';
import classes from './styles.module.scss';
const messages = defineMessages({
@@ -62,14 +65,54 @@ export const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
);
};
+export const RevokeControls: React.FC<{
+ collection: ApiCollectionJSON;
+}> = ({ collection }) => {
+ const authorAccount = useAccount(collection.account_id);
+ const confirmRevoke = useConfirmRevoke(collection);
+
+ return (
+
+ }
+ primaryLabel={
+
+ }
+ onPrimary={confirmRevoke}
+ >
+ ,
+ date: '{date}', // TODO: Data not yet provided by API
+ }}
+ />
+
+ );
+};
+
const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
collection,
}) => {
const intl = useIntl();
- const { name, description, tag, account_id } = collection;
+ const { name, description, tag, account_id, items } = collection;
const dispatch = useAppDispatch();
const history = useHistory();
+ const isOwnCollection = account_id === me;
+ const currentUserIndex = items.findIndex(
+ (account) => account.account_id === me,
+ );
+ const isCurrentUserInCollection = !isOwnCollection && currentUserIndex > -1;
+
const handleShare = useCallback(() => {
dispatch(
openModal({
@@ -115,6 +158,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
{description && {description}
}
+ {isCurrentUserInCollection && }
);
};
diff --git a/app/javascript/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal.tsx b/app/javascript/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal.tsx
index cf7eac6a0c..d75f41751e 100644
--- a/app/javascript/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal.tsx
+++ b/app/javascript/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal.tsx
@@ -3,8 +3,11 @@ import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { showAlert } from 'flavours/glitch/actions/alerts';
+import { openModal } from 'flavours/glitch/actions/modal';
+import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import type { BaseConfirmationModalProps } from 'flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal';
import { ConfirmationModal } from 'flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal';
+import { me } from 'flavours/glitch/initial_state';
import { revokeCollectionInclusion } from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
@@ -24,6 +27,24 @@ const messages = defineMessages({
},
});
+export function useConfirmRevoke(collection?: ApiCollectionJSON) {
+ const dispatch = useAppDispatch();
+ const { id, items = [] } = collection ?? {};
+ const ownCollectionItemId = items.find((item) => item.account_id === me)?.id;
+
+ return useCallback(() => {
+ void dispatch(
+ openModal({
+ modalType: 'REVOKE_COLLECTION_INCLUSION',
+ modalProps: {
+ collectionId: id,
+ collectionItemId: ownCollectionItemId,
+ },
+ }),
+ );
+ }, [dispatch, id, ownCollectionItemId]);
+}
+
export const RevokeCollectionInclusionModal: React.FC<
{
collectionId: string;
diff --git a/app/javascript/flavours/glitch/features/collections/detail/styles.module.scss b/app/javascript/flavours/glitch/features/collections/detail/styles.module.scss
index 62e2285294..54d421e037 100644
--- a/app/javascript/flavours/glitch/features/collections/detail/styles.module.scss
+++ b/app/javascript/flavours/glitch/features/collections/detail/styles.module.scss
@@ -1,4 +1,7 @@
.header {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
padding: 24px;
}
@@ -49,7 +52,6 @@
.description {
font-size: 15px;
- margin-top: 12px;
}
.headerButtonWrapper {
@@ -59,8 +61,14 @@
.iconButton {
box-sizing: content-box;
- padding: 5px;
+ padding: 7px;
border-radius: 4px;
+ border: 1px solid var(--color-border-primary);
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
}
.itemList {
@@ -81,6 +89,18 @@
&[data-with-border='true'] {
border-bottom: 1px solid var(--color-border-primary);
}
+
+ :global(.account__note) {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ line-clamp: 3;
+ }
+
+ // Hide 'No description provided' message added by `Account` component
+ :global(.account__note--missing) {
+ display: none;
+ }
}
.accountItem {
@@ -93,26 +113,3 @@
flex-grow: 1;
}
-
-.youWereAddedWrapper {
- padding-bottom: 16px;
-}
-
-.revokeControlWrapper {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 10px;
- margin-bottom: 8px;
-
- :global(.button) {
- min-width: 30%;
- white-space: normal;
- }
-
- --avatar-width: 46px;
-
- @container (width < 360px) {
- --avatar-width: 35px;
- }
-}