Merge pull request #139 from mkljczk/interaction-requests
pl-fe: Display interaction requests
This commit is contained in:
commit
a83616e9e0
11 changed files with 416 additions and 14 deletions
|
@ -0,0 +1,85 @@
|
||||||
|
import { type InfiniteData, useInfiniteQuery, useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { importEntities } from 'pl-fe/actions/importer';
|
||||||
|
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||||
|
import { useClient } from 'pl-fe/hooks/use-client';
|
||||||
|
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||||
|
|
||||||
|
import type { InteractionRequest, PaginatedResponse } from 'pl-api';
|
||||||
|
import type { AppDispatch } from 'pl-fe/store';
|
||||||
|
|
||||||
|
const minifyInteractionRequest = ({ account, status, reply, ...interactionRequest }: InteractionRequest) => ({
|
||||||
|
account_id: account.id,
|
||||||
|
status_id: status?.id || null,
|
||||||
|
reply_id: reply?.id || null,
|
||||||
|
...interactionRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
type MinifiedInteractionRequest = ReturnType<typeof minifyInteractionRequest>;
|
||||||
|
|
||||||
|
const minifyInteractionRequestsList = (dispatch: AppDispatch, { previous, next, items, ...response }: PaginatedResponse<InteractionRequest>): PaginatedResponse<MinifiedInteractionRequest> => {
|
||||||
|
dispatch(importEntities({
|
||||||
|
statuses: items.map(item => [item.status, item.reply]).flat(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
previous: previous ? () => previous().then(response => minifyInteractionRequestsList(dispatch, response)) : null,
|
||||||
|
next: next ? () => next().then(response => minifyInteractionRequestsList(dispatch, response)) : null,
|
||||||
|
items: items.map(minifyInteractionRequest),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useInteractionRequests = <T>(
|
||||||
|
select?: ((data: InfiniteData<PaginatedResponse<MinifiedInteractionRequest>>) => T),
|
||||||
|
) => {
|
||||||
|
const client = useClient();
|
||||||
|
const features = useFeatures();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: ['interactionRequests'],
|
||||||
|
queryFn: ({ pageParam }) => pageParam.next?.() || client.interactionRequests.getInteractionRequests().then(response => minifyInteractionRequestsList(dispatch, response)),
|
||||||
|
initialPageParam: { previous: null, next: null, items: [], partial: false } as PaginatedResponse<MinifiedInteractionRequest>,
|
||||||
|
getNextPageParam: (page) => page.next ? page : undefined,
|
||||||
|
enabled: features.interactionRequests,
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 useAuthorizeInteractionRequestMutation = (requestId: string) => {
|
||||||
|
const client = useClient();
|
||||||
|
const { refetch } = useInteractionRequests();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ['interactionRequests', requestId],
|
||||||
|
mutationFn: () => client.interactionRequests.authorizeInteractionRequest(requestId),
|
||||||
|
onSettled: () => refetch(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRejectInteractionRequestMutation = (requestId: string) => {
|
||||||
|
const client = useClient();
|
||||||
|
const { refetch } = useInteractionRequests();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ['interactionRequests', requestId],
|
||||||
|
mutationFn: () => client.interactionRequests.rejectInteractionRequest(requestId),
|
||||||
|
onSettled: () => refetch(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
useInteractionRequests,
|
||||||
|
useInteractionRequestsCount,
|
||||||
|
useFlatInteractionRequests,
|
||||||
|
useAuthorizeInteractionRequestMutation,
|
||||||
|
useRejectInteractionRequestMutation,
|
||||||
|
type MinifiedInteractionRequest,
|
||||||
|
};
|
|
@ -6,6 +6,7 @@ import { Link, NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { fetchOwnAccounts, logOut, switchAccount } from 'pl-fe/actions/auth';
|
import { fetchOwnAccounts, logOut, switchAccount } from 'pl-fe/actions/auth';
|
||||||
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
||||||
|
import { useInteractionRequestsCount } from 'pl-fe/api/hooks/statuses/use-interaction-requests';
|
||||||
import Account from 'pl-fe/components/account';
|
import Account from 'pl-fe/components/account';
|
||||||
import Divider from 'pl-fe/components/ui/divider';
|
import Divider from 'pl-fe/components/ui/divider';
|
||||||
import HStack from 'pl-fe/components/ui/hstack';
|
import HStack from 'pl-fe/components/ui/hstack';
|
||||||
|
@ -42,6 +43,7 @@ const messages = defineMessages({
|
||||||
drafts: { id: 'navigation.drafts', defaultMessage: 'Drafts' },
|
drafts: { id: 'navigation.drafts', defaultMessage: 'Drafts' },
|
||||||
addAccount: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
addAccount: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
||||||
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
|
interactionRequests: { id: 'navigation.interaction_requests', defaultMessage: 'Interaction requests' },
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
login: { id: 'account.login', defaultMessage: 'Log in' },
|
login: { id: 'account.login', defaultMessage: 'Log in' },
|
||||||
register: { id: 'account.register', defaultMessage: 'Sign up' },
|
register: { id: 'account.register', defaultMessage: 'Sign up' },
|
||||||
|
@ -96,6 +98,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state));
|
const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state));
|
||||||
const { settings } = useSettingsStore();
|
const { settings } = useSettingsStore();
|
||||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||||
|
const interactionRequestsCount = useInteractionRequestsCount().data || 0;
|
||||||
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
||||||
const draftCount = useAppSelector((state) => state.draft_statuses.size);
|
const draftCount = useAppSelector((state) => state.draft_statuses.size);
|
||||||
// const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
// const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
||||||
|
@ -232,6 +235,15 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(interactionRequestsCount > 0) && (
|
||||||
|
<SidebarLink
|
||||||
|
to='/interaction_requests'
|
||||||
|
icon={require('@tabler/icons/outline/flag-question.svg')}
|
||||||
|
text={intl.formatMessage(messages.interactionRequests)}
|
||||||
|
onClick={closeSidebar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{features.conversations && (
|
{features.conversations && (
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
to='/conversations'
|
to='/conversations'
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { useInteractionRequestsCount } from 'pl-fe/api/hooks/statuses/use-interaction-requests';
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import { useStatContext } from 'pl-fe/contexts/stat-context';
|
import { useStatContext } from 'pl-fe/contexts/stat-context';
|
||||||
|
@ -31,6 +32,7 @@ const messages = defineMessages({
|
||||||
scheduledStatuses: { id: 'column.scheduled_statuses', defaultMessage: 'Scheduled posts' },
|
scheduledStatuses: { id: 'column.scheduled_statuses', defaultMessage: 'Scheduled posts' },
|
||||||
drafts: { id: 'navigation.drafts', defaultMessage: 'Drafts' },
|
drafts: { id: 'navigation.drafts', defaultMessage: 'Drafts' },
|
||||||
conversations: { id: 'navigation.direct_messages', defaultMessage: 'Direct messages' },
|
conversations: { id: 'navigation.direct_messages', defaultMessage: 'Direct messages' },
|
||||||
|
interactionRequests: { id: 'navigation.interaction_requests', defaultMessage: 'Interaction requests' },
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Desktop sidebar with links to different views in the app. */
|
/** Desktop sidebar with links to different views in the app. */
|
||||||
|
@ -47,6 +49,7 @@ const SidebarNavigation = () => {
|
||||||
|
|
||||||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||||
|
const interactionRequestsCount = useInteractionRequestsCount().data || 0;
|
||||||
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
||||||
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
||||||
const draftCount = useAppSelector((state) => state.draft_statuses.size);
|
const draftCount = useAppSelector((state) => state.draft_statuses.size);
|
||||||
|
@ -74,6 +77,15 @@ const SidebarNavigation = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (interactionRequestsCount > 0) {
|
||||||
|
menu.push({
|
||||||
|
to: '/interaction_requests',
|
||||||
|
text: intl.formatMessage(messages.interactionRequests),
|
||||||
|
icon: require('@tabler/icons/outline/flag-question.svg'),
|
||||||
|
count: interactionRequestsCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (features.bookmarks) {
|
if (features.bookmarks) {
|
||||||
menu.push({
|
menu.push({
|
||||||
to: '/bookmarks',
|
to: '/bookmarks',
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
268
packages/pl-fe/src/features/interaction-requests/index.tsx
Normal file
268
packages/pl-fe/src/features/interaction-requests/index.tsx
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
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 intl = useIntl();
|
||||||
|
|
||||||
|
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 };
|
|
@ -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}
|
||||||
|
|
|
@ -138,6 +138,7 @@ import {
|
||||||
Circle,
|
Circle,
|
||||||
BubbleTimeline,
|
BubbleTimeline,
|
||||||
InteractionPolicies,
|
InteractionPolicies,
|
||||||
|
InteractionRequests,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import GlobalHotkeys from './util/global-hotkeys';
|
import GlobalHotkeys from './util/global-hotkeys';
|
||||||
import { WrappedRoute } from './util/react-router-helpers';
|
import { WrappedRoute } from './util/react-router-helpers';
|
||||||
|
@ -255,6 +256,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
||||||
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters/:id' layout={DefaultLayout} component={EditFilter} content={children} />}
|
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters/:id' layout={DefaultLayout} component={EditFilter} content={children} />}
|
||||||
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters' layout={DefaultLayout} component={Filters} content={children} />}
|
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters' layout={DefaultLayout} component={Filters} content={children} />}
|
||||||
{(features.followedHashtagsList) && <WrappedRoute path='/followed_tags' layout={DefaultLayout} component={FollowedTags} content={children} />}
|
{(features.followedHashtagsList) && <WrappedRoute path='/followed_tags' layout={DefaultLayout} component={FollowedTags} content={children} />}
|
||||||
|
{features.interactionRequests && <WrappedRoute path='/interaction_requests' layout={DefaultLayout} component={InteractionRequests} content={children} />}
|
||||||
<WrappedRoute path='/@:username' publicRoute exact layout={ProfileLayout} component={AccountTimeline} content={children} />
|
<WrappedRoute path='/@:username' publicRoute exact layout={ProfileLayout} component={AccountTimeline} content={children} />
|
||||||
<WrappedRoute path='/@:username/with_replies' publicRoute={!authenticatedProfile} layout={ProfileLayout} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
<WrappedRoute path='/@:username/with_replies' publicRoute={!authenticatedProfile} layout={ProfileLayout} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
||||||
<WrappedRoute path='/@:username/followers' publicRoute={!authenticatedProfile} layout={ProfileLayout} component={Followers} content={children} />
|
<WrappedRoute path='/@:username/followers' publicRoute={!authenticatedProfile} layout={ProfileLayout} component={Followers} content={children} />
|
||||||
|
|
|
@ -56,6 +56,7 @@ export const HomeTimeline = lazy(() => import('pl-fe/features/home-timeline'));
|
||||||
export const ImportData = lazy(() => import('pl-fe/features/import-data'));
|
export const ImportData = lazy(() => import('pl-fe/features/import-data'));
|
||||||
export const IntentionalError = lazy(() => import('pl-fe/features/intentional-error'));
|
export const IntentionalError = lazy(() => import('pl-fe/features/intentional-error'));
|
||||||
export const InteractionPolicies = lazy(() => import('pl-fe/features/interaction-policies'));
|
export const InteractionPolicies = lazy(() => import('pl-fe/features/interaction-policies'));
|
||||||
|
export const InteractionRequests = lazy(() => import('pl-fe/features/interaction-requests'));
|
||||||
export const LandingTimeline = lazy(() => import('pl-fe/features/landing-timeline'));
|
export const LandingTimeline = lazy(() => import('pl-fe/features/landing-timeline'));
|
||||||
export const Lists = lazy(() => import('pl-fe/features/lists'));
|
export const Lists = lazy(() => import('pl-fe/features/lists'));
|
||||||
export const ListTimeline = lazy(() => import('pl-fe/features/list-timeline'));
|
export const ListTimeline = lazy(() => import('pl-fe/features/list-timeline'));
|
||||||
|
|
|
@ -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