mirror of
https://github.com/mastodon/mastodon.git
synced 2026-08-25 02:06:40 -05:00
Status interaction action (#39681)
This commit is contained in:
@@ -7,6 +7,8 @@ import type { AxiosResponse } from 'axios';
|
||||
|
||||
import type { Alert } from 'mastodon/models/alert';
|
||||
|
||||
import { createAppThunk } from '../store/typed_functions';
|
||||
|
||||
interface ApiErrorResponse {
|
||||
error?: string;
|
||||
}
|
||||
@@ -74,3 +76,12 @@ export const showAlertForError = (error: unknown, skipNotFound = false) => {
|
||||
message: messages.unexpectedMessage,
|
||||
});
|
||||
};
|
||||
|
||||
export const showGenericAlert = createAppThunk((_arg, { dispatch }) => {
|
||||
dispatch(
|
||||
showAlert({
|
||||
title: messages.unexpectedTitle,
|
||||
message: messages.unexpectedMessage,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,263 @@
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import {
|
||||
apiReblog,
|
||||
apiUnreblog,
|
||||
apiRevokeQuote,
|
||||
apiGetQuotes,
|
||||
} from 'mastodon/api/interactions';
|
||||
import type { StatusVisibility } from 'mastodon/models/status';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
} from '@/mastodon/api/interactions';
|
||||
import type { StatusContextType } from '@/mastodon/components/status/types';
|
||||
import type { VisibilityModalCallback } from '@/mastodon/features/ui/components/visibility_modal';
|
||||
import type { StatusShape, StatusVisibility } from '@/mastodon/models/status';
|
||||
import {
|
||||
createAppThunk,
|
||||
createDataLoadingThunk,
|
||||
} from '@/mastodon/store/typed_functions';
|
||||
|
||||
import { deleteModal } from '../initial_state';
|
||||
import { selectStatusInteractions } from '../selectors/statuses';
|
||||
|
||||
import { showAlert, showGenericAlert } from './alerts';
|
||||
import { replyCompose } from './compose';
|
||||
import { quoteComposeById } from './compose_typed';
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import {
|
||||
bookmark,
|
||||
favourite,
|
||||
pin,
|
||||
unbookmark,
|
||||
unfavourite,
|
||||
unpin,
|
||||
} from './interactions';
|
||||
import { openModal } from './modal';
|
||||
import {
|
||||
deleteStatus,
|
||||
editStatus,
|
||||
muteStatus,
|
||||
setStatusQuotePolicy,
|
||||
unmuteStatus,
|
||||
} from './statuses';
|
||||
|
||||
export type StatusInteractionIntent =
|
||||
| 'bookmark'
|
||||
| 'delete'
|
||||
| 'editQuotePolicy'
|
||||
| 'edit'
|
||||
| 'embed'
|
||||
| 'favourite'
|
||||
| 'filter'
|
||||
| 'mute'
|
||||
| 'pin'
|
||||
| 'quote'
|
||||
| 'reblog'
|
||||
| 'redraft'
|
||||
| 'reply'
|
||||
| 'report'
|
||||
| 'revokeQuote';
|
||||
|
||||
const messages = defineMessages({
|
||||
noEdits: {
|
||||
id: 'status.cannot_edit',
|
||||
defaultMessage: 'You are not allowed to edit this post',
|
||||
},
|
||||
privateStatus: {
|
||||
id: 'status.public_only',
|
||||
defaultMessage: 'This status is private',
|
||||
},
|
||||
notQuoted: {
|
||||
id: 'status.not_quoted',
|
||||
defaultMessage: 'You are not quoted in this status',
|
||||
},
|
||||
});
|
||||
|
||||
export const statusInteraction = createAppThunk(
|
||||
(
|
||||
{
|
||||
statusId,
|
||||
contextType,
|
||||
intent,
|
||||
}: {
|
||||
statusId: string;
|
||||
contextType?: StatusContextType;
|
||||
intent: StatusInteractionIntent;
|
||||
},
|
||||
{ getState, dispatch },
|
||||
) => {
|
||||
const state = getState();
|
||||
const statusImmutable = state.statuses.get(statusId);
|
||||
if (!statusImmutable) {
|
||||
dispatch(showGenericAlert());
|
||||
return;
|
||||
}
|
||||
const status = statusImmutable.toJS() as unknown as StatusShape;
|
||||
|
||||
// Check the permissions and respond if it's not allowed.
|
||||
const interactions = selectStatusInteractions(state, statusId);
|
||||
const permissions = interactions[intent];
|
||||
if (!permissions.allowed) {
|
||||
// Must strictly check for false, as values can be undefined.
|
||||
if (permissions.isLoggedIn === false) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent,
|
||||
accountId: status.account,
|
||||
url: status.uri,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else if (permissions.isMine === false) {
|
||||
dispatch(showAlert({ message: messages.noEdits }));
|
||||
} else if (
|
||||
permissions.isNotDirect === false ||
|
||||
permissions.isPublic === false
|
||||
) {
|
||||
dispatch(showAlert({ message: messages.privateStatus }));
|
||||
} else if (permissions.isQuoted === false) {
|
||||
dispatch(showAlert({ message: messages.notQuoted }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle intents for all statuses.
|
||||
switch (intent) {
|
||||
case 'bookmark':
|
||||
if (status.bookmarked) {
|
||||
dispatch(unbookmark(statusImmutable));
|
||||
} else {
|
||||
dispatch(bookmark(statusImmutable));
|
||||
}
|
||||
return;
|
||||
case 'delete':
|
||||
if (!deleteModal) {
|
||||
void dispatch(deleteStatus(statusId));
|
||||
} else {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_DELETE_STATUS',
|
||||
modalProps: { statusId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
return;
|
||||
case 'edit': {
|
||||
const composerText = state.compose.get('text');
|
||||
if (typeof composerText === 'string' && composerText.trim()) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_EDIT_STATUS',
|
||||
modalProps: { statusId },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(editStatus(statusId));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'editQuotePolicy':
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'COMPOSE_PRIVACY',
|
||||
modalProps: {
|
||||
statusId,
|
||||
onChange: ((_, policy) => {
|
||||
void dispatch(setStatusQuotePolicy({ statusId, policy }));
|
||||
}) satisfies VisibilityModalCallback,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case 'embed':
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'EMBED',
|
||||
modalProps: { id: statusId },
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case 'filter':
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'FILTER',
|
||||
modalProps: { statusId, contextType },
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case 'favourite':
|
||||
if (status.favourited) {
|
||||
dispatch(unfavourite(statusImmutable));
|
||||
} else {
|
||||
dispatch(favourite(statusImmutable));
|
||||
}
|
||||
return;
|
||||
case 'mute':
|
||||
if (status.muted) {
|
||||
dispatch(unmuteStatus(statusId));
|
||||
} else {
|
||||
dispatch(muteStatus(statusId));
|
||||
}
|
||||
return;
|
||||
case 'pin':
|
||||
if (status.pinned) {
|
||||
dispatch(unpin(statusImmutable));
|
||||
} else {
|
||||
dispatch(pin(statusImmutable));
|
||||
}
|
||||
return;
|
||||
case 'quote':
|
||||
dispatch(quoteComposeById(statusId));
|
||||
return;
|
||||
case 'reblog':
|
||||
if (status.reblogged) {
|
||||
void dispatch(unreblog({ statusId }));
|
||||
} else {
|
||||
void dispatch(reblog({ statusId, visibility: status.visibility }));
|
||||
}
|
||||
return;
|
||||
case 'redraft':
|
||||
if (!deleteModal) {
|
||||
void dispatch(deleteStatus(statusId, true));
|
||||
} else {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_DELETE_STATUS',
|
||||
modalProps: {
|
||||
statusId,
|
||||
withRedraft: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return;
|
||||
case 'reply':
|
||||
dispatch(replyCompose(statusImmutable));
|
||||
return;
|
||||
case 'report':
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'REPORT',
|
||||
modalProps: {
|
||||
accountId: status.account,
|
||||
statusId: statusId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case 'revokeQuote':
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_REVOKE_QUOTE',
|
||||
modalProps: {
|
||||
statusId: statusId,
|
||||
quotedStatusId: status.quote?.quoted_status,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const reblog = createDataLoadingThunk(
|
||||
'status/reblog',
|
||||
|
||||
@@ -16,19 +16,9 @@ import {
|
||||
|
||||
import type { AttachmentArgs } from './testing';
|
||||
import { attachmentArgTypes, attachmentFactory } from './testing';
|
||||
import type { StatusContextType } from './types';
|
||||
import { TypedStatus } from './types';
|
||||
|
||||
type ContextTypes =
|
||||
| 'account'
|
||||
| 'bookmarks'
|
||||
| 'detailed'
|
||||
| 'favourites'
|
||||
| 'home'
|
||||
| 'notifications'
|
||||
| 'public'
|
||||
| 'search'
|
||||
| 'thread';
|
||||
|
||||
interface StatusStoryProps extends AttachmentArgs {
|
||||
// Contents
|
||||
text: string;
|
||||
@@ -51,7 +41,7 @@ interface StatusStoryProps extends AttachmentArgs {
|
||||
|
||||
// Display
|
||||
showThread?: boolean;
|
||||
contextType?: ContextTypes;
|
||||
contextType?: StatusContextType;
|
||||
showCounters?: boolean;
|
||||
favouriteCount?: number;
|
||||
reblogCount?: number;
|
||||
@@ -319,6 +309,7 @@ const meta = {
|
||||
options: [
|
||||
'account',
|
||||
'bookmarks',
|
||||
'composer',
|
||||
'detailed',
|
||||
'favourites',
|
||||
'home',
|
||||
@@ -326,7 +317,7 @@ const meta = {
|
||||
'public',
|
||||
'search',
|
||||
'thread',
|
||||
] satisfies ContextTypes[],
|
||||
] satisfies StatusContextType[],
|
||||
},
|
||||
hidden: categoryDisplay,
|
||||
muted: categoryDisplay,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ComponentType, MouseEventHandler, ReactNode } from 'react';
|
||||
|
||||
import StatusContainer from '@/mastodon/containers/status_container';
|
||||
import type { Account as TAccount } from '@/mastodon/models/account';
|
||||
import type { Status as TStatus } from '@/mastodon/models/status';
|
||||
|
||||
@@ -7,15 +8,55 @@ import Status from '../status';
|
||||
|
||||
import type { StatusHeaderRenderFn } from './header';
|
||||
|
||||
// Taken from the Status component.
|
||||
export interface StatusProps {
|
||||
status: TStatus;
|
||||
export type StatusContextType =
|
||||
| 'account'
|
||||
| 'bookmarks'
|
||||
| 'composer'
|
||||
| 'detailed'
|
||||
| 'favourites'
|
||||
| 'home'
|
||||
| `list:${string}`
|
||||
| 'notifications'
|
||||
| 'public'
|
||||
| 'search'
|
||||
| 'thread';
|
||||
|
||||
export interface StatusContainerProps {
|
||||
id?: string | null;
|
||||
account?: TAccount;
|
||||
children?: ReactNode;
|
||||
previousId?: string;
|
||||
nextInReplyToId?: string;
|
||||
rootId?: string;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
muted?: boolean;
|
||||
hidden?: boolean;
|
||||
unread?: boolean;
|
||||
featured?: boolean;
|
||||
showThread?: boolean;
|
||||
showActions?: boolean;
|
||||
isQuotedPost?: boolean;
|
||||
shouldHighlightOnMount?: boolean;
|
||||
contextType?: StatusContextType;
|
||||
withCounters?: boolean;
|
||||
unfocusable?: boolean;
|
||||
headerRenderFn?: StatusHeaderRenderFn;
|
||||
getScrollPosition?: () => null | { height: number; top: number };
|
||||
updateScrollBottom?: (snapshot: number) => void;
|
||||
cacheMediaWidth?: (width: number) => void;
|
||||
cachedMediaWidth?: number;
|
||||
scrollKey?: string;
|
||||
skipPrepend?: boolean;
|
||||
avatarSize?: number;
|
||||
withDismiss?: boolean;
|
||||
}
|
||||
|
||||
export const TypedStatusContainer =
|
||||
StatusContainer as ComponentType<StatusContainerProps>;
|
||||
|
||||
// Taken from the Status component.
|
||||
export interface StatusProps extends StatusContainerProps {
|
||||
status: TStatus;
|
||||
nextInReplyToId?: string;
|
||||
onReply: (status: TStatus) => void;
|
||||
onFavourite: (status: TStatus) => void;
|
||||
onReblog: (status: TStatus, event?: unknown) => void;
|
||||
@@ -44,31 +85,12 @@ export interface StatusProps {
|
||||
onToggleCollapsed: (status: TStatus, isCollapsed: boolean) => void;
|
||||
onTranslate: (status: TStatus) => void;
|
||||
onInteractionModal?: (type: string, status: TStatus) => void;
|
||||
muted?: boolean;
|
||||
hidden?: boolean;
|
||||
unread?: boolean;
|
||||
featured?: boolean;
|
||||
showThread?: boolean;
|
||||
showActions?: boolean;
|
||||
isQuotedPost?: boolean;
|
||||
shouldHighlightOnMount?: boolean;
|
||||
getScrollPosition?: () => null | { height: number; top: number };
|
||||
updateScrollBottom?: (snapshot: number) => void;
|
||||
cacheMediaWidth?: (width: number) => void;
|
||||
cachedMediaWidth?: number;
|
||||
scrollKey?: string;
|
||||
skipPrepend?: boolean;
|
||||
avatarSize?: number;
|
||||
deployPictureInPicture: (
|
||||
status: TStatus,
|
||||
type: string,
|
||||
mediaProps: unknown,
|
||||
) => void;
|
||||
unfocusable?: boolean;
|
||||
headerRenderFn?: StatusHeaderRenderFn;
|
||||
pictureInPicture: Immutable.Map<'inUse' | 'available', boolean>;
|
||||
contextType?: string;
|
||||
withCounters?: boolean;
|
||||
}
|
||||
|
||||
export const TypedStatus = Status as ComponentType<StatusProps>;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
|
||||
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||
import { fetchRelationships } from 'mastodon/actions/accounts';
|
||||
import { revealAccount } from 'mastodon/actions/accounts_typed';
|
||||
import { fetchStatus } from 'mastodon/actions/statuses';
|
||||
import { LearnMoreLink } from 'mastodon/components/learn_more_link';
|
||||
import StatusContainer from 'mastodon/containers/status_container';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
import type { Status } from 'mastodon/models/status';
|
||||
@@ -23,6 +23,8 @@ import { Button } from './button';
|
||||
import { IconButton } from './icon_button';
|
||||
import type { StatusHeaderRenderFn } from './status/header';
|
||||
import { StatusHeader } from './status/header';
|
||||
import { TypedStatusContainer } from './status/types';
|
||||
import type { StatusContainerProps, StatusContextType } from './status/types';
|
||||
|
||||
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
|
||||
|
||||
@@ -143,24 +145,9 @@ const FilteredQuote: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
// Adds a wrapper around StatusContainer as the types aren't inheriting correctly with Redux + React 19.
|
||||
// TODO: Remove this after the Status component is in TS.
|
||||
interface StatusContainerForQuotesProps {
|
||||
id?: string | null;
|
||||
contextType?: string;
|
||||
isQuotedPost?: boolean;
|
||||
avatarSize?: number;
|
||||
headerRenderFn?: StatusHeaderRenderFn;
|
||||
children?: ReactNode;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const StatusContainerWithChildren =
|
||||
StatusContainer as unknown as ComponentType<StatusContainerForQuotesProps>;
|
||||
|
||||
interface QuotedStatusProps {
|
||||
quote: QuoteMap;
|
||||
contextType?: string;
|
||||
contextType?: StatusContextType;
|
||||
parentQuotePostId?: string | null;
|
||||
variant?: 'full' | 'link';
|
||||
nestingLevel?: number;
|
||||
@@ -354,7 +341,7 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
|
||||
return (
|
||||
<div className='status__quote'>
|
||||
<StatusContainerWithChildren
|
||||
<TypedStatusContainer
|
||||
isQuotedPost
|
||||
id={quotedStatusId}
|
||||
contextType={contextType}
|
||||
@@ -372,16 +359,17 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
nestingLevel={nestingLevel + 1}
|
||||
/>
|
||||
)}
|
||||
</StatusContainerWithChildren>
|
||||
</TypedStatusContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface StatusQuoteManagerProps {
|
||||
id: string;
|
||||
contextType?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export type StatusQuoteManagerProps = Merge<
|
||||
StatusContainerProps,
|
||||
{
|
||||
id: string;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* This wrapper component takes a status ID and, if the associated status
|
||||
@@ -399,15 +387,15 @@ export const StatusQuoteManager = (props: StatusQuoteManagerProps) => {
|
||||
|
||||
if (quote) {
|
||||
return (
|
||||
<StatusContainerWithChildren {...props}>
|
||||
<TypedStatusContainer {...props}>
|
||||
<QuotedStatus
|
||||
quote={quote}
|
||||
parentQuotePostId={status?.get('id') as string}
|
||||
contextType={props.contextType}
|
||||
/>
|
||||
</StatusContainerWithChildren>
|
||||
</TypedStatusContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return <StatusContainerWithChildren {...props} />;
|
||||
return <TypedStatusContainer {...props} />;
|
||||
};
|
||||
|
||||
@@ -26,8 +26,6 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
type InteractionIntent = 'follow' | 'reblog' | 'favourite' | 'reply' | 'vote';
|
||||
|
||||
interface LoginFormMessage {
|
||||
type:
|
||||
| 'fetchInteractionURL'
|
||||
@@ -36,7 +34,7 @@ interface LoginFormMessage {
|
||||
uri_or_domain: string;
|
||||
template?: string;
|
||||
param?: string;
|
||||
intent?: InteractionIntent;
|
||||
intent?: string;
|
||||
}
|
||||
|
||||
const PERSISTENCE_KEY = 'mastodon_home';
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
"status.block": "Block @{name}",
|
||||
"status.bookmark": "Bookmark",
|
||||
"status.cancel_reblog_private": "Unboost",
|
||||
"status.cannot_edit": "You are not allowed to edit this post",
|
||||
"status.cannot_quote": "You are not allowed to quote this post",
|
||||
"status.cannot_reblog": "This post cannot be boosted",
|
||||
"status.contains_quote": "Contains quote",
|
||||
@@ -1264,8 +1265,10 @@
|
||||
"status.more": "More",
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute_conversation": "Mute conversation",
|
||||
"status.not_quoted": "You are not quoted in this status",
|
||||
"status.open": "Expand this post",
|
||||
"status.pin": "Pin on profile",
|
||||
"status.public_only": "This status is private",
|
||||
"status.quote": "Quote",
|
||||
"status.quote.cancel": "Cancel quote",
|
||||
"status.quote_error.blocked_account_hint.title": "This post is hidden because you've blocked @{name}.",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import { Record as ImmutableRecord, List as ImmutableList } from 'immutable';
|
||||
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { accountDefaultValues } from 'mastodon/models/account';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import { accountDefaultValues } from '@/mastodon/models/account';
|
||||
import type {
|
||||
Account,
|
||||
AccountShape,
|
||||
AccountShapeFull,
|
||||
} from 'mastodon/models/account';
|
||||
import type { Relationship } from 'mastodon/models/relationship';
|
||||
import { createAppSelector } from 'mastodon/store';
|
||||
import type { RootState } from 'mastodon/store';
|
||||
} from '@/mastodon/models/account';
|
||||
import type { Relationship } from '@/mastodon/models/relationship';
|
||||
import type { RootState } from '@/mastodon/store';
|
||||
import { createAppSelector } from '@/mastodon/store/typed_functions';
|
||||
|
||||
import type { ApiHashtagJSON } from '../api_types/tags';
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
|
||||
import { createAppSelector } from 'mastodon/store';
|
||||
|
||||
import type { ExpandedStatusShape, StatusShape } from '../models/status';
|
||||
import type { StatusInteractionIntent } from '@/mastodon/actions/interactions_typed';
|
||||
import type {
|
||||
ExpandedStatusShape,
|
||||
StatusShape,
|
||||
} from '@/mastodon/models/status';
|
||||
import { createAppSelector } from '@/mastodon/store/typed_functions';
|
||||
|
||||
import { selectPlainAccount } from './accounts';
|
||||
import type { FilterShape } from './filters';
|
||||
@@ -71,6 +74,75 @@ export const selectExpandedStatus = createAppSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const selectStatusConditions = createAppSelector(
|
||||
[
|
||||
selectPlainStatus,
|
||||
(state) => state.meta.get('me', null) as string | null,
|
||||
(state, statusId: string) =>
|
||||
state.statuses.getIn([
|
||||
state.statuses.getIn([statusId, 'quote', 'quoted_status']),
|
||||
'account',
|
||||
]) as string | undefined | null,
|
||||
],
|
||||
(status, currentAccountId, quotedAccountId) => ({
|
||||
isPublic: status && ['public', 'unlisted'].includes(status.visibility),
|
||||
isLoggedIn: !!currentAccountId,
|
||||
isMine: status && !!currentAccountId && status.account === currentAccountId,
|
||||
isQuoted:
|
||||
status && !!currentAccountId && quotedAccountId === currentAccountId,
|
||||
isNotDirect: status && status.visibility !== 'direct',
|
||||
}),
|
||||
);
|
||||
|
||||
export const selectStatusInteractions = createAppSelector(
|
||||
[(_, statusId: string) => statusId, selectStatusConditions],
|
||||
(statusId, conditionals) => {
|
||||
function addAllowed(conditions: Partial<typeof conditionals>) {
|
||||
return {
|
||||
...conditions,
|
||||
allowed: Object.values(conditions).every(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
const { isLoggedIn, isMine, isPublic, isQuoted, isNotDirect } =
|
||||
conditionals;
|
||||
|
||||
const interactions: Record<
|
||||
StatusInteractionIntent,
|
||||
Partial<typeof conditionals> & { allowed: boolean }
|
||||
> = {
|
||||
bookmark: addAllowed({ isLoggedIn }),
|
||||
delete: addAllowed({ isMine }),
|
||||
edit: addAllowed({ isMine }),
|
||||
editQuotePolicy: addAllowed({ isMine, isPublic }),
|
||||
embed: addAllowed({ isPublic }),
|
||||
favourite: addAllowed({ isLoggedIn }),
|
||||
filter: addAllowed({ isLoggedIn }),
|
||||
mute: addAllowed({ isMine }),
|
||||
pin: addAllowed({ isMine, isNotDirect }),
|
||||
quote: addAllowed({ isLoggedIn }),
|
||||
reblog: addAllowed({ isLoggedIn }),
|
||||
redraft: addAllowed({ isMine }),
|
||||
reply: addAllowed({ isLoggedIn }),
|
||||
report: addAllowed({ isLoggedIn }),
|
||||
revokeQuote: addAllowed({ isQuoted }),
|
||||
};
|
||||
|
||||
return {
|
||||
statusId,
|
||||
...interactions,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const selectStatusIntentAllowed = createAppSelector(
|
||||
[
|
||||
selectStatusInteractions,
|
||||
(_state, _statusId: string, intent: StatusInteractionIntent) => intent,
|
||||
],
|
||||
(allowedInteractions, intent) => allowedInteractions[intent].allowed,
|
||||
);
|
||||
|
||||
export const selectPictureInPicture = createAppSelector(
|
||||
[
|
||||
(state, statusId: string) =>
|
||||
|
||||
Reference in New Issue
Block a user