mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-13 02:36:36 -05:00
Status Action Bar (#39702)
This commit is contained in:
597
app/javascript/mastodon/components/status/action_bar.tsx
Normal file
597
app/javascript/mastodon/components/status/action_bar.tsx
Normal file
@@ -0,0 +1,597 @@
|
||||
import type React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import {
|
||||
muteAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
} from '@/mastodon/actions/accounts';
|
||||
import { initBlockModal } from '@/mastodon/actions/blocks';
|
||||
import { directCompose, mentionCompose } from '@/mastodon/actions/compose';
|
||||
import {
|
||||
initDomainBlockModal,
|
||||
unblockDomain,
|
||||
} from '@/mastodon/actions/domain_blocks';
|
||||
import type { StatusInteractionIntent } from '@/mastodon/actions/interactions';
|
||||
import { statusInteraction } from '@/mastodon/actions/interactions';
|
||||
import { fetchStatus } from '@/mastodon/actions/statuses';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { useRelationship } from '@/mastodon/hooks/useRelationship';
|
||||
import { useStatus } from '@/mastodon/hooks/useStatus';
|
||||
import { useIdentity } from '@/mastodon/identity_context';
|
||||
import { quickBoosting } from '@/mastodon/initial_state';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type { MenuItem } from '@/mastodon/models/dropdown_menu';
|
||||
import type { Relationship } from '@/mastodon/models/relationship';
|
||||
import type { StatusShape } from '@/mastodon/models/status';
|
||||
import {
|
||||
PERMISSION_MANAGE_FEDERATION,
|
||||
PERMISSION_MANAGE_USERS,
|
||||
} from '@/mastodon/permissions';
|
||||
import type {
|
||||
StatusConditions,
|
||||
StatusInteractionsAllowed,
|
||||
} from '@/mastodon/selectors/statuses';
|
||||
import {
|
||||
selectStatusConditions,
|
||||
selectStatusInteractionsAllowed,
|
||||
} from '@/mastodon/selectors/statuses';
|
||||
import type { AppDispatch } from '@/mastodon/store';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
||||
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
|
||||
|
||||
import { Dropdown } from '../dropdown_menu';
|
||||
import { IconButton } from '../icon_button';
|
||||
import { RemoveQuoteHint } from '../status_action_bar/remove_quote_hint';
|
||||
|
||||
import { BoostButton } from './boost_button';
|
||||
import { quoteItemState } from './boost_button_utils';
|
||||
import type { StatusContextType } from './types';
|
||||
|
||||
interface StatusActionBarProps {
|
||||
statusId: string;
|
||||
contextType?: StatusContextType;
|
||||
withDismiss?: boolean;
|
||||
withCounters?: boolean;
|
||||
scrollKey?: string;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
||||
removeFavourite: {
|
||||
id: 'status.remove_favourite',
|
||||
defaultMessage: 'Remove from favorites',
|
||||
},
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
removeBookmark: {
|
||||
id: 'status.remove_bookmark',
|
||||
defaultMessage: 'Remove bookmark',
|
||||
},
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
muteConversation: {
|
||||
id: 'status.mute_conversation',
|
||||
defaultMessage: 'Mute conversation',
|
||||
},
|
||||
unmuteConversation: {
|
||||
id: 'status.unmute_conversation',
|
||||
defaultMessage: 'Unmute conversation',
|
||||
},
|
||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
|
||||
admin_account: {
|
||||
id: 'status.admin_account',
|
||||
defaultMessage: 'Open moderation interface for @{name}',
|
||||
},
|
||||
admin_status: {
|
||||
id: 'status.admin_status',
|
||||
defaultMessage: 'Open this post in the moderation interface',
|
||||
},
|
||||
admin_domain: {
|
||||
id: 'status.admin_domain',
|
||||
defaultMessage: 'Open moderation interface for {domain}',
|
||||
},
|
||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
|
||||
blockDomain: {
|
||||
id: 'account.block_domain',
|
||||
defaultMessage: 'Block domain {domain}',
|
||||
},
|
||||
unblockDomain: {
|
||||
id: 'account.unblock_domain',
|
||||
defaultMessage: 'Unblock domain {domain}',
|
||||
},
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
|
||||
openOriginalPage: {
|
||||
id: 'account.open_original_page',
|
||||
defaultMessage: 'Open original page',
|
||||
},
|
||||
revokeQuote: {
|
||||
id: 'status.revoke_quote',
|
||||
defaultMessage: 'Remove my post from @{name}’s post',
|
||||
},
|
||||
quotePolicyChange: {
|
||||
id: 'status.quote_policy_change',
|
||||
defaultMessage: 'Change who can quote',
|
||||
},
|
||||
});
|
||||
|
||||
export const StatusActionBar: React.FC<StatusActionBarProps> = ({
|
||||
statusId,
|
||||
contextType,
|
||||
withCounters,
|
||||
scrollKey,
|
||||
}) => {
|
||||
const status = useStatus(statusId);
|
||||
const quotedAccountId = useAppSelector(
|
||||
(state) =>
|
||||
state.statuses.getIn([status?.quote?.quoted_status, 'account']) ?? null,
|
||||
);
|
||||
const currentAccountId = useCurrentAccountId();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleReplyClick = useCallback(() => {
|
||||
dispatch(statusInteraction({ statusId, intent: 'reply' }));
|
||||
}, [dispatch, statusId]);
|
||||
const handleFavouriteClick = useCallback(() => {
|
||||
dispatch(statusInteraction({ statusId, intent: 'favourite' }));
|
||||
}, [dispatch, statusId]);
|
||||
const handleBookmarkClick = useCallback(() => {
|
||||
dispatch(statusInteraction({ statusId, intent: 'bookmark' }));
|
||||
}, [dispatch, statusId]);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isReply =
|
||||
!status.in_reply_to_id || status.in_reply_to_account_id === status.account;
|
||||
const replyTitle = isReply
|
||||
? intl.formatMessage(messages.reply)
|
||||
: intl.formatMessage(messages.replyAll);
|
||||
const replyIcon = isReply ? 'reply' : 'reply-all';
|
||||
const replyIconComponent = isReply ? ReplyIcon : ReplyAllIcon;
|
||||
|
||||
const bookmarkTitle = intl.formatMessage(
|
||||
status.bookmarked ? messages.removeBookmark : messages.bookmark,
|
||||
);
|
||||
const favouriteTitle = intl.formatMessage(
|
||||
status.favourited ? messages.removeFavourite : messages.favourite,
|
||||
);
|
||||
|
||||
const isQuotingMe = quotedAccountId === currentAccountId;
|
||||
const shouldShowQuoteRemovalHint =
|
||||
isQuotingMe && contextType === 'notifications';
|
||||
|
||||
return (
|
||||
<div className='status__action-bar'>
|
||||
<div className='status__action-bar__button-wrapper'>
|
||||
<IconButton
|
||||
className='status__action-bar__button'
|
||||
title={replyTitle}
|
||||
icon={replyIcon}
|
||||
iconComponent={replyIconComponent}
|
||||
onClick={handleReplyClick}
|
||||
counter={status.replies_count}
|
||||
/>
|
||||
</div>
|
||||
<div className='status__action-bar__button-wrapper'>
|
||||
<BoostButton statusId={statusId} counters={withCounters} />
|
||||
</div>
|
||||
<div className='status__action-bar__button-wrapper'>
|
||||
<IconButton
|
||||
className='status__action-bar__button star-icon'
|
||||
animate
|
||||
active={status.favourited}
|
||||
title={favouriteTitle}
|
||||
icon='star'
|
||||
iconComponent={status.favourited ? StarIcon : StarBorderIcon}
|
||||
onClick={handleFavouriteClick}
|
||||
counter={withCounters ? status.favourites_count : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className='status__action-bar__button-wrapper'>
|
||||
<IconButton
|
||||
className='status__action-bar__button bookmark-icon'
|
||||
disabled={!currentAccountId}
|
||||
active={status.bookmarked}
|
||||
title={bookmarkTitle}
|
||||
icon='bookmark'
|
||||
iconComponent={status.bookmarked ? BookmarkIcon : BookmarkBorderIcon}
|
||||
onClick={handleBookmarkClick}
|
||||
/>
|
||||
</div>
|
||||
<RemoveQuoteHint
|
||||
className='status__action-bar__button-wrapper'
|
||||
canShowHint={shouldShowQuoteRemovalHint}
|
||||
>
|
||||
{(dismissQuoteHint) => (
|
||||
<StatusActionMenu
|
||||
dismissQuoteHint={dismissQuoteHint}
|
||||
status={status}
|
||||
contextType={contextType}
|
||||
scrollKey={scrollKey}
|
||||
/>
|
||||
)}
|
||||
</RemoveQuoteHint>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusActionMenu: React.FC<{
|
||||
dismissQuoteHint: () => void;
|
||||
status: StatusShape;
|
||||
contextType?: StatusContextType;
|
||||
scrollKey?: string;
|
||||
withDismiss?: boolean;
|
||||
}> = ({ status, dismissQuoteHint, contextType, scrollKey, withDismiss }) => {
|
||||
const account = useAppSelector((state) => state.accounts.get(status.account));
|
||||
const { permissions } = useIdentity();
|
||||
const relationship = useRelationship(account?.id);
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const conditions = useAppSelector((state) =>
|
||||
selectStatusConditions(state, status.id),
|
||||
);
|
||||
const interactions = useAppSelector((state) =>
|
||||
selectStatusInteractionsAllowed(state, status.id),
|
||||
);
|
||||
const statusInteractionFactory = useCallback(
|
||||
(intent: StatusInteractionIntent) => {
|
||||
return () => {
|
||||
dispatch(
|
||||
statusInteraction({ statusId: status.id, contextType, intent }),
|
||||
);
|
||||
};
|
||||
},
|
||||
[contextType, dispatch, status.id],
|
||||
);
|
||||
|
||||
const menu = useMemo(
|
||||
() =>
|
||||
getMenuItems({
|
||||
status,
|
||||
account,
|
||||
conditions,
|
||||
interactions,
|
||||
onStatusInteraction: statusInteractionFactory,
|
||||
withDismiss,
|
||||
permissions,
|
||||
intl,
|
||||
relationship,
|
||||
dispatch,
|
||||
}),
|
||||
[
|
||||
status,
|
||||
account,
|
||||
conditions,
|
||||
interactions,
|
||||
statusInteractionFactory,
|
||||
withDismiss,
|
||||
permissions,
|
||||
intl,
|
||||
relationship,
|
||||
dispatch,
|
||||
],
|
||||
);
|
||||
const handleOpen = useCallback(() => {
|
||||
// Replicates needsStatusRefresh of the Dropdown component.
|
||||
if (quickBoosting && !status.quote_approval) {
|
||||
dispatch(
|
||||
fetchStatus(status.id, { forceFetch: true, alsoFetchContext: false }),
|
||||
);
|
||||
}
|
||||
|
||||
dismissQuoteHint();
|
||||
return true;
|
||||
}, [dismissQuoteHint, dispatch, status.id, status.quote_approval]);
|
||||
|
||||
return (
|
||||
<Dropdown scrollKey={scrollKey} items={menu} onOpen={handleOpen}>
|
||||
<IconButton
|
||||
className='status__action-bar__button'
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
interface MenuItemsParams {
|
||||
status: StatusShape;
|
||||
account?: Account;
|
||||
conditions: StatusConditions;
|
||||
interactions: StatusInteractionsAllowed;
|
||||
onStatusInteraction: (intent: StatusInteractionIntent) => () => void;
|
||||
withDismiss?: boolean;
|
||||
permissions: number;
|
||||
intl: ReturnType<typeof useIntl>;
|
||||
relationship?: Relationship | null;
|
||||
dispatch: AppDispatch;
|
||||
}
|
||||
|
||||
function getMenuItems({
|
||||
status,
|
||||
account,
|
||||
conditions,
|
||||
interactions,
|
||||
onStatusInteraction,
|
||||
withDismiss,
|
||||
permissions,
|
||||
intl,
|
||||
relationship,
|
||||
dispatch,
|
||||
}: MenuItemsParams) {
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
const statusId = status.id;
|
||||
const statusUrl = status.url ?? status.uri;
|
||||
const { isPublic, isLocal, isLoggedIn, isMine } = conditions;
|
||||
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.open),
|
||||
to: `/@${account?.acct}/${statusId}`,
|
||||
});
|
||||
|
||||
if (isPublic && !isLocal) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.openOriginalPage),
|
||||
href: statusUrl,
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.copy),
|
||||
action: () => {
|
||||
void navigator.clipboard.writeText(statusUrl);
|
||||
},
|
||||
});
|
||||
|
||||
if (isPublic && 'share' in navigator) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.share),
|
||||
action: () => {
|
||||
void navigator.share({
|
||||
url: statusUrl,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (interactions.embed) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.embed),
|
||||
action: onStatusInteraction('embed'),
|
||||
});
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
if (quickBoosting) {
|
||||
menu.push(null);
|
||||
const quoteItem = quoteItemState(conditions);
|
||||
menu.push({
|
||||
text: intl.formatMessage(quoteItem.title),
|
||||
description: quoteItem.meta
|
||||
? intl.formatMessage(quoteItem.meta)
|
||||
: undefined,
|
||||
disabled: quoteItem.disabled,
|
||||
action: onStatusInteraction('quote'),
|
||||
});
|
||||
}
|
||||
|
||||
menu.push(null);
|
||||
|
||||
if (interactions.pin) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.pinned ? messages.unpin : messages.pin),
|
||||
action: onStatusInteraction('pin'),
|
||||
});
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if (interactions.mute || withDismiss) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(
|
||||
status.muted ? messages.unmuteConversation : messages.muteConversation,
|
||||
),
|
||||
action: onStatusInteraction('mute'),
|
||||
});
|
||||
if (interactions.editQuotePolicy) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.quotePolicyChange),
|
||||
action: onStatusInteraction('editQuotePolicy'),
|
||||
});
|
||||
}
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if (interactions.edit && interactions.delete && interactions.redraft) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.edit),
|
||||
action: onStatusInteraction('edit'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.delete),
|
||||
action: onStatusInteraction('delete'),
|
||||
dangerous: true,
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.redraft),
|
||||
action: onStatusInteraction('redraft'),
|
||||
dangerous: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (isMine || !account) {
|
||||
// Add the filter to handle the edge case of not having account data.
|
||||
if (interactions.filter) {
|
||||
menu.push(null);
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.filter),
|
||||
action: onStatusInteraction('filter'),
|
||||
dangerous: true,
|
||||
});
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.mention, {
|
||||
name: account.username,
|
||||
}),
|
||||
action: () => {
|
||||
dispatch(mentionCompose(account));
|
||||
},
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.direct, {
|
||||
name: account.username,
|
||||
}),
|
||||
action: () => {
|
||||
dispatch(directCompose(account));
|
||||
},
|
||||
});
|
||||
menu.push(null);
|
||||
|
||||
if (interactions.revokeQuote) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.revokeQuote, {
|
||||
name: account.username,
|
||||
}),
|
||||
action: onStatusInteraction('revokeQuote'),
|
||||
dangerous: true,
|
||||
});
|
||||
}
|
||||
|
||||
const isMuted = !!relationship?.get('muting');
|
||||
menu.push({
|
||||
text: intl.formatMessage(isMuted ? messages.unmute : messages.mute, {
|
||||
name: account.username,
|
||||
}),
|
||||
action: () => {
|
||||
if (isMuted) {
|
||||
dispatch(unmuteAccount(account.id));
|
||||
} else {
|
||||
dispatch(muteAccount(account.id));
|
||||
}
|
||||
},
|
||||
dangerous: !isMuted,
|
||||
});
|
||||
|
||||
const isBlocking = !!relationship?.blocking;
|
||||
menu.push({
|
||||
text: intl.formatMessage(isBlocking ? messages.unblock : messages.block, {
|
||||
name: account.username,
|
||||
}),
|
||||
action: () => {
|
||||
if (isBlocking) {
|
||||
dispatch(unblockAccount(account.id));
|
||||
} else {
|
||||
dispatch(initBlockModal(account));
|
||||
}
|
||||
},
|
||||
dangerous: !isBlocking,
|
||||
});
|
||||
|
||||
if (interactions.filter) {
|
||||
menu.push(null);
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.filter),
|
||||
action: onStatusInteraction('filter'),
|
||||
dangerous: true,
|
||||
});
|
||||
}
|
||||
menu.push(null);
|
||||
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.report, {
|
||||
name: account.username,
|
||||
}),
|
||||
action: onStatusInteraction('report'),
|
||||
dangerous: true,
|
||||
});
|
||||
|
||||
const domain = account.acct.split('@')[1];
|
||||
|
||||
if (!isLocal) {
|
||||
menu.push(null);
|
||||
|
||||
const isDomainBlocking = !!relationship?.domain_blocking;
|
||||
menu.push({
|
||||
text: intl.formatMessage(
|
||||
isDomainBlocking ? messages.unblockDomain : messages.blockDomain,
|
||||
{ domain },
|
||||
),
|
||||
action: () => {
|
||||
if (isDomainBlocking) {
|
||||
dispatch(unblockDomain(domain));
|
||||
} else {
|
||||
dispatch(initDomainBlockModal(account));
|
||||
}
|
||||
},
|
||||
dangerous: !isDomainBlocking,
|
||||
});
|
||||
}
|
||||
|
||||
const canManageUsers =
|
||||
(permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS;
|
||||
const canManageFederation =
|
||||
(permissions & PERMISSION_MANAGE_FEDERATION) ===
|
||||
PERMISSION_MANAGE_FEDERATION && !isLocal;
|
||||
|
||||
if (!canManageUsers && !canManageFederation) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
menu.push(null);
|
||||
if (canManageUsers) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.admin_account, {
|
||||
name: account.username,
|
||||
}),
|
||||
href: `/admin/accounts/${status.account}`,
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.admin_status),
|
||||
href: `/admin/accounts/${status.account}/statuses/${status.id}`,
|
||||
});
|
||||
}
|
||||
if (canManageFederation) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.admin_domain, {
|
||||
domain,
|
||||
}),
|
||||
href: `/admin/instances/${domain}`,
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { DeployPictureInPictureCallback } from '@/mastodon/actions/picture_
|
||||
import { deployPictureInPicture } from '@/mastodon/actions/picture_in_picture';
|
||||
import { CollectionPreviewCard } from '@/mastodon/features/collections/components/collection_preview_card';
|
||||
import Card from '@/mastodon/features/status/components/card';
|
||||
import { useExpandedStatus } from '@/mastodon/hooks/useStatus';
|
||||
import { displayMedia } from '@/mastodon/initial_state';
|
||||
import type {
|
||||
MediaAttachment,
|
||||
@@ -12,7 +13,6 @@ import type {
|
||||
} from '@/mastodon/models/status';
|
||||
import { isMediaAttachmentOfType } from '@/mastodon/models/status';
|
||||
import {
|
||||
selectExpandedStatus,
|
||||
selectMediaMatchFilters,
|
||||
selectPictureInPicture,
|
||||
} from '@/mastodon/selectors/statuses';
|
||||
@@ -25,10 +25,7 @@ export const StatusAttachments: React.FC<{
|
||||
statusId: string;
|
||||
contextType?: string;
|
||||
}> = ({ statusId, contextType }) => {
|
||||
// Selectors
|
||||
const status = useAppSelector((state) =>
|
||||
selectExpandedStatus(state, statusId),
|
||||
);
|
||||
const status = useExpandedStatus(statusId);
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
|
||||
@@ -38,30 +38,35 @@ const meta = {
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<BoostButton status={argsToStatus(args)} counters={args.reblogCount > 0} />
|
||||
<BoostButton statusId='1' counters={args.reblogCount > 0} />
|
||||
),
|
||||
parameters: {
|
||||
stateFn({
|
||||
reblogCount,
|
||||
visibility,
|
||||
quoteAllowed,
|
||||
alreadyBoosted,
|
||||
}: StoryProps) {
|
||||
return {
|
||||
statuses: {
|
||||
'1': statusFactoryImmutable({
|
||||
reblogs_count: reblogCount,
|
||||
visibility,
|
||||
reblogged: alreadyBoosted,
|
||||
quote_approval: {
|
||||
automatic: [],
|
||||
manual: [],
|
||||
current_user: quoteAllowed ? 'automatic' : 'denied',
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
} satisfies Meta<StoryProps>;
|
||||
|
||||
export default meta;
|
||||
|
||||
function argsToStatus({
|
||||
reblogCount,
|
||||
visibility,
|
||||
quoteAllowed,
|
||||
alreadyBoosted,
|
||||
}: StoryProps) {
|
||||
return statusFactoryImmutable({
|
||||
reblogs_count: reblogCount,
|
||||
visibility,
|
||||
reblogged: alreadyBoosted,
|
||||
quote_approval: {
|
||||
automatic: [],
|
||||
manual: [],
|
||||
current_user: quoteAllowed ? 'automatic' : 'denied',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
@@ -11,28 +11,28 @@ import { quoteComposeById } from '@/mastodon/actions/compose_typed';
|
||||
import { toggleReblog } from '@/mastodon/actions/interactions';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { fetchStatus } from '@/mastodon/actions/statuses';
|
||||
import { useStatus } from '@/mastodon/hooks/useStatus';
|
||||
import { quickBoosting } from '@/mastodon/initial_state';
|
||||
import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu';
|
||||
import type { Status } from '@/mastodon/models/status';
|
||||
import { selectStatusConditions } from '@/mastodon/selectors/statuses';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import type { RenderItemFn } from '../dropdown_menu';
|
||||
import { Dropdown, DropdownMenuItemContent } from '../dropdown_menu';
|
||||
import { IconButton } from '../icon_button';
|
||||
|
||||
import {
|
||||
boostItemState,
|
||||
messages,
|
||||
quoteItemState,
|
||||
selectStatusState,
|
||||
} from './boost_button_utils';
|
||||
import { boostItemState, messages, quoteItemState } from './boost_button_utils';
|
||||
|
||||
const StandaloneBoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
const StandaloneBoostButton: FC<ReblogButtonProps> = ({
|
||||
statusId,
|
||||
counters,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const status = useStatus(statusId);
|
||||
const statusState = useAppSelector((state) =>
|
||||
selectStatusState(state, status),
|
||||
selectStatusConditions(state, statusId),
|
||||
);
|
||||
const { title, meta, iconComponent, disabled } = useMemo(
|
||||
() => boostItemState(statusState),
|
||||
@@ -42,36 +42,35 @@ const StandaloneBoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
const handleClick: MouseEventHandler = useCallback(
|
||||
(event) => {
|
||||
if (statusState.isLoggedIn) {
|
||||
dispatch(toggleReblog(status.get('id') as string, event.shiftKey));
|
||||
dispatch(toggleReblog(statusId, event.shiftKey));
|
||||
} else {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'reblog',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
accountId: status?.account,
|
||||
url: status?.uri,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, status, statusState.isLoggedIn],
|
||||
[dispatch, status, statusId, statusState.isLoggedIn],
|
||||
);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
active={!!status.get('reblogged')}
|
||||
active={!!status?.reblogged}
|
||||
title={intl.formatMessage(meta ?? title)}
|
||||
icon='retweet'
|
||||
iconComponent={iconComponent}
|
||||
className='status__action-bar__button'
|
||||
onClick={!disabled ? handleClick : undefined}
|
||||
counter={
|
||||
counters
|
||||
? (status.get('reblogs_count') as number) +
|
||||
(status.get('quotes_count') as number)
|
||||
counters && status
|
||||
? status.reblogs_count + status.quotes_count
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
@@ -88,22 +87,23 @@ const renderMenuItem: RenderItemFn<ActionMenuItem> = (item, index, onClick) => (
|
||||
);
|
||||
|
||||
interface ReblogButtonProps {
|
||||
status: Status;
|
||||
statusId: string;
|
||||
counters?: boolean;
|
||||
}
|
||||
|
||||
type ActionMenuItemWithIcon = SetRequired<ActionMenuItem, 'icon'>;
|
||||
|
||||
const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ statusId, counters }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const status = useStatus(statusId);
|
||||
const statusState = useAppSelector((state) =>
|
||||
selectStatusState(state, status),
|
||||
selectStatusConditions(state, statusId),
|
||||
);
|
||||
const {
|
||||
isLoggedIn,
|
||||
isReblogged,
|
||||
isReblogAllowed,
|
||||
isBoosted,
|
||||
isBoostingAllowed,
|
||||
isQuoteAutomaticallyAccepted,
|
||||
isQuoteManuallyAccepted,
|
||||
} = statusState;
|
||||
@@ -111,11 +111,10 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
const isMenuDisabled =
|
||||
!isQuoteAutomaticallyAccepted &&
|
||||
!isQuoteManuallyAccepted &&
|
||||
!isReblogAllowed;
|
||||
!isBoostingAllowed;
|
||||
|
||||
const statusId = status.get('id') as string;
|
||||
const wasBoosted = !!status.get('reblogged');
|
||||
const quoteApproval = status.get('quote_approval');
|
||||
const wasBoosted = !!status?.reblogged;
|
||||
const quoteApproval = status?.quote_approval;
|
||||
|
||||
const showLoginPrompt = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -123,8 +122,8 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'reblog',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
accountId: status?.account,
|
||||
url: status?.uri,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -170,7 +169,7 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
dispatch(toggleReblog(status.get('id'), true));
|
||||
dispatch(toggleReblog(statusId, true));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -182,7 +181,7 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
|
||||
return true;
|
||||
},
|
||||
[dispatch, isLoggedIn, showLoginPrompt, status, quoteApproval, statusId],
|
||||
[dispatch, isLoggedIn, showLoginPrompt, quoteApproval, statusId],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -202,12 +201,11 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
className='status__action-bar__button'
|
||||
iconComponent={boostIcon}
|
||||
counter={
|
||||
counters
|
||||
? (status.get('reblogs_count') as number) +
|
||||
(status.get('quotes_count') as number)
|
||||
counters && status
|
||||
? status.reblogs_count + status.quotes_count
|
||||
: undefined
|
||||
}
|
||||
active={isReblogged}
|
||||
active={isBoosted}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { defineMessages } from 'react-intl';
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
|
||||
import type { Status, StatusVisibility } from '@/mastodon/models/status';
|
||||
import { createAppSelector } from '@/mastodon/store';
|
||||
import type { StatusConditions } from '@/mastodon/selectors/statuses';
|
||||
import FormatQuote from '@/material-icons/400-24px/format_quote-fill.svg?react';
|
||||
import FormatQuoteOff from '@/material-icons/400-24px/format_quote_off-fill.svg?react';
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
@@ -62,42 +61,6 @@ export const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
export const selectStatusState = createAppSelector(
|
||||
[
|
||||
(state) => state.meta.get('me') as string | undefined,
|
||||
(_, status: Status) => status,
|
||||
],
|
||||
(userId, status) => {
|
||||
const isPublic = ['public', 'unlisted'].includes(
|
||||
status.get('visibility') as StatusVisibility,
|
||||
);
|
||||
const isMineAndPrivate =
|
||||
userId === status.getIn(['account', 'id']) &&
|
||||
status.get('visibility') === 'private';
|
||||
return {
|
||||
isLoggedIn: !!userId,
|
||||
isPublic,
|
||||
isMine: userId === status.getIn(['account', 'id']),
|
||||
isPrivateReblog:
|
||||
userId === status.getIn(['account', 'id']) &&
|
||||
status.get('visibility') === 'private',
|
||||
isReblogged: !!status.get('reblogged'),
|
||||
isReblogAllowed: isPublic || isMineAndPrivate,
|
||||
isQuoteAutomaticallyAccepted:
|
||||
status.getIn(['quote_approval', 'current_user']) === 'automatic' &&
|
||||
(isPublic || isMineAndPrivate),
|
||||
isQuoteManuallyAccepted:
|
||||
status.getIn(['quote_approval', 'current_user']) === 'manual' &&
|
||||
(isPublic || isMineAndPrivate),
|
||||
isQuoteFollowersOnly:
|
||||
status.getIn(['quote_approval', 'automatic', 0]) === 'followers' ||
|
||||
status.getIn(['quote_approval', 'manual', 0]) === 'followers',
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export type StatusState = ReturnType<typeof selectStatusState>;
|
||||
|
||||
export interface MenuItemState {
|
||||
title: MessageDescriptor;
|
||||
meta?: MessageDescriptor;
|
||||
@@ -108,9 +71,9 @@ export interface MenuItemState {
|
||||
export function boostItemState({
|
||||
isPublic,
|
||||
isPrivateReblog,
|
||||
isReblogged,
|
||||
}: StatusState): MenuItemState {
|
||||
if (isReblogged) {
|
||||
isBoosted,
|
||||
}: StatusConditions): MenuItemState {
|
||||
if (isBoosted) {
|
||||
return {
|
||||
title: messages.reblog_cancel,
|
||||
iconComponent: isPublic ? RepeatActiveIcon : RepeatPrivateActiveIcon,
|
||||
@@ -139,7 +102,7 @@ export function quoteItemState({
|
||||
isQuoteManuallyAccepted,
|
||||
isQuoteFollowersOnly,
|
||||
isPublic,
|
||||
}: StatusState): MenuItemState {
|
||||
}: StatusConditions): MenuItemState {
|
||||
const iconText: MenuItemState = {
|
||||
title: messages.quote,
|
||||
iconComponent: FormatQuote,
|
||||
|
||||
@@ -6,10 +6,10 @@ import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { toggleStatusCollapse } from '@/mastodon/actions/statuses';
|
||||
import { useStatus } from '@/mastodon/hooks/useStatus';
|
||||
import { useIdentity } from '@/mastodon/identity_context';
|
||||
import { languages as preloadedLanguages } from '@/mastodon/initial_state';
|
||||
import type { StatusTranslation } from '@/mastodon/models/status';
|
||||
import { selectPlainStatus } from '@/mastodon/selectors/statuses';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
|
||||
@@ -27,7 +27,7 @@ export const StatusContent: React.FC<{
|
||||
onTranslate?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
collapsible?: boolean;
|
||||
}> = ({ statusId, onClick, onTranslate, collapsible }) => {
|
||||
const status = useAppSelector((state) => selectPlainStatus(state, statusId));
|
||||
const status = useStatus(statusId);
|
||||
const { signedIn } = useIdentity();
|
||||
const targetLanguages = useAppSelector(
|
||||
(state) =>
|
||||
|
||||
@@ -26,7 +26,8 @@ import { IconButton } from '../icon_button';
|
||||
import { injectIntl } from '../intl';
|
||||
import { BoostButton } from '../status/boost_button';
|
||||
import { RemoveQuoteHint } from './remove_quote_hint';
|
||||
import { quoteItemState, selectStatusState } from '../status/boost_button_utils';
|
||||
import { quoteItemState } from '../status/boost_button_utils';
|
||||
import { selectStatusConditions } from '@/mastodon/selectors/statuses';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -71,7 +72,7 @@ const mapStateToProps = (state, { status }) => {
|
||||
return ({
|
||||
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
|
||||
quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null,
|
||||
statusQuoteState: selectStatusState(state, status),
|
||||
statusQuoteState: selectStatusConditions(state, status.get('id')),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -393,7 +394,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
|
||||
</div>
|
||||
<div className='status__action-bar__button-wrapper'>
|
||||
<BoostButton status={status} counters={withCounters} />
|
||||
<BoostButton statusId={status.get('id')} counters={withCounters} />
|
||||
</div>
|
||||
<div className='status__action-bar__button-wrapper'>
|
||||
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={favouriteTitle} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||
|
||||
@@ -170,7 +170,7 @@ export const Footer: React.FC<{
|
||||
counter={status.get('replies_count') as number}
|
||||
/>
|
||||
|
||||
<BoostButton counters status={status} />
|
||||
<BoostButton counters statusId={statusId} />
|
||||
|
||||
<IconButton
|
||||
className='status__action-bar-button star-icon'
|
||||
|
||||
@@ -21,7 +21,8 @@ import { IconButton } from '../../../components/icon_button';
|
||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||
import { me, quickBoosting } from '../../../initial_state';
|
||||
import { BoostButton } from '@/mastodon/components/status/boost_button';
|
||||
import { quoteItemState, selectStatusState } from '@/mastodon/components/status/boost_button_utils';
|
||||
import { quoteItemState } from '@/mastodon/components/status/boost_button_utils';
|
||||
import { selectStatusConditions } from '@/mastodon/selectors/statuses';
|
||||
|
||||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
@@ -62,7 +63,7 @@ const mapStateToProps = (state, { status }) => {
|
||||
return ({
|
||||
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
|
||||
quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null,
|
||||
statusQuoteState: selectStatusState(state, status),
|
||||
statusQuoteState: selectStatusConditions(state, status.get('id')),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -330,7 +331,7 @@ class ActionBar extends PureComponent {
|
||||
<div className='detailed-status__action-bar'>
|
||||
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div>
|
||||
<div className='detailed-status__button'>
|
||||
<BoostButton status={status} />
|
||||
<BoostButton statusId={status.get('id')} />
|
||||
</div>
|
||||
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={favouriteTitle} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={bookmarkTitle} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>
|
||||
|
||||
10
app/javascript/mastodon/hooks/useStatus.ts
Normal file
10
app/javascript/mastodon/hooks/useStatus.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { selectExpandedStatus, selectPlainStatus } from '../selectors/statuses';
|
||||
import { useAppSelector } from '../store';
|
||||
|
||||
export function useStatus(id: string) {
|
||||
return useAppSelector((state) => selectPlainStatus(state, id));
|
||||
}
|
||||
|
||||
export function useExpandedStatus(id: string) {
|
||||
return useAppSelector((state) => selectExpandedStatus(state, id));
|
||||
}
|
||||
@@ -59,6 +59,11 @@ export const selectPlainAccount = createAppSelector(
|
||||
(account) => (account ? (account.toJS() as AccountShapeFull) : null),
|
||||
);
|
||||
|
||||
export const selectIsAccountLocal = createAppSelector(
|
||||
[selectPlainAccount],
|
||||
(account) => !!account && account.username === account.acct,
|
||||
);
|
||||
|
||||
export const getAccountHidden = createAppSelector(
|
||||
[
|
||||
(state, id: string) => state.accounts.get(id)?.hidden,
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
} from '@/mastodon/models/status';
|
||||
import { createAppSelector } from '@/mastodon/store/typed_functions';
|
||||
|
||||
import { selectPlainAccount } from './accounts';
|
||||
import { selectIsAccountLocal, selectPlainAccount } from './accounts';
|
||||
import type { FilterShape } from './filters';
|
||||
import { getFilters } from './filters';
|
||||
|
||||
@@ -83,16 +83,42 @@ export const selectStatusConditions = createAppSelector(
|
||||
state.statuses.getIn([statusId, 'quote', 'quoted_status']),
|
||||
'account',
|
||||
]) as string | undefined | null,
|
||||
(state, statusId: string) =>
|
||||
selectIsAccountLocal(
|
||||
state,
|
||||
state.statuses.getIn([statusId, 'account']) as string,
|
||||
),
|
||||
],
|
||||
(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',
|
||||
}),
|
||||
(status, userId, quotedAccountId, isLocal) => {
|
||||
const isPublic =
|
||||
!!status && ['public', 'unlisted'].includes(status.visibility);
|
||||
const isMine = status?.account === userId;
|
||||
const isMineAndPrivate =
|
||||
userId === status?.account && status.visibility === 'private';
|
||||
const quoteApproval = status?.quote_approval?.current_user;
|
||||
return {
|
||||
isPublic,
|
||||
isLocal,
|
||||
isLoggedIn: !!userId,
|
||||
isMine,
|
||||
isNotMine: !isMine,
|
||||
isQuoted: !!status && !!userId && quotedAccountId === userId,
|
||||
isNotDirect: !!status && status.visibility !== 'direct',
|
||||
isPrivateReblog:
|
||||
userId === status?.account && status.visibility === 'private',
|
||||
isBoosted: status?.reblogged ?? false,
|
||||
isBoostingAllowed: isPublic || isMineAndPrivate,
|
||||
isQuoteAutomaticallyAccepted:
|
||||
quoteApproval === 'automatic' && (isPublic || isMineAndPrivate),
|
||||
isQuoteManuallyAccepted:
|
||||
quoteApproval === 'manual' && (isPublic || isMineAndPrivate),
|
||||
isQuoteFollowersOnly:
|
||||
status?.quote_approval?.automatic[0] === 'followers' ||
|
||||
status?.quote_approval?.manual[0] === 'followers',
|
||||
};
|
||||
},
|
||||
);
|
||||
export type StatusConditions = ReturnType<typeof selectStatusConditions>;
|
||||
|
||||
export const selectStatusInteractions = createAppSelector(
|
||||
[(_, statusId: string) => statusId, selectStatusConditions],
|
||||
@@ -104,8 +130,15 @@ export const selectStatusInteractions = createAppSelector(
|
||||
};
|
||||
}
|
||||
|
||||
const { isLoggedIn, isMine, isPublic, isQuoted, isNotDirect } =
|
||||
conditionals;
|
||||
const {
|
||||
isLoggedIn,
|
||||
isMine,
|
||||
isNotMine,
|
||||
isPublic,
|
||||
isLocal,
|
||||
isQuoted,
|
||||
isNotDirect,
|
||||
} = conditionals;
|
||||
|
||||
const interactions: Record<
|
||||
StatusInteractionIntent,
|
||||
@@ -115,17 +148,17 @@ export const selectStatusInteractions = createAppSelector(
|
||||
delete: addAllowed({ isMine }),
|
||||
edit: addAllowed({ isMine }),
|
||||
editQuotePolicy: addAllowed({ isMine, isPublic }),
|
||||
embed: addAllowed({ isPublic }),
|
||||
embed: addAllowed({ isPublic, isLocal }),
|
||||
favourite: addAllowed({ isLoggedIn }),
|
||||
filter: addAllowed({ isLoggedIn }),
|
||||
filter: addAllowed({ isLoggedIn, isNotMine }),
|
||||
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 }),
|
||||
report: addAllowed({ isLoggedIn, isNotMine }),
|
||||
revokeQuote: addAllowed({ isQuoted, isNotMine }),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -134,6 +167,20 @@ export const selectStatusInteractions = createAppSelector(
|
||||
};
|
||||
},
|
||||
);
|
||||
export type StatusInteractions = ReturnType<typeof selectStatusInteractions>;
|
||||
|
||||
export const selectStatusInteractionsAllowed = createAppSelector(
|
||||
[selectStatusInteractions],
|
||||
({ statusId, ...interactions }) => ({
|
||||
statusId,
|
||||
...(Object.fromEntries(
|
||||
Object.entries(interactions).map(([key, { allowed }]) => [key, allowed]),
|
||||
) as Record<StatusInteractionIntent, boolean>),
|
||||
}),
|
||||
);
|
||||
export type StatusInteractionsAllowed = ReturnType<
|
||||
typeof selectStatusInteractionsAllowed
|
||||
>;
|
||||
|
||||
export const selectStatusIntentAllowed = createAppSelector(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user