pl-fe: required approval notifications
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
0c84346c3a
commit
3b0cd07e1d
7 changed files with 50 additions and 25 deletions
|
@ -167,11 +167,13 @@ interface ComposeReplyAction {
|
||||||
explicitAddressing: boolean;
|
explicitAddressing: boolean;
|
||||||
preserveSpoilers: boolean;
|
preserveSpoilers: boolean;
|
||||||
rebloggedBy?: Pick<Account, 'acct' | 'id'>;
|
rebloggedBy?: Pick<Account, 'acct' | 'id'>;
|
||||||
|
approvalRequired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyCompose = (
|
const replyCompose = (
|
||||||
status: ComposeReplyAction['status'],
|
status: ComposeReplyAction['status'],
|
||||||
rebloggedBy?: ComposeReplyAction['rebloggedBy'],
|
rebloggedBy?: ComposeReplyAction['rebloggedBy'],
|
||||||
|
approvalRequired?: ComposeReplyAction['approvalRequired'],
|
||||||
) =>
|
) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -190,6 +192,7 @@ const replyCompose = (
|
||||||
explicitAddressing,
|
explicitAddressing,
|
||||||
preserveSpoilers,
|
preserveSpoilers,
|
||||||
rebloggedBy,
|
rebloggedBy,
|
||||||
|
approvalRequired,
|
||||||
});
|
});
|
||||||
useModalsStore.getState().openModal('COMPOSE');
|
useModalsStore.getState().openModal('COMPOSE');
|
||||||
};
|
};
|
||||||
|
|
|
@ -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_SUCCESS = 'REBLOGS_EXPAND_SUCCESS' as const;
|
||||||
const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL' as const;
|
const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL' as const;
|
||||||
|
|
||||||
|
const noOp = () => new Promise(f => f(undefined));
|
||||||
|
|
||||||
type AccountListLink = () => Promise<PaginatedResponse<Account>>;
|
type AccountListLink = () => Promise<PaginatedResponse<Account>>;
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -90,7 +92,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const reblog = (status: Pick<Status, 'id'>) =>
|
const reblog = (status: Pick<Status, 'id'>) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return noOp();
|
||||||
|
|
||||||
dispatch(reblogRequest(status.id));
|
dispatch(reblogRequest(status.id));
|
||||||
|
|
||||||
|
@ -106,7 +108,7 @@ const reblog = (status: Pick<Status, 'id'>) =>
|
||||||
|
|
||||||
const unreblog = (status: Pick<Status, 'id'>) =>
|
const unreblog = (status: Pick<Status, 'id'>) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return noOp();
|
||||||
|
|
||||||
dispatch(unreblogRequest(status.id));
|
dispatch(unreblogRequest(status.id));
|
||||||
|
|
||||||
|
@ -117,14 +119,13 @@ const unreblog = (status: Pick<Status, 'id'>) =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleReblog = (status: Pick<Status, 'id' | 'reblogged'>) =>
|
const toggleReblog = (status: Pick<Status, 'id' | 'reblogged'>) => {
|
||||||
(dispatch: AppDispatch) => {
|
if (status.reblogged) {
|
||||||
if (status.reblogged) {
|
return unreblog(status);
|
||||||
dispatch(unreblog(status));
|
} else {
|
||||||
} else {
|
return reblog(status);
|
||||||
dispatch(reblog(status));
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const reblogRequest = (statusId: string) => ({
|
const reblogRequest = (statusId: string) => ({
|
||||||
type: REBLOG_REQUEST,
|
type: REBLOG_REQUEST,
|
||||||
|
@ -162,7 +163,7 @@ const unreblogFail = (statusId: string, error: unknown) => ({
|
||||||
|
|
||||||
const favourite = (status: Pick<Status, 'id'>) =>
|
const favourite = (status: Pick<Status, 'id'>) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return noOp();
|
||||||
|
|
||||||
dispatch(favouriteRequest(status.id));
|
dispatch(favouriteRequest(status.id));
|
||||||
|
|
||||||
|
@ -175,7 +176,7 @@ const favourite = (status: Pick<Status, 'id'>) =>
|
||||||
|
|
||||||
const unfavourite = (status: Pick<Status, 'id'>) =>
|
const unfavourite = (status: Pick<Status, 'id'>) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return noOp();
|
||||||
|
|
||||||
dispatch(unfavouriteRequest(status.id));
|
dispatch(unfavouriteRequest(status.id));
|
||||||
|
|
||||||
|
@ -186,14 +187,13 @@ const unfavourite = (status: Pick<Status, 'id'>) =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFavourite = (status: Pick<Status, 'id' | 'favourited'>) =>
|
const toggleFavourite = (status: Pick<Status, 'id' | 'favourited'>) => {
|
||||||
(dispatch: AppDispatch) => {
|
if (status.favourited) {
|
||||||
if (status.favourited) {
|
return unfavourite(status);
|
||||||
dispatch(unfavourite(status));
|
} else {
|
||||||
} else {
|
return favourite(status);
|
||||||
dispatch(favourite(status));
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const favouriteRequest = (statusId: string) => ({
|
const favouriteRequest = (statusId: string) => ({
|
||||||
type: FAVOURITE_REQUEST,
|
type: FAVOURITE_REQUEST,
|
||||||
|
|
|
@ -134,6 +134,9 @@ const messages = defineMessages({
|
||||||
replyInteractionPolicyFollowing: { id: 'status.interaction_policy.reply.following_only', defaultMessage: 'Only users followed by the author can reply.' },
|
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.' },
|
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.' },
|
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 {
|
interface IInteractionPopover {
|
||||||
|
@ -225,7 +228,7 @@ const ReplyButton: React.FC<IReplyButton> = ({
|
||||||
|
|
||||||
const handleReplyClick: React.MouseEventHandler = (e) => {
|
const handleReplyClick: React.MouseEventHandler = (e) => {
|
||||||
if (me) {
|
if (me) {
|
||||||
dispatch(replyCompose(status, rebloggedBy));
|
dispatch(replyCompose(status, rebloggedBy, canReply.approvalRequired || false));
|
||||||
} else {
|
} else {
|
||||||
onOpenUnauthorizedModal('REPLY');
|
onOpenUnauthorizedModal('REPLY');
|
||||||
}
|
}
|
||||||
|
@ -292,7 +295,9 @@ const ReblogButton: React.FC<IReblogButton> = ({
|
||||||
|
|
||||||
const handleReblogClick: React.EventHandler<React.MouseEvent> = e => {
|
const handleReblogClick: React.EventHandler<React.MouseEvent> = e => {
|
||||||
if (me) {
|
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) {
|
if ((e && e.shiftKey) || !boostModal) {
|
||||||
modalReblog();
|
modalReblog();
|
||||||
} else {
|
} else {
|
||||||
|
@ -378,7 +383,9 @@ const FavouriteButton: React.FC<IActionButton> = ({
|
||||||
|
|
||||||
const handleFavouriteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleFavouriteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
if (me) {
|
if (me) {
|
||||||
dispatch(toggleFavourite(status));
|
dispatch(toggleFavourite(status)).then(() => {
|
||||||
|
if (canFavourite.approvalRequired) toast.info(messages.favouriteApprovalRequired);
|
||||||
|
}).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
onOpenUnauthorizedModal('FAVOURITE');
|
onOpenUnauthorizedModal('FAVOURITE');
|
||||||
}
|
}
|
||||||
|
@ -411,6 +418,7 @@ const FavouriteButton: React.FC<IActionButton> = ({
|
||||||
{favouriteButton}
|
{favouriteButton}
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
return favouriteButton;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DislikeButton: React.FC<IActionButton> = ({
|
const DislikeButton: React.FC<IActionButton> = ({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor } from 'lexical';
|
import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor } from 'lexical';
|
||||||
import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
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 { useHistory } from 'react-router-dom';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ import SpoilerInput from './spoiler-input';
|
||||||
import TextCharacterCounter from './text-character-counter';
|
import TextCharacterCounter from './text-character-counter';
|
||||||
import UploadForm from './upload-form';
|
import UploadForm from './upload-form';
|
||||||
import VisualCharacterCounter from './visual-character-counter';
|
import VisualCharacterCounter from './visual-character-counter';
|
||||||
|
import Warning from './warning';
|
||||||
|
|
||||||
import type { AutoSuggestion } from 'pl-fe/components/autosuggest-input';
|
import type { AutoSuggestion } from 'pl-fe/components/autosuggest-input';
|
||||||
import type { Emoji } from 'pl-fe/features/emoji';
|
import type { Emoji } from 'pl-fe/features/emoji';
|
||||||
|
@ -227,6 +228,14 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
<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} />
|
<WarningContainer composeId={id} />
|
||||||
|
|
||||||
{!shouldCondense && !event && !group && groupId && <ReplyGroupIndicator composeId={id} />}
|
{!shouldCondense && !event && !group && groupId && <ReplyGroupIndicator composeId={id} />}
|
||||||
|
|
|
@ -11,7 +11,7 @@ interface IWarning {
|
||||||
const Warning: React.FC<IWarning> = ({ message }) => (
|
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 }) }}>
|
<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 }) => (
|
{({ 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}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -455,6 +455,7 @@
|
||||||
"compose_event.tabs.pending": "Manage requests",
|
"compose_event.tabs.pending": "Manage requests",
|
||||||
"compose_event.update": "Update",
|
"compose_event.update": "Update",
|
||||||
"compose_event.upload_banner": "Upload event banner",
|
"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.content_type.change": "Change content type",
|
||||||
"compose_form.direct_message_warning": "This post will only be sent to the mentioned users.",
|
"compose_form.direct_message_warning": "This post will only be sent to the mentioned users.",
|
||||||
"compose_form.event_placeholder": "Post to this event",
|
"compose_form.event_placeholder": "Post to this event",
|
||||||
|
@ -1459,11 +1460,13 @@
|
||||||
"status.group": "Posted in {group}",
|
"status.group": "Posted in {group}",
|
||||||
"status.group_mod_delete": "Delete post from group",
|
"status.group_mod_delete": "Delete post from group",
|
||||||
"status.hide_translation": "Hide translation",
|
"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.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.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.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.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.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.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.following_only": "Only users followed by the author can repost.",
|
||||||
"status.interaction_policy.reblog.header": "The author limits who can repost this post.",
|
"status.interaction_policy.reblog.header": "The author limits who can repost this post.",
|
||||||
|
|
|
@ -120,6 +120,7 @@ const ReducerCompose = ImmutableRecord({
|
||||||
modified_language: null as Language | null,
|
modified_language: null as Language | null,
|
||||||
suggested_language: null as string | null,
|
suggested_language: null as string | null,
|
||||||
federated: true,
|
federated: true,
|
||||||
|
approvalRequired: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
type State = ImmutableMap<string, Compose>;
|
type State = ImmutableMap<string, Compose>;
|
||||||
|
@ -345,6 +346,7 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
||||||
map.set('caretPosition', null);
|
map.set('caretPosition', null);
|
||||||
map.set('idempotencyKey', crypto.randomUUID());
|
map.set('idempotencyKey', crypto.randomUUID());
|
||||||
map.set('content_type', defaultCompose.content_type);
|
map.set('content_type', defaultCompose.content_type);
|
||||||
|
map.set('approvalRequired', action.approvalRequired || false);
|
||||||
if (action.preserveSpoilers && action.status.spoiler_text) {
|
if (action.preserveSpoilers && action.status.spoiler_text) {
|
||||||
map.set('sensitive', true);
|
map.set('sensitive', true);
|
||||||
map.set('spoiler_text', action.status.spoiler_text);
|
map.set('spoiler_text', action.status.spoiler_text);
|
||||||
|
|
Loading…
Reference in a new issue