mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Allow removing yourself from a collection (#38096)
This commit is contained in:
@@ -49,3 +49,9 @@ export const apiRemoveCollectionItem = (collectionId: string, itemId: string) =>
|
||||
apiRequestDelete<WrappedCollectionAccountItem>(
|
||||
`v1_alpha/collections/${collectionId}/items/${itemId}`,
|
||||
);
|
||||
|
||||
export const apiRevokeCollectionInclusion = (
|
||||
collectionId: string,
|
||||
itemId: string,
|
||||
) =>
|
||||
apiRequestPost(`v1_alpha/collections/${collectionId}/items/${itemId}/revoke`);
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface ApiCollectionWithAccountsJSON extends ApiWrappedCollectionJSON
|
||||
/**
|
||||
* Nested account item
|
||||
*/
|
||||
interface CollectionAccountItem {
|
||||
export interface CollectionAccountItem {
|
||||
id: string;
|
||||
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
||||
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
||||
|
||||
@@ -2,17 +2,23 @@ import { Fragment, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import { useRelationship } from '@/mastodon/hooks/useRelationship';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import type {
|
||||
ApiCollectionJSON,
|
||||
CollectionAccountItem,
|
||||
} from 'mastodon/api_types/collections';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import {
|
||||
Article,
|
||||
ItemList,
|
||||
} from 'mastodon/components/scrollable_list/components';
|
||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||
import { useDismissible } from 'mastodon/hooks/useDismissible';
|
||||
import { useRelationship } from 'mastodon/hooks/useRelationship';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
@@ -62,6 +68,52 @@ const AccountItem: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className={classes.revokeControlWrapper}>
|
||||
<Button secondary onClick={dismiss}>
|
||||
<FormattedMessage
|
||||
id='collections.detail.accept_inclusion'
|
||||
defaultMessage='Okay'
|
||||
tagName={Fragment}
|
||||
/>
|
||||
</Button>
|
||||
<Button secondary onClick={confirmRevoke}>
|
||||
<FormattedMessage
|
||||
id='collections.detail.revoke_inclusion'
|
||||
defaultMessage='Remove me'
|
||||
tagName={Fragment}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SensitiveScreen: React.FC<{
|
||||
sensitive: boolean | undefined;
|
||||
focusTargetRef: React.RefObject<HTMLHeadingElement>;
|
||||
@@ -166,6 +218,10 @@ export const CollectionAccountsList: React.FC<{
|
||||
accountId={currentUserInCollection.account_id}
|
||||
collectionOwnerId={collection.account_id}
|
||||
/>
|
||||
<RevokeControls
|
||||
collectionId={collection.id}
|
||||
collectionItem={currentUserInCollection}
|
||||
/>
|
||||
</Article>
|
||||
<h3
|
||||
className={classes.columnSubheading}
|
||||
@@ -33,6 +33,10 @@ const messages = defineMessages({
|
||||
id: 'collections.report_collection',
|
||||
defaultMessage: 'Report this collection',
|
||||
},
|
||||
revoke: {
|
||||
id: 'collections.revoke_collection_inclusion',
|
||||
defaultMessage: 'Remove myself from this collection',
|
||||
},
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
@@ -71,6 +75,22 @@ export const CollectionMenu: React.FC<{
|
||||
);
|
||||
}, [collection, dispatch]);
|
||||
|
||||
const currentAccountInCollection = collection.items.find(
|
||||
(item) => item.account_id === me,
|
||||
);
|
||||
|
||||
const openRevokeConfirmation = useCallback(() => {
|
||||
void dispatch(
|
||||
openModal({
|
||||
modalType: 'REVOKE_COLLECTION_INCLUSION',
|
||||
modalProps: {
|
||||
collectionId: collection.id,
|
||||
collectionItemId: currentAccountInCollection?.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [collection.id, currentAccountInCollection?.id, dispatch]);
|
||||
|
||||
const menu = useMemo(() => {
|
||||
if (isOwnCollection) {
|
||||
const commonItems: MenuItem[] = [
|
||||
@@ -99,34 +119,43 @@ export const CollectionMenu: React.FC<{
|
||||
} else {
|
||||
return commonItems;
|
||||
}
|
||||
} else if (ownerAccount) {
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
text: intl.formatMessage(messages.report),
|
||||
action: openReportModal,
|
||||
},
|
||||
];
|
||||
const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`;
|
||||
// Don't show menu link to featured collections while on that very page
|
||||
if (
|
||||
!matchPath(location.pathname, {
|
||||
path: featuredCollectionsPath,
|
||||
exact: true,
|
||||
})
|
||||
) {
|
||||
items.unshift(
|
||||
...[
|
||||
{
|
||||
text: intl.formatMessage(messages.viewOtherCollections),
|
||||
to: featuredCollectionsPath,
|
||||
},
|
||||
null,
|
||||
],
|
||||
);
|
||||
}
|
||||
return items;
|
||||
} else {
|
||||
return [];
|
||||
const items: MenuItem[] = [];
|
||||
|
||||
if (ownerAccount) {
|
||||
const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`;
|
||||
// Don't show menu link to featured collections while on that very page
|
||||
if (
|
||||
!matchPath(location.pathname, {
|
||||
path: featuredCollectionsPath,
|
||||
exact: true,
|
||||
})
|
||||
) {
|
||||
items.push(
|
||||
...[
|
||||
{
|
||||
text: intl.formatMessage(messages.viewOtherCollections),
|
||||
to: featuredCollectionsPath,
|
||||
},
|
||||
null,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentAccountInCollection) {
|
||||
items.push({
|
||||
text: intl.formatMessage(messages.revoke),
|
||||
action: openRevokeConfirmation,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
text: intl.formatMessage(messages.report),
|
||||
action: openReportModal,
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
}, [
|
||||
isOwnCollection,
|
||||
@@ -134,6 +163,8 @@ export const CollectionMenu: React.FC<{
|
||||
id,
|
||||
openDeleteConfirmation,
|
||||
context,
|
||||
currentAccountInCollection,
|
||||
openRevokeConfirmation,
|
||||
ownerAccount,
|
||||
openReportModal,
|
||||
]);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { me } from 'mastodon/initial_state';
|
||||
import { fetchCollection } from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { CollectionAccountsList } from './collection_list';
|
||||
import { CollectionAccountsList } from './accounts_list';
|
||||
import { CollectionMetaData } from './collection_list_item';
|
||||
import { CollectionMenu } from './collection_menu';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { showAlert } from 'mastodon/actions/alerts';
|
||||
import type { BaseConfirmationModalProps } from 'mastodon/features/ui/components/confirmation_modals/confirmation_modal';
|
||||
import { ConfirmationModal } from 'mastodon/features/ui/components/confirmation_modals/confirmation_modal';
|
||||
import { revokeCollectionInclusion } from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
revokeCollectionInclusionTitle: {
|
||||
id: 'confirmations.revoke_collection_inclusion.title',
|
||||
defaultMessage: 'Remove yourself from this collection?',
|
||||
},
|
||||
revokeCollectionInclusionMessage: {
|
||||
id: 'confirmations.revoke_collection_inclusion.message',
|
||||
defaultMessage:
|
||||
"This action is permanent, and the curator won't be able to re-add you to the collection later on.",
|
||||
},
|
||||
revokeCollectionInclusionConfirm: {
|
||||
id: 'confirmations.revoke_collection_inclusion.confirm',
|
||||
defaultMessage: 'Remove me',
|
||||
},
|
||||
});
|
||||
|
||||
export const RevokeCollectionInclusionModal: React.FC<
|
||||
{
|
||||
collectionId: string;
|
||||
collectionItemId: string;
|
||||
} & BaseConfirmationModalProps
|
||||
> = ({ collectionId, collectionItemId, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const collectionName = useAppSelector(
|
||||
(state) => state.collections.collections[collectionId]?.name,
|
||||
);
|
||||
|
||||
const onConfirm = useCallback(async () => {
|
||||
try {
|
||||
await dispatch(
|
||||
revokeCollectionInclusion({
|
||||
collectionId,
|
||||
itemId: collectionItemId,
|
||||
}),
|
||||
).unwrap();
|
||||
|
||||
dispatch(
|
||||
showAlert({
|
||||
message: intl.formatMessage(
|
||||
{
|
||||
id: 'collections.revoke_inclusion.confirmation',
|
||||
defaultMessage: 'You\'ve been removed from "{collection}"',
|
||||
},
|
||||
{
|
||||
collection: collectionName,
|
||||
},
|
||||
),
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
dispatch(
|
||||
showAlert({
|
||||
message: intl.formatMessage({
|
||||
id: 'collections.revoke_inclusion.error',
|
||||
defaultMessage: 'There was an error, please try again later.',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [dispatch, collectionId, collectionName, collectionItemId, intl]);
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
title={intl.formatMessage(messages.revokeCollectionInclusionTitle)}
|
||||
message={intl.formatMessage(messages.revokeCollectionInclusionMessage)}
|
||||
confirm={intl.formatMessage(messages.revokeCollectionInclusionConfirm)}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -103,3 +103,24 @@
|
||||
line-height: 1.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.revokeControlWrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: -10px;
|
||||
padding-bottom: 16px;
|
||||
padding-inline: calc(26px + var(--avatar-width)) 16px;
|
||||
|
||||
:global(.button) {
|
||||
min-width: 30%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
--avatar-width: 46px;
|
||||
|
||||
@container (width < 360px) {
|
||||
--avatar-width: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ interface ConfirmationModalProps {
|
||||
cancel?: React.ReactNode;
|
||||
secondary?: React.ReactNode;
|
||||
onSecondary?: () => void;
|
||||
onConfirm: () => void;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
noCloseOnConfirm?: boolean;
|
||||
extraContent?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
@@ -56,7 +56,7 @@ export const ConfirmationModal: React.FC<
|
||||
onClose();
|
||||
}
|
||||
|
||||
onConfirm();
|
||||
void onConfirm();
|
||||
}, [onClose, onConfirm, noCloseOnConfirm]);
|
||||
|
||||
const handleSecondary = useCallback(() => {
|
||||
|
||||
@@ -80,6 +80,7 @@ export const MODAL_COMPONENTS = {
|
||||
'REPORT': ReportModal,
|
||||
'REPORT_COLLECTION': ReportCollectionModal,
|
||||
'SHARE_COLLECTION': () => import('@/mastodon/features/collections/detail/share_modal').then(module => ({ default: module.CollectionShareModal })),
|
||||
'REVOKE_COLLECTION_INCLUSION': () => import('@/mastodon/features/collections/detail/revoke_collection_inclusion_modal').then(module => ({ default: module.RevokeCollectionInclusionModal })),
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: AltTextModal }),
|
||||
|
||||
@@ -338,12 +338,14 @@
|
||||
"collections.create_collection": "Create collection",
|
||||
"collections.delete_collection": "Delete collection",
|
||||
"collections.description_length_hint": "100 characters limit",
|
||||
"collections.detail.accept_inclusion": "Okay",
|
||||
"collections.detail.accounts_heading": "Accounts",
|
||||
"collections.detail.author_added_you": "{author} added you to this collection",
|
||||
"collections.detail.curated_by_author": "Curated by {author}",
|
||||
"collections.detail.curated_by_you": "Curated by you",
|
||||
"collections.detail.loading": "Loading collection…",
|
||||
"collections.detail.other_accounts_in_collection": "Others in this collection:",
|
||||
"collections.detail.revoke_inclusion": "Remove me",
|
||||
"collections.detail.sensitive_note": "This collection contains accounts and content that may be sensitive to some users.",
|
||||
"collections.detail.share": "Share this collection",
|
||||
"collections.edit_details": "Edit details",
|
||||
@@ -359,6 +361,9 @@
|
||||
"collections.old_last_post_note": "Last posted over a week ago",
|
||||
"collections.remove_account": "Remove this account",
|
||||
"collections.report_collection": "Report this collection",
|
||||
"collections.revoke_collection_inclusion": "Remove myself from this collection",
|
||||
"collections.revoke_inclusion.confirmation": "You've been removed from \"{collection}\"",
|
||||
"collections.revoke_inclusion.error": "There was an error, please try again later.",
|
||||
"collections.search_accounts_label": "Search for accounts to add…",
|
||||
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
|
||||
"collections.sensitive": "Sensitive",
|
||||
@@ -482,6 +487,9 @@
|
||||
"confirmations.remove_from_followers.confirm": "Remove follower",
|
||||
"confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?",
|
||||
"confirmations.remove_from_followers.title": "Remove follower?",
|
||||
"confirmations.revoke_collection_inclusion.confirm": "Remove me",
|
||||
"confirmations.revoke_collection_inclusion.message": "This action is permanent, and the curator won't be able to re-add you to the collection later on.",
|
||||
"confirmations.revoke_collection_inclusion.title": "Remove yourself from this collection?",
|
||||
"confirmations.revoke_quote.confirm": "Remove post",
|
||||
"confirmations.revoke_quote.message": "This action cannot be undone.",
|
||||
"confirmations.revoke_quote.title": "Remove post?",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
apiDeleteCollection,
|
||||
apiAddCollectionItem,
|
||||
apiRemoveCollectionItem,
|
||||
apiRevokeCollectionInclusion,
|
||||
} from '@/mastodon/api/collections';
|
||||
import type {
|
||||
ApiCollectionJSON,
|
||||
@@ -17,6 +18,7 @@ import type {
|
||||
} from '@/mastodon/api_types/collections';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import {
|
||||
createAppAsyncThunk,
|
||||
createAppSelector,
|
||||
createDataLoadingThunk,
|
||||
} from '@/mastodon/store/typed_functions';
|
||||
@@ -158,7 +160,10 @@ const collectionSlice = createSlice({
|
||||
* Removing an account from a collection
|
||||
*/
|
||||
|
||||
builder.addCase(removeCollectionItem.fulfilled, (state, action) => {
|
||||
const removeAccountFromCollection = (
|
||||
state: CollectionState,
|
||||
action: { meta: { arg: { itemId: string; collectionId: string } } },
|
||||
) => {
|
||||
const { itemId, collectionId } = action.meta.arg;
|
||||
|
||||
const collection = state.collections[collectionId];
|
||||
@@ -167,7 +172,17 @@ const collectionSlice = createSlice({
|
||||
(item) => item.id !== itemId,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
builder.addCase(
|
||||
removeCollectionItem.fulfilled,
|
||||
removeAccountFromCollection,
|
||||
);
|
||||
|
||||
builder.addCase(
|
||||
revokeCollectionInclusion.fulfilled,
|
||||
removeAccountFromCollection,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -218,6 +233,12 @@ export const removeCollectionItem = createDataLoadingThunk(
|
||||
apiRemoveCollectionItem(collectionId, itemId),
|
||||
);
|
||||
|
||||
export const revokeCollectionInclusion = createAppAsyncThunk(
|
||||
`${collectionSlice.name}/revokeCollectionInclusion`,
|
||||
({ collectionId, itemId }: { collectionId: string; itemId: string }) =>
|
||||
apiRevokeCollectionInclusion(collectionId, itemId),
|
||||
);
|
||||
|
||||
export const collections = collectionSlice.reducer;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user