pl-fe: Allow maanaging interaction requests
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
adf1fe9b7d
commit
02a601a8e8
7 changed files with 314 additions and 25 deletions
|
@ -10,8 +10,8 @@ import type { AppDispatch } from 'pl-fe/store';
|
||||||
|
|
||||||
const minifyInteractionRequest = ({ account, status, reply, ...interactionRequest }: InteractionRequest) => ({
|
const minifyInteractionRequest = ({ account, status, reply, ...interactionRequest }: InteractionRequest) => ({
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
status: status?.id || null,
|
status_id: status?.id || null,
|
||||||
reply: reply?.id || null,
|
reply_id: reply?.id || null,
|
||||||
...interactionRequest,
|
...interactionRequest,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,13 +30,15 @@ const minifyInteractionRequestsList = (dispatch: AppDispatch, { previous, next,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const useInteractionRequests = <T>(select?: (data: InfiniteData<PaginatedResponse<MinifiedInteractionRequest>>) => T) => {
|
const useInteractionRequests = <T>(
|
||||||
|
select?: ((data: InfiniteData<PaginatedResponse<MinifiedInteractionRequest>>) => T),
|
||||||
|
) => {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: ['interaction_requests'],
|
queryKey: ['interactionRequests'],
|
||||||
queryFn: ({ pageParam }) => pageParam.next?.() || client.interactionRequests.getInteractionRequests().then(response => minifyInteractionRequestsList(dispatch, response)),
|
queryFn: ({ pageParam }) => pageParam.next?.() || client.interactionRequests.getInteractionRequests().then(response => minifyInteractionRequestsList(dispatch, response)),
|
||||||
initialPageParam: { previous: null, next: null, items: [], partial: false } as PaginatedResponse<MinifiedInteractionRequest>,
|
initialPageParam: { previous: null, next: null, items: [], partial: false } as PaginatedResponse<MinifiedInteractionRequest>,
|
||||||
getNextPageParam: (page) => page.next ? page : undefined,
|
getNextPageParam: (page) => page.next ? page : undefined,
|
||||||
|
@ -45,24 +47,30 @@ const useInteractionRequests = <T>(select?: (data: InfiniteData<PaginatedRespons
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useFlatInteractionRequests = () => useInteractionRequests(
|
||||||
|
(data: InfiniteData<PaginatedResponse<MinifiedInteractionRequest>>) => data.pages.map(page => page.items).flat(),
|
||||||
|
);
|
||||||
|
|
||||||
const useInteractionRequestsCount = () => useInteractionRequests(data => data.pages.map(({ items }) => items).flat().length);
|
const useInteractionRequestsCount = () => useInteractionRequests(data => data.pages.map(({ items }) => items).flat().length);
|
||||||
|
|
||||||
const useAuthorizeInteractionRequestMutation = () => {
|
const useAuthorizeInteractionRequestMutation = (requestId: string) => {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { refetch } = useInteractionRequests();
|
const { refetch } = useInteractionRequests();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (requestId: string) => client.interactionRequests.authorizeInteractionRequest(requestId),
|
mutationKey: ['interactionRequests', requestId],
|
||||||
|
mutationFn: () => client.interactionRequests.authorizeInteractionRequest(requestId),
|
||||||
onSettled: () => refetch(),
|
onSettled: () => refetch(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const useRejectInteractionRequestMutation = () => {
|
const useRejectInteractionRequestMutation = (requestId: string) => {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { refetch } = useInteractionRequests();
|
const { refetch } = useInteractionRequests();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (requestId: string) => client.interactionRequests.rejectInteractionRequest(requestId),
|
mutationKey: ['interactionRequests', requestId],
|
||||||
|
mutationFn: () => client.interactionRequests.rejectInteractionRequest(requestId),
|
||||||
onSettled: () => refetch(),
|
onSettled: () => refetch(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -70,6 +78,8 @@ const useRejectInteractionRequestMutation = () => {
|
||||||
export {
|
export {
|
||||||
useInteractionRequests,
|
useInteractionRequests,
|
||||||
useInteractionRequestsCount,
|
useInteractionRequestsCount,
|
||||||
|
useFlatInteractionRequests,
|
||||||
useAuthorizeInteractionRequestMutation,
|
useAuthorizeInteractionRequestMutation,
|
||||||
useRejectInteractionRequestMutation,
|
useRejectInteractionRequestMutation,
|
||||||
|
type MinifiedInteractionRequest,
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,11 +27,17 @@ interface IReadMoreButton {
|
||||||
onClick: React.MouseEventHandler;
|
onClick: React.MouseEventHandler;
|
||||||
quote?: boolean;
|
quote?: boolean;
|
||||||
poll?: boolean;
|
poll?: boolean;
|
||||||
|
preview?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Button to expand a truncated status (due to too much content) */
|
/** Button to expand a truncated status (due to too much content) */
|
||||||
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick, quote, poll }) => (
|
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick, quote, poll, preview }) => (
|
||||||
<div className='relative -mt-4'>
|
<div
|
||||||
|
className={clsx('relative', {
|
||||||
|
'-mt-4': !preview,
|
||||||
|
'-mt-2': preview,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx('absolute -top-16 h-16 w-full bg-gradient-to-b from-transparent', {
|
className={clsx('absolute -top-16 h-16 w-full bg-gradient-to-b from-transparent', {
|
||||||
'to-white black:to-black dark:to-primary-900': !poll,
|
'to-white black:to-black dark:to-primary-900': !poll,
|
||||||
|
@ -39,10 +45,12 @@ const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick, quote, poll }) =>
|
||||||
'group-hover:to-gray-100 black:group-hover:to-gray-800 dark:group-hover:to-gray-800': quote,
|
'group-hover:to-gray-100 black:group-hover:to-gray-800 dark:group-hover:to-gray-800': quote,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<button className='flex items-center border-0 bg-transparent p-0 pt-2 text-gray-900 hover:underline active:underline dark:text-gray-300' onClick={onClick}>
|
{!preview && (
|
||||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
|
<button className='flex items-center border-0 bg-transparent p-0 pt-2 text-gray-900 hover:underline active:underline dark:text-gray-300' onClick={onClick}>
|
||||||
<Icon className='inline-block size-5' src={require('@tabler/icons/outline/chevron-right.svg')} />
|
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
|
||||||
</button>
|
<Icon className='inline-block size-5' src={require('@tabler/icons/outline/chevron-right.svg')} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -53,6 +61,7 @@ interface IStatusContent {
|
||||||
translatable?: boolean;
|
translatable?: boolean;
|
||||||
textSize?: Sizes;
|
textSize?: Sizes;
|
||||||
quote?: boolean;
|
quote?: boolean;
|
||||||
|
preview?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Renders the text content of a status */
|
/** Renders the text content of a status */
|
||||||
|
@ -63,6 +72,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
||||||
translatable,
|
translatable,
|
||||||
textSize = 'md',
|
textSize = 'md',
|
||||||
quote = false,
|
quote = false,
|
||||||
|
preview,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { displaySpoilers } = useSettings();
|
const { displaySpoilers } = useSettings();
|
||||||
|
@ -77,9 +87,9 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
||||||
const maybeSetCollapsed = (): void => {
|
const maybeSetCollapsed = (): void => {
|
||||||
if (!node.current) return;
|
if (!node.current) return;
|
||||||
|
|
||||||
if (collapsable && !collapsed) {
|
if ((collapsable || preview) && !collapsed) {
|
||||||
// 20px * x lines (+ 2px padding at the top)
|
// 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);
|
setCollapsed(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,8 +140,9 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
||||||
const className = clsx('relative text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', {
|
const className = clsx('relative text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', {
|
||||||
'cursor-pointer': onClick,
|
'cursor-pointer': onClick,
|
||||||
'overflow-hidden': collapsed,
|
'overflow-hidden': collapsed,
|
||||||
'max-h-[200px]': collapsed && !quote,
|
'max-h-[200px]': collapsed && !quote && !preview,
|
||||||
'max-h-[120px]': collapsed && quote,
|
'max-h-[120px]': collapsed && quote,
|
||||||
|
'max-h-[80px]': collapsed && preview,
|
||||||
'leading-normal big-emoji': onlyEmoji,
|
'leading-normal big-emoji': onlyEmoji,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -212,7 +223,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
output.push(<ReadMoreButton onClick={() => {}} key='read-more' quote={quote} />);
|
output.push(<ReadMoreButton onClick={() => {}} key='read-more' quote={quote} preview={preview} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.poll_id) {
|
if (status.poll_id) {
|
||||||
|
|
|
@ -1,11 +1,268 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
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 <link>post</link>' },
|
||||||
|
reply: { id: 'interaction_request.reply', defaultMessage: '{name} wants to reply to your <link>post</link>' },
|
||||||
|
reblog: { id: 'interaction_request.reblog', defaultMessage: '{name} wants to repost your <link>post</link>' },
|
||||||
|
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<IInteractionRequestStatus> = ({ id: statusId, hasReply, isReply, actions }) => {
|
||||||
|
const status = useAppSelector((state) => state.statuses.get(statusId));
|
||||||
|
|
||||||
|
if (!status) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className='relative py-2' space={2}>
|
||||||
|
{hasReply && (
|
||||||
|
<div
|
||||||
|
className='absolute left-5 top-[62px] z-[1] block h-[calc(100%-58px)] w-0.5 bg-gray-200 black:bg-gray-800 dark:bg-primary-800 rtl:left-auto rtl:right-5'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AccountContainer
|
||||||
|
id={status.account_id}
|
||||||
|
showAccountHoverCard={false}
|
||||||
|
withLinkToProfile={false}
|
||||||
|
timestamp={status.created_at}
|
||||||
|
action={actions || <></>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack space={2} className={clsx(hasReply && 'pl-[54px]')}>
|
||||||
|
<StatusContent status={status} preview={!isReply} />
|
||||||
|
|
||||||
|
{status.media_attachments.length > 0 && (
|
||||||
|
<AttachmentThumbs status={status} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IInteractionRequest {
|
||||||
|
interactionRequest: MinifiedInteractionRequest;
|
||||||
|
onMoveUp?: (requestId: string) => void;
|
||||||
|
onMoveDown?: (requestId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InteractionRequest: React.FC<IInteractionRequest> = ({
|
||||||
|
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 (
|
||||||
|
<Link className='font-bold text-gray-800 hover:underline dark:text-gray-200' to={`/@${ownAccount?.acct}/posts/${interactionRequest.status_id}`}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = (
|
||||||
|
<HStack space={2}>
|
||||||
|
<Button
|
||||||
|
theme='secondary'
|
||||||
|
size='sm'
|
||||||
|
text={intl.formatMessage(messages.authorize)}
|
||||||
|
onClick={() => handleAuthorize()}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
theme='danger'
|
||||||
|
size='sm'
|
||||||
|
text={intl.formatMessage(messages.reject)}
|
||||||
|
onClick={() => handleReject()}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMoveUp = () => {
|
||||||
|
if (onMoveUp) {
|
||||||
|
onMoveUp(interactionRequest.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveDown = () => {
|
||||||
|
if (onMoveDown) {
|
||||||
|
onMoveDown(interactionRequest.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
moveUp: handleMoveUp,
|
||||||
|
moveDown: handleMoveDown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={handlers}>
|
||||||
|
<div className='notification focusable' tabIndex={0}>
|
||||||
|
<div className='focusable p-4'>
|
||||||
|
<Stack space={2}>
|
||||||
|
<div>
|
||||||
|
<HStack alignItems='center' space={3}>
|
||||||
|
<div
|
||||||
|
className='flex justify-end'
|
||||||
|
style={{ flexBasis: avatarSize }}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
src={icons[interactionRequest.type]}
|
||||||
|
className='flex-none text-primary-600 dark:text-primary-400'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='truncate'>
|
||||||
|
<Text theme='muted' size='xs' truncate>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{interactionRequest.type !== 'reply' && (
|
||||||
|
<div className='ml-auto'>
|
||||||
|
<Text theme='muted' size='xs' truncate>
|
||||||
|
<RelativeTimestamp timestamp={interactionRequest.created_at} theme='muted' size='sm' className='whitespace-nowrap' />
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{interactionRequest.status_id && <InteractionRequestStatus id={interactionRequest.status_id} hasReply={interactionRequest.type === 'reply'} actions={actions} />}
|
||||||
|
{interactionRequest.reply_id && <InteractionRequestStatus id={interactionRequest.reply_id} isReply />}
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const InteractionRequests = () => {
|
const InteractionRequests = () => {
|
||||||
const interactionRequestsQuery = useInteractionRequests();
|
const intl = useIntl();
|
||||||
|
|
||||||
return <>{JSON.stringify(interactionRequestsQuery.data)}</>;
|
const { data = [], fetchNextPage, hasNextPage, isFetching, isLoading, refetch } = useFlatInteractionRequests();
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.interaction_requests' defaultMessage='There are no pending interaction requests.' />;
|
||||||
|
|
||||||
|
const handleMoveUp = (id: string) => {
|
||||||
|
const elementIndex = data.findIndex(item => item !== null && item.id === id) - 1;
|
||||||
|
_selectChild(elementIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveDown = (id: string) => {
|
||||||
|
const elementIndex = data.findIndex(item => item !== null && item.id === id) + 1;
|
||||||
|
_selectChild(elementIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _selectChild = (index: number) => {
|
||||||
|
const selector = `[data-index="${index}"] .focusable`;
|
||||||
|
const element = document.querySelector<HTMLDivElement>(selector);
|
||||||
|
|
||||||
|
if (element) element.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column label={intl.formatMessage(messages.title)} withHeader={false}>
|
||||||
|
<PullToRefresh onRefresh={() => refetch()}>
|
||||||
|
<ScrollableList
|
||||||
|
isLoading={isFetching}
|
||||||
|
showLoading={isLoading}
|
||||||
|
hasMore={hasNextPage}
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
onLoadMore={() => fetchNextPage()}
|
||||||
|
listClassName={clsx('divide-y divide-solid divide-gray-200 black:divide-gray-800 dark:divide-primary-800', {
|
||||||
|
'animate-pulse': data?.length === 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{data?.map(request => (
|
||||||
|
<InteractionRequest
|
||||||
|
key={request.id}
|
||||||
|
interactionRequest={request}
|
||||||
|
onMoveUp={handleMoveUp}
|
||||||
|
onMoveDown={handleMoveDown}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
</PullToRefresh>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { InteractionRequests as default };
|
export { InteractionRequests as default };
|
||||||
|
|
|
@ -449,4 +449,4 @@ const Notification: React.FC<INotification> = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Notification as default, getNotificationStatus };
|
export { Notification as default, buildLink, getNotificationStatus };
|
||||||
|
|
|
@ -47,7 +47,6 @@ const Notifications = () => {
|
||||||
const hasMore = useAppSelector(state => state.notifications.hasMore);
|
const hasMore = useAppSelector(state => state.notifications.hasMore);
|
||||||
const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0);
|
const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0);
|
||||||
|
|
||||||
const column = useRef<HTMLDivElement>(null);
|
|
||||||
const scrollableContentRef = useRef<ImmutableList<JSX.Element> | null>(null);
|
const scrollableContentRef = useRef<ImmutableList<JSX.Element> | null>(null);
|
||||||
|
|
||||||
// const handleLoadGap = (maxId) => {
|
// const handleLoadGap = (maxId) => {
|
||||||
|
@ -143,7 +142,7 @@ const Notifications = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column ref={column} label={intl.formatMessage(messages.title)} withHeader={false}>
|
<Column label={intl.formatMessage(messages.title)} withHeader={false}>
|
||||||
{filterBarContainer}
|
{filterBarContainer}
|
||||||
|
|
||||||
<Portal>
|
<Portal>
|
||||||
|
|
|
@ -53,7 +53,7 @@ const SelectedStatus = ({ statusId }: { statusId: string }) => {
|
||||||
return (
|
return (
|
||||||
<Stack space={2} className='rounded-lg bg-gray-100 p-4 dark:bg-gray-800'>
|
<Stack space={2} className='rounded-lg bg-gray-100 p-4 dark:bg-gray-800'>
|
||||||
<AccountContainer
|
<AccountContainer
|
||||||
id={status.account?.id}
|
id={status.account_id}
|
||||||
showAccountHoverCard={false}
|
showAccountHoverCard={false}
|
||||||
withLinkToProfile={false}
|
withLinkToProfile={false}
|
||||||
timestamp={status.created_at}
|
timestamp={status.created_at}
|
||||||
|
|
|
@ -391,6 +391,7 @@
|
||||||
"column.import_data": "Import data",
|
"column.import_data": "Import data",
|
||||||
"column.info": "Server information",
|
"column.info": "Server information",
|
||||||
"column.interaction_policies": "Interaction policies",
|
"column.interaction_policies": "Interaction policies",
|
||||||
|
"column.interaction_requests": "Interaction requests",
|
||||||
"column.lists": "Lists",
|
"column.lists": "Lists",
|
||||||
"column.manage_group": "Manage group",
|
"column.manage_group": "Manage group",
|
||||||
"column.mentions": "Mentions",
|
"column.mentions": "Mentions",
|
||||||
|
@ -725,6 +726,7 @@
|
||||||
"empty_column.home.local_tab": "the {site_title} tab",
|
"empty_column.home.local_tab": "the {site_title} tab",
|
||||||
"empty_column.home.subtitle": "{siteTitle} gets more interesting once you follow other users.",
|
"empty_column.home.subtitle": "{siteTitle} gets more interesting once you follow other users.",
|
||||||
"empty_column.home.title": "You're not following anyone yet",
|
"empty_column.home.title": "You're not following anyone yet",
|
||||||
|
"empty_column.interaction_requests": "There are no pending interaction requests.",
|
||||||
"empty_column.list": "There is nothing in this list yet. When members of this list create new posts, they will appear here.",
|
"empty_column.list": "There is nothing in this list yet. When members of this list create new posts, they will appear here.",
|
||||||
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
||||||
"empty_column.mutes": "You haven't muted any users yet.",
|
"empty_column.mutes": "You haven't muted any users yet.",
|
||||||
|
@ -916,6 +918,15 @@
|
||||||
"interaction_policies.title.unlisted.can_reblog": "Who can repost an unlisted post?",
|
"interaction_policies.title.unlisted.can_reblog": "Who can repost an unlisted post?",
|
||||||
"interaction_policies.title.unlisted.can_reply": "Who can reply to an unlisted post?",
|
"interaction_policies.title.unlisted.can_reply": "Who can reply to an unlisted post?",
|
||||||
"interaction_policies.update": "Update",
|
"interaction_policies.update": "Update",
|
||||||
|
"interaction_request.authorize": "Accept",
|
||||||
|
"interaction_request.authorize.fail": "Failed to authorize @{acct} interaction request",
|
||||||
|
"interaction_request.authorize.success": "Authorized @{acct} interaction request",
|
||||||
|
"interaction_request.favourite": "{name} wants to like your <link>post</link>",
|
||||||
|
"interaction_request.reblog": "{name} wants to repost your <link>post</link>",
|
||||||
|
"interaction_request.reject": "Reject",
|
||||||
|
"interaction_request.reject.fail": "Failed to reject @{acct} interaction request",
|
||||||
|
"interaction_request.reject.success": "Rejected @{acct} interaction request",
|
||||||
|
"interaction_request.reply": "{name} wants to reply to your <link>post</link>",
|
||||||
"interactions_circle.compose": "Share",
|
"interactions_circle.compose": "Share",
|
||||||
"interactions_circle.confirmation_heading": "Do you want to generate an interaction circle for the user @{username}?",
|
"interactions_circle.confirmation_heading": "Do you want to generate an interaction circle for the user @{username}?",
|
||||||
"interactions_circle.download": "Download",
|
"interactions_circle.download": "Download",
|
||||||
|
@ -1068,6 +1079,7 @@
|
||||||
"navigation.direct_messages": "Direct messages",
|
"navigation.direct_messages": "Direct messages",
|
||||||
"navigation.drafts": "Drafts",
|
"navigation.drafts": "Drafts",
|
||||||
"navigation.home": "Home",
|
"navigation.home": "Home",
|
||||||
|
"navigation.interaction_requests": "Interaction requests",
|
||||||
"navigation.notifications": "Notifications",
|
"navigation.notifications": "Notifications",
|
||||||
"navigation.search": "Search",
|
"navigation.search": "Search",
|
||||||
"navigation.sidebar": "Open sidebar",
|
"navigation.sidebar": "Open sidebar",
|
||||||
|
|
Loading…
Reference in a new issue