diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index 01b60e13e..72fc4f506 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -167,11 +167,13 @@ interface ComposeReplyAction { explicitAddressing: boolean; preserveSpoilers: boolean; rebloggedBy?: Pick; + approvalRequired?: boolean; } const replyCompose = ( status: ComposeReplyAction['status'], rebloggedBy?: ComposeReplyAction['rebloggedBy'], + approvalRequired?: ComposeReplyAction['approvalRequired'], ) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -190,6 +192,7 @@ const replyCompose = ( explicitAddressing, preserveSpoilers, rebloggedBy, + approvalRequired, }); useModalsStore.getState().openModal('COMPOSE'); }; diff --git a/packages/pl-fe/src/actions/interactions.ts b/packages/pl-fe/src/actions/interactions.ts index 2909fb1c2..d3fc89b83 100644 --- a/packages/pl-fe/src/actions/interactions.ts +++ b/packages/pl-fe/src/actions/interactions.ts @@ -78,6 +78,8 @@ const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL' as const; const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS' as const; const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL' as const; +const noOp = () => new Promise(f => f(undefined)); + type AccountListLink = () => Promise>; const messages = defineMessages({ @@ -90,7 +92,7 @@ const messages = defineMessages({ const reblog = (status: Pick) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; + if (!isLoggedIn(getState)) return noOp(); dispatch(reblogRequest(status.id)); @@ -106,7 +108,7 @@ const reblog = (status: Pick) => const unreblog = (status: Pick) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; + if (!isLoggedIn(getState)) return noOp(); dispatch(unreblogRequest(status.id)); @@ -117,14 +119,13 @@ const unreblog = (status: Pick) => }); }; -const toggleReblog = (status: Pick) => - (dispatch: AppDispatch) => { - if (status.reblogged) { - dispatch(unreblog(status)); - } else { - dispatch(reblog(status)); - } - }; +const toggleReblog = (status: Pick) => { + if (status.reblogged) { + return unreblog(status); + } else { + return reblog(status); + } +}; const reblogRequest = (statusId: string) => ({ type: REBLOG_REQUEST, @@ -162,7 +163,7 @@ const unreblogFail = (statusId: string, error: unknown) => ({ const favourite = (status: Pick) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; + if (!isLoggedIn(getState)) return noOp(); dispatch(favouriteRequest(status.id)); @@ -175,7 +176,7 @@ const favourite = (status: Pick) => const unfavourite = (status: Pick) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; + if (!isLoggedIn(getState)) return noOp(); dispatch(unfavouriteRequest(status.id)); @@ -186,14 +187,13 @@ const unfavourite = (status: Pick) => }); }; -const toggleFavourite = (status: Pick) => - (dispatch: AppDispatch) => { - if (status.favourited) { - dispatch(unfavourite(status)); - } else { - dispatch(favourite(status)); - } - }; +const toggleFavourite = (status: Pick) => { + if (status.favourited) { + return unfavourite(status); + } else { + return favourite(status); + } +}; const favouriteRequest = (statusId: string) => ({ type: FAVOURITE_REQUEST, diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 1061a9b06..21beb71aa 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -134,6 +134,9 @@ const messages = defineMessages({ replyInteractionPolicyFollowing: { id: 'status.interaction_policy.reply.following_only', defaultMessage: 'Only users followed by the author can reply.' }, replyInteractionPolicyMutuals: { id: 'status.interaction_policy.reply.mutuals_only', defaultMessage: 'Only users mutually following the author can reply.' }, replyInteractionPolicyMentioned: { id: 'status.interaction_policy.reply.mentioned_only', defaultMessage: 'Only users mentioned by the author can reply.' }, + + favouriteApprovalRequired: { id: 'status.interaction_policy.favourite.approval_required', defaultMessage: 'The author needs to approve your like.' }, + reblogApprovalRequired: { id: 'status.interaction_policy.reblog.approval_required', defaultMessage: 'The author needs to approve your repost.' }, }); interface IInteractionPopover { @@ -225,7 +228,7 @@ const ReplyButton: React.FC = ({ const handleReplyClick: React.MouseEventHandler = (e) => { if (me) { - dispatch(replyCompose(status, rebloggedBy)); + dispatch(replyCompose(status, rebloggedBy, canReply.approvalRequired || false)); } else { onOpenUnauthorizedModal('REPLY'); } @@ -292,7 +295,9 @@ const ReblogButton: React.FC = ({ const handleReblogClick: React.EventHandler = e => { if (me) { - const modalReblog = () => dispatch(toggleReblog(status)); + const modalReblog = () => dispatch(toggleReblog(status)).then(() => { + if (canReblog.approvalRequired) toast.info(messages.reblogApprovalRequired); + }); if ((e && e.shiftKey) || !boostModal) { modalReblog(); } else { @@ -378,7 +383,9 @@ const FavouriteButton: React.FC = ({ const handleFavouriteClick: React.EventHandler = (e) => { if (me) { - dispatch(toggleFavourite(status)); + dispatch(toggleFavourite(status)).then(() => { + if (canFavourite.approvalRequired) toast.info(messages.favouriteApprovalRequired); + }).catch(() => {}); } else { onOpenUnauthorizedModal('FAVOURITE'); } @@ -411,6 +418,7 @@ const FavouriteButton: React.FC = ({ {favouriteButton} ); + return favouriteButton; }; const DislikeButton: React.FC = ({ diff --git a/packages/pl-fe/src/features/compose/components/compose-form.tsx b/packages/pl-fe/src/features/compose/components/compose-form.tsx index 6d443f505..4e20fdee6 100644 --- a/packages/pl-fe/src/features/compose/components/compose-form.tsx +++ b/packages/pl-fe/src/features/compose/components/compose-form.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor } from 'lexical'; import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import { length } from 'stringz'; @@ -45,6 +45,7 @@ import SpoilerInput from './spoiler-input'; import TextCharacterCounter from './text-character-counter'; import UploadForm from './upload-form'; import VisualCharacterCounter from './visual-character-counter'; +import Warning from './warning'; import type { AutoSuggestion } from 'pl-fe/components/autosuggest-input'; import type { Emoji } from 'pl-fe/features/emoji'; @@ -227,6 +228,14 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab return ( + {!!compose.in_reply_to && compose.approvalRequired && ( + + )} + /> + )} + {!shouldCondense && !event && !group && groupId && } diff --git a/packages/pl-fe/src/features/compose/components/warning.tsx b/packages/pl-fe/src/features/compose/components/warning.tsx index 1ad5fc266..5d7fabfb0 100644 --- a/packages/pl-fe/src/features/compose/components/warning.tsx +++ b/packages/pl-fe/src/features/compose/components/warning.tsx @@ -11,7 +11,7 @@ interface IWarning { const Warning: React.FC = ({ message }) => ( {({ opacity, scaleX, scaleY }) => ( -
+
{message}
)} diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 51dba15d6..5fd1ff288 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -455,6 +455,7 @@ "compose_event.tabs.pending": "Manage requests", "compose_event.update": "Update", "compose_event.upload_banner": "Upload event banner", + "compose_form.approval_required": "The reply needs to be approved by the post author.", "compose_form.content_type.change": "Change content type", "compose_form.direct_message_warning": "This post will only be sent to the mentioned users.", "compose_form.event_placeholder": "Post to this event", @@ -1459,11 +1460,13 @@ "status.group": "Posted in {group}", "status.group_mod_delete": "Delete post from group", "status.hide_translation": "Hide translation", + "status.interaction_policy.favourite.approval_required": "The author needs to approve your like.", "status.interaction_policy.favourite.followers_only": "Only users following the author can like.", "status.interaction_policy.favourite.following_only": "Only users followed by the author can like.", "status.interaction_policy.favourite.header": "The author limits who can like this post.", "status.interaction_policy.favourite.mentioned_only": "Only users mentioned by the author can like.", "status.interaction_policy.favourite.mutuals_only": "Only users mutually following the author can like.", + "status.interaction_policy.reblog.approval_required": "The author needs to approve your repost.", "status.interaction_policy.reblog.followers_only": "Only users following the author can repost.", "status.interaction_policy.reblog.following_only": "Only users followed by the author can repost.", "status.interaction_policy.reblog.header": "The author limits who can repost this post.", diff --git a/packages/pl-fe/src/reducers/compose.ts b/packages/pl-fe/src/reducers/compose.ts index 928a22086..5d7c4597c 100644 --- a/packages/pl-fe/src/reducers/compose.ts +++ b/packages/pl-fe/src/reducers/compose.ts @@ -120,6 +120,7 @@ const ReducerCompose = ImmutableRecord({ modified_language: null as Language | null, suggested_language: null as string | null, federated: true, + approvalRequired: false, }); type State = ImmutableMap; @@ -345,6 +346,7 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In map.set('caretPosition', null); map.set('idempotencyKey', crypto.randomUUID()); map.set('content_type', defaultCompose.content_type); + map.set('approvalRequired', action.approvalRequired || false); if (action.preserveSpoilers && action.status.spoiler_text) { map.set('sensitive', true); map.set('spoiler_text', action.status.spoiler_text);