From 1ec8e42dbbf43b0e69b9f966b9b857f3ecbb11c8 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 3 Nov 2025 14:56:07 +0100 Subject: [PATCH 1/7] [Glitch] Disable paste-link-to-quote flow when composing Private Mentions Port 6d53ca63d6fa84db527fab9b51b678c76bdf606e to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/actions/compose_typed.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/actions/compose_typed.ts b/app/javascript/flavours/glitch/actions/compose_typed.ts index 0b6b0f50de..fea0b3f71d 100644 --- a/app/javascript/flavours/glitch/actions/compose_typed.ts +++ b/app/javascript/flavours/glitch/actions/compose_typed.ts @@ -191,7 +191,8 @@ export const pasteLinkCompose = createDataLoadingThunk( composeState.get('is_submitting') || composeState.get('poll') || composeState.get('is_uploading') || - composeState.get('id') + composeState.get('id') || + composeState.get('privacy') === 'direct' ) return; From 0a8f96d3beed5e840cd87dca19a8b3a28a63c60a Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 4 Nov 2025 11:37:43 +0100 Subject: [PATCH 2/7] [Glitch] Remove option to disable access to local topic feeds for logged-in users Port dd708298a8c885ba53cc5528ad0e9158b58cd9ab to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/initial_state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/initial_state.ts b/app/javascript/flavours/glitch/initial_state.ts index 8f5d164f71..f00a296d8f 100644 --- a/app/javascript/flavours/glitch/initial_state.ts +++ b/app/javascript/flavours/glitch/initial_state.ts @@ -37,7 +37,7 @@ interface InitialStateMeta { streaming_api_base_url: string; local_live_feed_access: 'public' | 'authenticated' | 'disabled'; remote_live_feed_access: 'public' | 'authenticated' | 'disabled'; - local_topic_feed_access: 'public' | 'authenticated' | 'disabled'; + local_topic_feed_access: 'public' | 'authenticated'; remote_topic_feed_access: 'public' | 'authenticated' | 'disabled'; title: string; show_trends: boolean; From 1c1799041360937e01238db3a20b972b9f3cd318 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 4 Nov 2025 12:01:25 +0100 Subject: [PATCH 3/7] [Glitch] Fix quote dropdown menu item in detailed status view Port a978e37f4cb6813ad4af8eaf3f30e402b7f3760c to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/features/status/index.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index 072842ed3a..35299d8eed 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -329,6 +329,12 @@ class Status extends ImmutablePureComponent { dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } })); }; + handleQuote = (status) => { + const { dispatch } = this.props; + + dispatch(quoteComposeById(status.get('id'))); + }; + handleEditClick = (status) => { const { dispatch, askReplyConfirmation } = this.props; @@ -659,6 +665,7 @@ class Status extends ImmutablePureComponent { onDelete={this.handleDeleteClick} onRevokeQuote={this.handleRevokeQuoteClick} onQuotePolicyChange={this.handleQuotePolicyChange} + onQuote={this.handleQuote} onEdit={this.handleEditClick} onDirect={this.handleDirectClick} onMention={this.handleMentionClick} From 105a2d64a7e9b1114bc646e250ae3431ea451161 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 4 Nov 2025 16:30:10 +0100 Subject: [PATCH 4/7] [Glitch] Fix Skeleton placeholders being animated when setting to reduce animations is enabled Port 0b50789c5bfdf0225b4d716c11f0ea8977c66078 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/admin.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 69d6150865..3199358730 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1330,6 +1330,10 @@ a.sparkline { line-height: 1; width: 100%; animation: skeleton 1.2s ease-in-out infinite; + + .reduce-motion & { + animation: none; + } } @keyframes skeleton { From 949f15e2001214f627a1d7047714300f7057b74a Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 4 Nov 2025 17:32:52 +0100 Subject: [PATCH 5/7] [Glitch] Quote Posts: Add notifications for DMs and private posts Port 5bae08d1ff9962c2ab31400c06d1c264c8154460 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/actions/compose.js | 8 -- .../flavours/glitch/actions/compose_typed.ts | 37 +++++++- .../compose/components/compose_form.jsx | 6 +- .../compose/components/visibility_button.tsx | 6 +- .../containers/compose_form_container.js | 12 ++- .../containers/privacy_dropdown_container.js | 5 +- .../confirmation_modal.tsx | 4 + .../private_quote_notify.tsx | 88 +++++++++++++++++++ .../confirmation_modals/styles.module.css | 7 ++ .../features/ui/components/modal_root.jsx | 2 + .../ui/components/visibility_modal.tsx | 20 ++++- .../flavours/glitch/reducers/compose.js | 34 +++++-- .../flavours/glitch/styles/components.scss | 28 ++++++ 13 files changed, 230 insertions(+), 27 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/ui/components/confirmation_modals/private_quote_notify.tsx create mode 100644 app/javascript/flavours/glitch/features/ui/components/confirmation_modals/styles.module.css 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' && ( +
+ + +
+ )}