diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index 09d6e7bbb3..3553378b44 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -58,7 +58,6 @@ export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE'
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
-export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
@@ -825,13 +824,6 @@ export function changeComposeSpoilerText(text) {
};
}
-export function changeComposeVisibility(value) {
- return {
- type: COMPOSE_VISIBILITY_CHANGE,
- value,
- };
-}
-
export function insertEmojiCompose(position, emoji, needsSpace) {
return {
type: COMPOSE_EMOJI_INSERT,
diff --git a/app/javascript/flavours/glitch/actions/compose_typed.ts b/app/javascript/flavours/glitch/actions/compose_typed.ts
index fea0b3f71d..e7cefc3fb7 100644
--- a/app/javascript/flavours/glitch/actions/compose_typed.ts
+++ b/app/javascript/flavours/glitch/actions/compose_typed.ts
@@ -13,10 +13,10 @@ import {
} from 'flavours/glitch/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
-import type { Status } from '../models/status';
+import type { Status, StatusVisibility } from '../models/status';
import { showAlert } from './alerts';
-import { focusCompose } from './compose';
+import { changeCompose, focusCompose } from './compose';
import { importFetchedStatuses } from './importer';
import { openModal } from './modal';
@@ -67,6 +67,39 @@ const simulateModifiedApiResponse = (
return data;
};
+export const changeComposeVisibility = createAppThunk(
+ 'compose/visibility_change',
+ (visibility: StatusVisibility, { dispatch, getState }) => {
+ if (visibility !== 'direct') {
+ return visibility;
+ }
+
+ const state = getState();
+ const quotedStatusId = state.compose.get('quoted_status_id') as
+ | string
+ | null;
+ if (!quotedStatusId) {
+ return visibility;
+ }
+
+ // Remove the quoted status
+ dispatch(quoteComposeCancel());
+ const quotedStatus = state.statuses.get(quotedStatusId) as Status | null;
+ if (!quotedStatus) {
+ return visibility;
+ }
+
+ // Append the quoted status URL to the compose text
+ const url = quotedStatus.get('url') as string;
+ const text = state.compose.get('text') as string;
+ if (!text.includes(url)) {
+ const newText = text.trim() ? `${text}\n\n${url}` : url;
+ dispatch(changeCompose(newText));
+ }
+ return visibility;
+ },
+);
+
export const changeUploadCompose = createDataLoadingThunk(
'compose/changeUpload',
async (
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx
index 42fbeb3a33..60ff881604 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx
@@ -155,7 +155,11 @@ class ComposeForm extends ImmutablePureComponent {
return;
}
- this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct', overridePrivacy);
+ this.props.onSubmit({
+ missingAltTextModal: missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct',
+ quoteToPrivate: this.props.quoteToPrivate,
+ overridePrivacy,
+ });
if (e) {
e.preventDefault();
diff --git a/app/javascript/flavours/glitch/features/compose/components/visibility_button.tsx b/app/javascript/flavours/glitch/features/compose/components/visibility_button.tsx
index 1e6462ecd3..07f4815e02 100644
--- a/app/javascript/flavours/glitch/features/compose/components/visibility_button.tsx
+++ b/app/javascript/flavours/glitch/features/compose/components/visibility_button.tsx
@@ -5,8 +5,10 @@ import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
-import { changeComposeVisibility } from '@/flavours/glitch/actions/compose';
-import { setComposeQuotePolicy } from '@/flavours/glitch/actions/compose_typed';
+import {
+ changeComposeVisibility,
+ setComposeQuotePolicy,
+} from '@/flavours/glitch/actions/compose_typed';
import { openModal } from '@/flavours/glitch/actions/modal';
import type { ApiQuotePolicy } from '@/flavours/glitch/api_types/quotes';
import type { StatusVisibility } from '@/flavours/glitch/api_types/statuses';
diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
index fd7186d71a..eb9efb2e32 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
@@ -12,6 +12,7 @@ import {
} from 'flavours/glitch/actions/compose';
import { pasteLinkCompose } from 'flavours/glitch/actions/compose_typed';
import { openModal } from 'flavours/glitch/actions/modal';
+import { PRIVATE_QUOTE_MODAL_ID } from 'flavours/glitch/features/ui/components/confirmation_modals/private_quote_notify';
import { privacyPreference } from 'flavours/glitch/utils/privacy_preference';
import ComposeForm from '../components/compose_form';
@@ -52,6 +53,10 @@ const mapStateToProps = state => ({
isUploading: state.getIn(['compose', 'is_uploading']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
missingAltText: state.getIn(['compose', 'media_attachments']).some(media => ['image', 'gifv'].includes(media.get('type')) && (media.get('description') ?? '').length === 0),
+ quoteToPrivate:
+ !!state.getIn(['compose', 'quoted_status_id'])
+ && state.getIn(['compose', 'privacy']) === 'private'
+ && !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),
sideArm: sideArmPrivacy(state),
@@ -65,12 +70,17 @@ const mapDispatchToProps = (dispatch, props) => ({
dispatch(changeCompose(text));
},
- onSubmit (missingAltText, overridePrivacy = null) {
+ onSubmit ({ missingAltText, quoteToPrivate, overridePrivacy = null }) {
if (missingAltText) {
dispatch(openModal({
modalType: 'CONFIRM_MISSING_ALT_TEXT',
modalProps: { overridePrivacy },
}));
+ } else if (quoteToPrivate) {
+ dispatch(openModal({
+ modalType: 'CONFIRM_PRIVATE_QUOTE_NOTIFY',
+ modalProps: {},
+ }));
} else {
dispatch(submitCompose(overridePrivacy, (status) => {
if (props.redirectOnSuccess) {
diff --git a/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js
index 6d3eef13aa..a44b5c0d97 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js
@@ -1,8 +1,7 @@
import { connect } from 'react-redux';
-import { changeComposeVisibility } from '../../../actions/compose';
-import { openModal, closeModal } from '../../../actions/modal';
-import { isUserTouching } from '../../../is_mobile';
+import { changeComposeVisibility } from '@/flavours/glitch/actions/compose_typed';
+
import PrivacyDropdown from '../components/privacy_dropdown';
const mapStateToProps = state => ({
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal.tsx
index 065ffc9d7c..cf22159073 100644
--- a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal.tsx
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal.tsx
@@ -26,6 +26,7 @@ export const ConfirmationModal: React.FC<
onSecondary?: () => void;
onConfirm: () => void;
closeWhenConfirm?: boolean;
+ extraContent?: React.ReactNode;
} & BaseConfirmationModalProps
> = ({
title,
@@ -37,6 +38,7 @@ export const ConfirmationModal: React.FC<
secondary,
onSecondary,
closeWhenConfirm = true,
+ extraContent,
}) => {
const handleClick = useCallback(() => {
if (closeWhenConfirm) {
@@ -57,6 +59,8 @@ export const ConfirmationModal: React.FC<
{title}
{message &&
{message}
}
+
+ {extraContent}
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/private_quote_notify.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/private_quote_notify.tsx
new file mode 100644
index 0000000000..c461e1e43e
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/private_quote_notify.tsx
@@ -0,0 +1,88 @@
+import { forwardRef, useCallback, useState } from 'react';
+
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+
+import { submitCompose } from '@/flavours/glitch/actions/compose';
+import { changeSetting } from '@/flavours/glitch/actions/settings';
+import { CheckBox } from '@/flavours/glitch/components/check_box';
+import { useAppDispatch } from '@/flavours/glitch/store';
+
+import { ConfirmationModal } from './confirmation_modal';
+import type { BaseConfirmationModalProps } from './confirmation_modal';
+import classes from './styles.module.css';
+
+export const PRIVATE_QUOTE_MODAL_ID = 'quote/private_notify';
+
+const messages = defineMessages({
+ title: {
+ id: 'confirmations.private_quote_notify.title',
+ defaultMessage: 'Share with followers and mentioned users?',
+ },
+ message: {
+ id: 'confirmations.private_quote_notify.message',
+ defaultMessage:
+ 'The person you are quoting and other mentions ' +
+ "will be notified and will be able to view your post, even if they're not following you.",
+ },
+ confirm: {
+ id: 'confirmations.private_quote_notify.confirm',
+ defaultMessage: 'Publish post',
+ },
+ cancel: {
+ id: 'confirmations.private_quote_notify.cancel',
+ defaultMessage: 'Back to editing',
+ },
+});
+
+export const PrivateQuoteNotify = forwardRef<
+ HTMLDivElement,
+ BaseConfirmationModalProps
+>(
+ (
+ { onClose },
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _ref,
+ ) => {
+ const intl = useIntl();
+
+ const [dismiss, setDismissed] = useState(false);
+ const handleDismissToggle = useCallback(() => {
+ setDismissed((prev) => !prev);
+ }, []);
+
+ const dispatch = useAppDispatch();
+ const handleConfirm = useCallback(() => {
+ dispatch(submitCompose());
+ if (dismiss) {
+ dispatch(
+ changeSetting(['dismissed_banners', PRIVATE_QUOTE_MODAL_ID], true),
+ );
+ }
+ }, [dismiss, dispatch]);
+
+ return (
+
+ {' '}
+
+
+ }
+ />
+ );
+ },
+);
+PrivateQuoteNotify.displayName = 'PrivateQuoteNotify';
diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/styles.module.css b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/styles.module.css
new file mode 100644
index 0000000000..f685c4525f
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/styles.module.css
@@ -0,0 +1,7 @@
+.checkbox_wrapper {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 1rem 0;
+ cursor: pointer;
+}
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx
index 4f2e63fb0d..9ba2970f93 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx
@@ -51,6 +51,7 @@ import MediaModal from './media_modal';
import { ModalPlaceholder } from './modal_placeholder';
import VideoModal from './video_modal';
import { VisibilityModal } from './visibility_modal';
+import { PrivateQuoteNotify } from './confirmation_modals/private_quote_notify';
export const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
@@ -72,6 +73,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
+ 'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
'CONFIRM_QUIET_QUOTE': () => Promise.resolve({ default: QuietPostQuoteInfoModal }),
'MUTE': MuteModal,
diff --git a/app/javascript/flavours/glitch/features/ui/components/visibility_modal.tsx b/app/javascript/flavours/glitch/features/ui/components/visibility_modal.tsx
index 9edd061e9f..4184c84c4b 100644
--- a/app/javascript/flavours/glitch/features/ui/components/visibility_modal.tsx
+++ b/app/javascript/flavours/glitch/features/ui/components/visibility_modal.tsx
@@ -128,9 +128,12 @@ export const VisibilityModal: FC = forwardRef(
const disableVisibility = !!statusId;
const disableQuotePolicy =
visibility === 'private' || visibility === 'direct';
- const disablePublicVisibilities: boolean = useAppSelector(
+ const disablePublicVisibilities = useAppSelector(
selectDisablePublicVisibilities,
);
+ const isQuotePost = useAppSelector(
+ (state) => state.compose.get('quoted_status_id') !== null,
+ );
const visibilityItems = useMemo[]>(() => {
const items: SelectItem[] = [
@@ -315,6 +318,21 @@ export const VisibilityModal: FC = forwardRef(
id={quoteDescriptionId}
/>
+
+ {isQuotePost && visibility === 'direct' && (
+
+
+
+
+ )}