[Glitch] Allow removing yourself from a collection
Port 3a796544e3c20687c14632e87c5d099154a0b8e5 to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
4bcd0caa79
commit
e616200e59
@ -49,3 +49,9 @@ export const apiRemoveCollectionItem = (collectionId: string, itemId: string) =>
|
|||||||
apiRequestDelete<WrappedCollectionAccountItem>(
|
apiRequestDelete<WrappedCollectionAccountItem>(
|
||||||
`v1_alpha/collections/${collectionId}/items/${itemId}`,
|
`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
|
* Nested account item
|
||||||
*/
|
*/
|
||||||
interface CollectionAccountItem {
|
export interface CollectionAccountItem {
|
||||||
id: string;
|
id: string;
|
||||||
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
||||||
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
||||||
|
|||||||
@ -2,17 +2,23 @@ import { Fragment, useCallback, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Button } from '@/flavours/glitch/components/button';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
|
import type {
|
||||||
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
ApiCollectionJSON,
|
||||||
|
CollectionAccountItem,
|
||||||
|
} from 'flavours/glitch/api_types/collections';
|
||||||
import { Account } from 'flavours/glitch/components/account';
|
import { Account } from 'flavours/glitch/components/account';
|
||||||
|
import { Button } from 'flavours/glitch/components/button';
|
||||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||||
import {
|
import {
|
||||||
Article,
|
Article,
|
||||||
ItemList,
|
ItemList,
|
||||||
} from 'flavours/glitch/components/scrollable_list/components';
|
} from 'flavours/glitch/components/scrollable_list/components';
|
||||||
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
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 { me } from 'flavours/glitch/initial_state';
|
||||||
|
import { useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import classes from './styles.module.scss';
|
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<{
|
const SensitiveScreen: React.FC<{
|
||||||
sensitive: boolean | undefined;
|
sensitive: boolean | undefined;
|
||||||
focusTargetRef: React.RefObject<HTMLHeadingElement>;
|
focusTargetRef: React.RefObject<HTMLHeadingElement>;
|
||||||
@ -166,6 +218,10 @@ export const CollectionAccountsList: React.FC<{
|
|||||||
accountId={currentUserInCollection.account_id}
|
accountId={currentUserInCollection.account_id}
|
||||||
collectionOwnerId={collection.account_id}
|
collectionOwnerId={collection.account_id}
|
||||||
/>
|
/>
|
||||||
|
<RevokeControls
|
||||||
|
collectionId={collection.id}
|
||||||
|
collectionItem={currentUserInCollection}
|
||||||
|
/>
|
||||||
</Article>
|
</Article>
|
||||||
<h3
|
<h3
|
||||||
className={classes.columnSubheading}
|
className={classes.columnSubheading}
|
||||||
@ -33,6 +33,10 @@ const messages = defineMessages({
|
|||||||
id: 'collections.report_collection',
|
id: 'collections.report_collection',
|
||||||
defaultMessage: 'Report this collection',
|
defaultMessage: 'Report this collection',
|
||||||
},
|
},
|
||||||
|
revoke: {
|
||||||
|
id: 'collections.revoke_collection_inclusion',
|
||||||
|
defaultMessage: 'Remove myself from this collection',
|
||||||
|
},
|
||||||
more: { id: 'status.more', defaultMessage: 'More' },
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -71,6 +75,22 @@ export const CollectionMenu: React.FC<{
|
|||||||
);
|
);
|
||||||
}, [collection, dispatch]);
|
}, [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(() => {
|
const menu = useMemo(() => {
|
||||||
if (isOwnCollection) {
|
if (isOwnCollection) {
|
||||||
const commonItems: MenuItem[] = [
|
const commonItems: MenuItem[] = [
|
||||||
@ -99,34 +119,43 @@ export const CollectionMenu: React.FC<{
|
|||||||
} else {
|
} else {
|
||||||
return commonItems;
|
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 {
|
} 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,
|
isOwnCollection,
|
||||||
@ -134,6 +163,8 @@ export const CollectionMenu: React.FC<{
|
|||||||
id,
|
id,
|
||||||
openDeleteConfirmation,
|
openDeleteConfirmation,
|
||||||
context,
|
context,
|
||||||
|
currentAccountInCollection,
|
||||||
|
openRevokeConfirmation,
|
||||||
ownerAccount,
|
ownerAccount,
|
||||||
openReportModal,
|
openReportModal,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -24,7 +24,7 @@ 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 { CollectionAccountsList } from './accounts_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';
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { showAlert } from 'flavours/glitch/actions/alerts';
|
||||||
|
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 { revokeCollectionInclusion } from 'flavours/glitch/reducers/slices/collections';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/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;
|
line-height: 1.5;
|
||||||
cursor: default;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ interface ConfirmationModalProps {
|
|||||||
cancel?: React.ReactNode;
|
cancel?: React.ReactNode;
|
||||||
secondary?: React.ReactNode;
|
secondary?: React.ReactNode;
|
||||||
onSecondary?: () => void;
|
onSecondary?: () => void;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void | Promise<void>;
|
||||||
noCloseOnConfirm?: boolean;
|
noCloseOnConfirm?: boolean;
|
||||||
extraContent?: React.ReactNode;
|
extraContent?: React.ReactNode;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@ -64,7 +64,7 @@ export const ConfirmationModal: React.FC<
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfirm();
|
void onConfirm();
|
||||||
}, [onClose, onConfirm, noCloseOnConfirm]);
|
}, [onClose, onConfirm, noCloseOnConfirm]);
|
||||||
|
|
||||||
const handleSecondary = useCallback(() => {
|
const handleSecondary = useCallback(() => {
|
||||||
|
|||||||
@ -86,6 +86,7 @@ export const MODAL_COMPONENTS = {
|
|||||||
'REPORT': ReportModal,
|
'REPORT': ReportModal,
|
||||||
'REPORT_COLLECTION': ReportCollectionModal,
|
'REPORT_COLLECTION': ReportCollectionModal,
|
||||||
'SHARE_COLLECTION': () => import('@/flavours/glitch/features/collections/detail/share_modal').then(module => ({ default: module.CollectionShareModal })),
|
'SHARE_COLLECTION': () => import('@/flavours/glitch/features/collections/detail/share_modal').then(module => ({ default: module.CollectionShareModal })),
|
||||||
|
'REVOKE_COLLECTION_INCLUSION': () => import('@/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal').then(module => ({ default: module.RevokeCollectionInclusionModal })),
|
||||||
'SETTINGS': SettingsModal,
|
'SETTINGS': SettingsModal,
|
||||||
'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),
|
'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),
|
||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
apiDeleteCollection,
|
apiDeleteCollection,
|
||||||
apiAddCollectionItem,
|
apiAddCollectionItem,
|
||||||
apiRemoveCollectionItem,
|
apiRemoveCollectionItem,
|
||||||
|
apiRevokeCollectionInclusion,
|
||||||
} from '@/flavours/glitch/api/collections';
|
} from '@/flavours/glitch/api/collections';
|
||||||
import type {
|
import type {
|
||||||
ApiCollectionJSON,
|
ApiCollectionJSON,
|
||||||
@ -17,6 +18,7 @@ import type {
|
|||||||
} from '@/flavours/glitch/api_types/collections';
|
} from '@/flavours/glitch/api_types/collections';
|
||||||
import { me } from '@/flavours/glitch/initial_state';
|
import { me } from '@/flavours/glitch/initial_state';
|
||||||
import {
|
import {
|
||||||
|
createAppAsyncThunk,
|
||||||
createAppSelector,
|
createAppSelector,
|
||||||
createDataLoadingThunk,
|
createDataLoadingThunk,
|
||||||
} from '@/flavours/glitch/store/typed_functions';
|
} from '@/flavours/glitch/store/typed_functions';
|
||||||
@ -158,7 +160,10 @@ const collectionSlice = createSlice({
|
|||||||
* Removing an account from a collection
|
* 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 { itemId, collectionId } = action.meta.arg;
|
||||||
|
|
||||||
const collection = state.collections[collectionId];
|
const collection = state.collections[collectionId];
|
||||||
@ -167,7 +172,17 @@ const collectionSlice = createSlice({
|
|||||||
(item) => item.id !== itemId,
|
(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),
|
apiRemoveCollectionItem(collectionId, itemId),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const revokeCollectionInclusion = createAppAsyncThunk(
|
||||||
|
`${collectionSlice.name}/revokeCollectionInclusion`,
|
||||||
|
({ collectionId, itemId }: { collectionId: string; itemId: string }) =>
|
||||||
|
apiRevokeCollectionInclusion(collectionId, itemId),
|
||||||
|
);
|
||||||
|
|
||||||
export const collections = collectionSlice.reducer;
|
export const collections = collectionSlice.reducer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user