pl-fe: required approval notifications

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-10-30 21:28:33 +01:00
parent 0c84346c3a
commit 3b0cd07e1d
7 changed files with 50 additions and 25 deletions

View file

@ -167,11 +167,13 @@ interface ComposeReplyAction {
explicitAddressing: boolean;
preserveSpoilers: boolean;
rebloggedBy?: Pick<Account, 'acct' | 'id'>;
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');
};

View file

@ -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<PaginatedResponse<Account>>;
const messages = defineMessages({
@ -90,7 +92,7 @@ const messages = defineMessages({
const reblog = (status: Pick<Status, 'id'>) =>
(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<Status, 'id'>) =>
const unreblog = (status: Pick<Status, 'id'>) =>
(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<Status, 'id'>) =>
});
};
const toggleReblog = (status: Pick<Status, 'id' | 'reblogged'>) =>
(dispatch: AppDispatch) => {
if (status.reblogged) {
dispatch(unreblog(status));
} else {
dispatch(reblog(status));
}
};
const toggleReblog = (status: Pick<Status, 'id' | 'reblogged'>) => {
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<Status, 'id'>) =>
(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<Status, 'id'>) =>
const unfavourite = (status: Pick<Status, 'id'>) =>
(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<Status, 'id'>) =>
});
};
const toggleFavourite = (status: Pick<Status, 'id' | 'favourited'>) =>
(dispatch: AppDispatch) => {
if (status.favourited) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
};
const toggleFavourite = (status: Pick<Status, 'id' | 'favourited'>) => {
if (status.favourited) {
return unfavourite(status);
} else {
return favourite(status);
}
};
const favouriteRequest = (statusId: string) => ({
type: FAVOURITE_REQUEST,

View file

@ -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<IReplyButton> = ({
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<IReblogButton> = ({
const handleReblogClick: React.EventHandler<React.MouseEvent> = 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<IActionButton> = ({
const handleFavouriteClick: React.EventHandler<React.MouseEvent> = (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<IActionButton> = ({
{favouriteButton}
</Popover>
);
return favouriteButton;
};
const DislikeButton: React.FC<IActionButton> = ({

View file

@ -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 extends string>({ id, shouldCondense, autoFocus, clickab
return (
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
{!!compose.in_reply_to && compose.approvalRequired && (
<Warning
message={(
<FormattedMessage id='compose_form.approval_required' defaultMessage='The reply needs to be approved by the post author.' />
)}
/>
)}
<WarningContainer composeId={id} />
{!shouldCondense && !event && !group && groupId && <ReplyGroupIndicator composeId={id} />}

View file

@ -11,7 +11,7 @@ interface IWarning {
const Warning: React.FC<IWarning> = ({ message }) => (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => (
<div className='mb-2.5 rounded border border-solid border-gray-400 bg-transparent px-2.5 py-2 text-xs text-gray-900 dark:border-gray-800 dark:text-white' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
<div className='rounded border border-solid border-gray-400 bg-transparent px-2.5 py-2 text-xs text-gray-900 dark:border-gray-800 dark:text-white' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
{message}
</div>
)}

View file

@ -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.",

View file

@ -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<string, Compose>;
@ -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);