From 02a601a8e8348a81ef845e9701747937db11af49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 31 Oct 2024 20:48:30 +0100 Subject: [PATCH] pl-fe: Allow maanaging interaction requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../statuses/use-interaction-requests.ts | 26 +- .../pl-fe/src/components/status-content.tsx | 31 ++- .../features/interaction-requests/index.tsx | 263 +++++++++++++++++- .../notifications/components/notification.tsx | 2 +- .../src/features/notifications/index.tsx | 3 +- .../components/modals/report-modal/index.tsx | 2 +- packages/pl-fe/src/locales/en.json | 12 + 7 files changed, 314 insertions(+), 25 deletions(-) diff --git a/packages/pl-fe/src/api/hooks/statuses/use-interaction-requests.ts b/packages/pl-fe/src/api/hooks/statuses/use-interaction-requests.ts index 379b7ea54..b28aa065b 100644 --- a/packages/pl-fe/src/api/hooks/statuses/use-interaction-requests.ts +++ b/packages/pl-fe/src/api/hooks/statuses/use-interaction-requests.ts @@ -10,8 +10,8 @@ import type { AppDispatch } from 'pl-fe/store'; const minifyInteractionRequest = ({ account, status, reply, ...interactionRequest }: InteractionRequest) => ({ account_id: account.id, - status: status?.id || null, - reply: reply?.id || null, + status_id: status?.id || null, + reply_id: reply?.id || null, ...interactionRequest, }); @@ -30,13 +30,15 @@ const minifyInteractionRequestsList = (dispatch: AppDispatch, { previous, next, }; }; -const useInteractionRequests = (select?: (data: InfiniteData>) => T) => { +const useInteractionRequests = ( + select?: ((data: InfiniteData>) => T), +) => { const client = useClient(); const features = useFeatures(); const dispatch = useAppDispatch(); return useInfiniteQuery({ - queryKey: ['interaction_requests'], + queryKey: ['interactionRequests'], queryFn: ({ pageParam }) => pageParam.next?.() || client.interactionRequests.getInteractionRequests().then(response => minifyInteractionRequestsList(dispatch, response)), initialPageParam: { previous: null, next: null, items: [], partial: false } as PaginatedResponse, getNextPageParam: (page) => page.next ? page : undefined, @@ -45,24 +47,30 @@ const useInteractionRequests = (select?: (data: InfiniteData useInteractionRequests( + (data: InfiniteData>) => data.pages.map(page => page.items).flat(), +); + const useInteractionRequestsCount = () => useInteractionRequests(data => data.pages.map(({ items }) => items).flat().length); -const useAuthorizeInteractionRequestMutation = () => { +const useAuthorizeInteractionRequestMutation = (requestId: string) => { const client = useClient(); const { refetch } = useInteractionRequests(); return useMutation({ - mutationFn: (requestId: string) => client.interactionRequests.authorizeInteractionRequest(requestId), + mutationKey: ['interactionRequests', requestId], + mutationFn: () => client.interactionRequests.authorizeInteractionRequest(requestId), onSettled: () => refetch(), }); }; -const useRejectInteractionRequestMutation = () => { +const useRejectInteractionRequestMutation = (requestId: string) => { const client = useClient(); const { refetch } = useInteractionRequests(); return useMutation({ - mutationFn: (requestId: string) => client.interactionRequests.rejectInteractionRequest(requestId), + mutationKey: ['interactionRequests', requestId], + mutationFn: () => client.interactionRequests.rejectInteractionRequest(requestId), onSettled: () => refetch(), }); }; @@ -70,6 +78,8 @@ const useRejectInteractionRequestMutation = () => { export { useInteractionRequests, useInteractionRequestsCount, + useFlatInteractionRequests, useAuthorizeInteractionRequestMutation, useRejectInteractionRequestMutation, + type MinifiedInteractionRequest, }; diff --git a/packages/pl-fe/src/components/status-content.tsx b/packages/pl-fe/src/components/status-content.tsx index c1e0795bc..8d8f22684 100644 --- a/packages/pl-fe/src/components/status-content.tsx +++ b/packages/pl-fe/src/components/status-content.tsx @@ -27,11 +27,17 @@ interface IReadMoreButton { onClick: React.MouseEventHandler; quote?: boolean; poll?: boolean; + preview?: boolean; } /** Button to expand a truncated status (due to too much content) */ -const ReadMoreButton: React.FC = ({ onClick, quote, poll }) => ( -
+const ReadMoreButton: React.FC = ({ onClick, quote, poll, preview }) => ( +
= ({ onClick, quote, poll }) => 'group-hover:to-gray-100 black:group-hover:to-gray-800 dark:group-hover:to-gray-800': quote, })} /> - + {!preview && ( + + )}
); @@ -53,6 +61,7 @@ interface IStatusContent { translatable?: boolean; textSize?: Sizes; quote?: boolean; + preview?: boolean; } /** Renders the text content of a status */ @@ -63,6 +72,7 @@ const StatusContent: React.FC = React.memo(({ translatable, textSize = 'md', quote = false, + preview, }) => { const dispatch = useAppDispatch(); const { displaySpoilers } = useSettings(); @@ -77,9 +87,9 @@ const StatusContent: React.FC = React.memo(({ const maybeSetCollapsed = (): void => { if (!node.current) return; - if (collapsable && !collapsed) { + if ((collapsable || preview) && !collapsed) { // 20px * x lines (+ 2px padding at the top) - if (node.current.clientHeight > (quote ? 202 : 282)) { + if (node.current.clientHeight > (preview ? 82 : quote ? 202 : 282)) { setCollapsed(true); } } @@ -130,8 +140,9 @@ const StatusContent: React.FC = React.memo(({ const className = clsx('relative text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', { 'cursor-pointer': onClick, 'overflow-hidden': collapsed, - 'max-h-[200px]': collapsed && !quote, + 'max-h-[200px]': collapsed && !quote && !preview, 'max-h-[120px]': collapsed && quote, + 'max-h-[80px]': collapsed && preview, 'leading-normal big-emoji': onlyEmoji, }); @@ -212,7 +223,7 @@ const StatusContent: React.FC = React.memo(({ } if (collapsed) { - output.push( {}} key='read-more' quote={quote} />); + output.push( {}} key='read-more' quote={quote} preview={preview} />); } if (status.poll_id) { diff --git a/packages/pl-fe/src/features/interaction-requests/index.tsx b/packages/pl-fe/src/features/interaction-requests/index.tsx index 97250fed8..fb651ac8b 100644 --- a/packages/pl-fe/src/features/interaction-requests/index.tsx +++ b/packages/pl-fe/src/features/interaction-requests/index.tsx @@ -1,11 +1,268 @@ +import clsx from 'clsx'; import React from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; -import { useInteractionRequests } from 'pl-fe/api/hooks/statuses/use-interaction-requests'; +import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; +import { type MinifiedInteractionRequest, useAuthorizeInteractionRequestMutation, useFlatInteractionRequests, useRejectInteractionRequestMutation } from 'pl-fe/api/hooks/statuses/use-interaction-requests'; +import AttachmentThumbs from 'pl-fe/components/attachment-thumbs'; +import Icon from 'pl-fe/components/icon'; +import PullToRefresh from 'pl-fe/components/pull-to-refresh'; +import RelativeTimestamp from 'pl-fe/components/relative-timestamp'; +import ScrollableList from 'pl-fe/components/scrollable-list'; +import StatusContent from 'pl-fe/components/status-content'; +import Button from 'pl-fe/components/ui/button'; +import Column from 'pl-fe/components/ui/column'; +import HStack from 'pl-fe/components/ui/hstack'; +import Stack from 'pl-fe/components/ui/stack'; +import Text from 'pl-fe/components/ui/text'; +import AccountContainer from 'pl-fe/containers/account-container'; +import { buildLink } from 'pl-fe/features/notifications/components/notification'; +import { HotKeys } from 'pl-fe/features/ui/components/hotkeys'; +import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useOwnAccount } from 'pl-fe/hooks/use-own-account'; +import toast from 'pl-fe/toast'; + +const messages = defineMessages({ + title: { id: 'column.interaction_requests', defaultMessage: 'Interaction requests' }, + favourite: { id: 'interaction_request.favourite', defaultMessage: '{name} wants to like your post' }, + reply: { id: 'interaction_request.reply', defaultMessage: '{name} wants to reply to your post' }, + reblog: { id: 'interaction_request.reblog', defaultMessage: '{name} wants to repost your post' }, + authorize: { id: 'interaction_request.authorize', defaultMessage: 'Accept' }, + reject: { id: 'interaction_request.reject', defaultMessage: 'Reject' }, + authorized: { id: 'interaction_request.authorize.success', defaultMessage: 'Authorized @{acct} interaction request' }, + authorizeFail: { id: 'interaction_request.authorize.fail', defaultMessage: 'Failed to authorize @{acct} interaction request' }, + rejected: { id: 'interaction_request.reject.success', defaultMessage: 'Rejected @{acct} interaction request' }, + rejectFail: { id: 'interaction_request.reject.fail', defaultMessage: 'Failed to reject @{acct} interaction request' }, +}); + +const icons = { + favourite: require('@tabler/icons/outline/heart.svg'), + reblog: require('@tabler/icons/outline/repeat.svg'), + reply: require('@tabler/icons/outline/corner-up-left.svg'), +}; + +const avatarSize = 42; + +interface IInteractionRequestStatus { + id: string; + hasReply?: boolean; + isReply?: boolean; + actions?: JSX.Element; +} + +const InteractionRequestStatus: React.FC = ({ id: statusId, hasReply, isReply, actions }) => { + const status = useAppSelector((state) => state.statuses.get(statusId)); + + if (!status) return null; + + return ( + + {hasReply && ( +
+ )} + + } + /> + + + + + {status.media_attachments.length > 0 && ( + + )} + + + ); +}; + +interface IInteractionRequest { + interactionRequest: MinifiedInteractionRequest; + onMoveUp?: (requestId: string) => void; + onMoveDown?: (requestId: string) => void; +} + +const InteractionRequest: React.FC = ({ + interactionRequest, onMoveUp, onMoveDown, +}) => { + const intl = useIntl(); + const { account: ownAccount } = useOwnAccount(); + const { account } = useAccount(interactionRequest.account_id); + + const { mutate: authorize } = useAuthorizeInteractionRequestMutation(interactionRequest.id); + const { mutate: reject } = useRejectInteractionRequestMutation(interactionRequest.id); + + const handleAuthorize = () => { + authorize(undefined, { + onSuccess: () => toast.success(intl.formatMessage(messages.authorized, { + acct: account?.acct, + })), + onError: () => toast.error(intl.formatMessage(messages.authorizeFail, { + acct: account?.acct, + })), + }); + }; + + const handleReject = () => { + reject(undefined, { + onSuccess: () => toast.success(intl.formatMessage(messages.rejected, { + acct: account?.acct, + })), + onError: () => toast.error(intl.formatMessage(messages.rejectFail, { + acct: account?.acct, + })), + }); + }; + + if (interactionRequest.accepted_at || interactionRequest.rejected_at) return null; + + const message = intl.formatMessage(messages[interactionRequest.type], { + name: account && buildLink(account), + link: (children: React.ReactNode) => { + if (interactionRequest.status_id) { + return ( + + {children} + + ); + } + + return children; + }, + }); + + const actions = ( + +