From bb131c994569a8fbb139054be656f107dda0abe3 Mon Sep 17 00:00:00 2001 From: Cytrogen Date: Tue, 31 Mar 2026 23:31:05 -0400 Subject: [PATCH] [feature] Quote post UI --- .../flavours/glitch/actions/compose.js | 20 ++++++ .../flavours/glitch/actions/importer/index.js | 4 ++ .../glitch/actions/importer/normalizer.js | 5 ++ .../glitch/components/quoted_status.jsx | 63 +++++++++++++++++++ .../flavours/glitch/components/status.jsx | 5 ++ .../glitch/components/status_action_bar.jsx | 11 ++++ .../glitch/containers/status_container.js | 5 ++ .../compose/components/compose_form.jsx | 2 + .../containers/quote_indicator_container.js | 26 ++++++++ .../flavours/glitch/reducers/compose.js | 17 +++++ .../glitch/styles/components/status.css | 35 +++++++++++ 11 files changed, 193 insertions(+) create mode 100644 app/javascript/flavours/glitch/components/quoted_status.jsx create mode 100644 app/javascript/flavours/glitch/features/compose/containers/quote_indicator_container.js diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 53520bada6553cb4826143aae90b678fac67bcf9..9af13b9c7c2d2e90da4c2c58f3352d94fcb6bccd 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -28,6 +28,8 @@ export const COMPOSE_SUBMIT_SUCCESS = "COMPOSE_SUBMIT_SUCCESS"; export const COMPOSE_SUBMIT_FAIL = "COMPOSE_SUBMIT_FAIL"; export const COMPOSE_REPLY = "COMPOSE_REPLY"; export const COMPOSE_REPLY_CANCEL = "COMPOSE_REPLY_CANCEL"; +export const COMPOSE_QUOTE = "COMPOSE_QUOTE"; +export const COMPOSE_QUOTE_CANCEL = "COMPOSE_QUOTE_CANCEL"; export const COMPOSE_DIRECT = "COMPOSE_DIRECT"; export const COMPOSE_MENTION = "COMPOSE_MENTION"; export const COMPOSE_RESET = "COMPOSE_RESET"; @@ -142,6 +144,23 @@ export function cancelReplyCompose() { }; } +export function quoteCompose(status, routerHistory) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_QUOTE, + status: status, + }); + + ensureComposeIsVisible(getState, routerHistory); + }; +} + +export function cancelQuoteCompose() { + return { + type: COMPOSE_QUOTE_CANCEL, + }; +} + export function resetCompose() { return { type: COMPOSE_RESET, @@ -216,6 +235,7 @@ export function submitCompose(routerHistory) { sensitive: getState().getIn(["compose", "sensitive"]) || (spoilerText.length > 0 && media.size !== 0), spoiler_text: spoilerText, visibility: getState().getIn(["compose", "privacy"]), + quote_id: getState().getIn(["compose", "quote_id"], null), poll: getState().getIn(["compose", "poll"], null), language: getState().getIn(["compose", "language"]), local_only: getState().getIn(["compose", "advanced_options", "do_not_federate"]), diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index d3601c8106eca559aeadb1649141b61a527878e5..50e1769c7775e65f9391e28c6e9039095c192970 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -80,6 +80,10 @@ export function importFetchedStatuses(statuses) { processStatus(status.reblog); } + if (status.quote && status.quote.id) { + processStatus(status.quote); + } + if (status.poll && status.poll.id) { pushUnique(polls, normalizePoll(status.poll, getState().getIn(["polls", status.poll.id]))); } diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 9ad1804d8c173ba7396e8ace5687e4ffd7207311..62e2d1c4171779764d8c9d6073521d58ceab41be 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -59,6 +59,11 @@ export function normalizeStatus(status, normalOldStatus, settings) { normalStatus.reblog = status.reblog.id; } + if (status.quote && status.quote.id) { + normalStatus.quote = status.quote.id; + normalStatus.quote_id = status.quote_id; + } + if (status.poll && status.poll.id) { normalStatus.poll = status.poll.id; } diff --git a/app/javascript/flavours/glitch/components/quoted_status.jsx b/app/javascript/flavours/glitch/components/quoted_status.jsx new file mode 100644 index 0000000000000000000000000000000000000000..eece596d457ade88ca3ddfcff4cf15ae95a70c75 --- /dev/null +++ b/app/javascript/flavours/glitch/components/quoted_status.jsx @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; + +import { makeGetStatus } from 'flavours/glitch/selectors'; + +import { Avatar } from './avatar'; +import { DisplayName } from './display_name'; +import StatusContent from './status_content'; +import Permalink from './permalink'; + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, { statusId }) => ({ + status: getStatus(state, { id: statusId }), + }); + + return mapStateToProps; +}; + +class QuotedStatus extends PureComponent { + + static propTypes = { + statusId: PropTypes.string.isRequired, + status: ImmutablePropTypes.map, + }; + + render () { + const { status } = this.props; + + if (!status) { + return ( +
+

This post is unavailable.

+
+ ); + } + + const account = status.get('account'); + + return ( +
+
+ + + + +
+ +
+ ); + } + +} + +export default connect(makeMapStateToProps)(QuotedStatus); diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index bfe4ebdb599061e40d5373dab7d8874536c346c6..18ec2f9f1aa899da5c7f3b8eaed7ed74b7988010 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -25,6 +25,7 @@ import StatusContent from "./status_content"; import StatusHeader from "./status_header"; import StatusIcons from "./status_icons"; import StatusPrepend from "./status_prepend"; +import QuotedStatus from "./quoted_status"; const domParser = new DOMParser(); @@ -855,6 +856,10 @@ class Status extends ImmutablePureComponent { rewriteMentions={settings.get("rewrite_mentions")} /> + {status.get('quote') ? ( + + ) : null} + {!isCollapsed || !(muted || !settings.getIn(["collapsed", "show_action_bar"])) ? ( { + const { signedIn } = this.context.identity; + + if (signedIn) { + this.props.onQuote(this.props.status, this.context.router.history); + } + }; + handleBookmarkClick = () => { this.props.onBookmark(this.props.status, undefined, this.context.router.history); }; @@ -325,6 +335,7 @@ class StatusActionBar extends ImmutablePureComponent { obfuscateCount /> + diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 436788b04e544130bcf67e2568bf9d66b47d8402..0c6d84b45fd3ffe8869b00ee2ef7f3e68e20bcb4 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -6,6 +6,7 @@ import { initBlockModal } from "flavours/glitch/actions/blocks"; import { initBoostModal } from "flavours/glitch/actions/boosts"; import { replyCompose, + quoteCompose, mentionCompose, directCompose, } from "flavours/glitch/actions/compose"; @@ -134,6 +135,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }); }, + onQuote (status, router) { + dispatch(quoteCompose(status, router)); + }, + onBookmark (status, folderId, router) { if (status.get("bookmarked")) { dispatch(unbookmark(status)); 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 4cb06e9ebc617997ee98716d627cb771f0d5dc57..13efc0f3934f6c1da1478cef7a875933377e3725 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -15,6 +15,7 @@ import AutosuggestTextarea from "../../../components/autosuggest_textarea"; import EmojiPickerDropdown from "../containers/emoji_picker_dropdown_container"; import OptionsContainer from "../containers/options_container"; import PollFormContainer from "../containers/poll_form_container"; +import QuoteIndicatorContainer from "../containers/quote_indicator_container"; import ReplyIndicatorContainer from "../containers/reply_indicator_container"; import UploadFormContainer from "../containers/upload_form_container"; import WarningContainer from "../containers/warning_container"; @@ -315,6 +316,7 @@ class ComposeForm extends ImmutablePureComponent { +
{ + const mapStateToProps = state => { + const statusId = state.getIn(['compose', 'quote_id']); + + return { + status: statusId ? state.getIn(['statuses', statusId]) : null, + editing: false, + }; + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = dispatch => ({ + onCancel () { + dispatch(cancelQuoteCompose()); + }, +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator); diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 4e5050fc609aa4b373d044df17919863e1167ac8..7ff0baf33b7d8df3d3c40ace993f6ba57f17ab5a 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -7,6 +7,8 @@ import { COMPOSE_CYCLE_ELEFRIEND, COMPOSE_REPLY, COMPOSE_REPLY_CANCEL, + COMPOSE_QUOTE, + COMPOSE_QUOTE_CANCEL, COMPOSE_DIRECT, COMPOSE_MENTION, COMPOSE_SUBMIT_REQUEST, @@ -87,6 +89,7 @@ const initialState = ImmutableMap({ caretPosition: null, preselectDate: null, in_reply_to: null, + quote_id: null, is_submitting: false, is_uploading: false, is_changing_upload: false, @@ -450,12 +453,26 @@ export default function compose(state = initialState, action) { map.set("spoiler_text", ""); } }); + case COMPOSE_QUOTE: + return state.withMutations(map => { + map.set("id", null); + map.set("quote_id", action.status.get("id")); + map.set("text", ""); + map.set("focusDate", new Date()); + map.set("caretPosition", null); + map.set("preselectDate", new Date()); + map.set("idempotencyKey", uuid()); + map.update("media_attachments", list => list.filter(media => media.get("unattached"))); + map.set("language", state.get("default_language")); + }); + case COMPOSE_QUOTE_CANCEL: case COMPOSE_REPLY_CANCEL: state = state.setIn(["advanced_options", "threaded_mode"], false); // eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended case COMPOSE_RESET: return state.withMutations(map => { map.set("in_reply_to", null); + map.set("quote_id", null); if (defaultContentType) { map.set("content_type", defaultContentType); } diff --git a/app/javascript/flavours/glitch/styles/components/status.css b/app/javascript/flavours/glitch/styles/components/status.css index 6759c9108b1c9dd03a7b94302b2c9e3ead54282a..61794bb7eeb50234e8e8329300ac7967340d8afa 100644 --- a/app/javascript/flavours/glitch/styles/components/status.css +++ b/app/javascript/flavours/glitch/styles/components/status.css @@ -1178,3 +1178,38 @@ a.status-card.compact:hover { border-color: var(--ui-base-color-lighten-12); } } + +.quoted-status { + margin: 10px 10px 0; + padding: 10px; + border: 1px solid var(--ui-base-color-lighten-8); + border-radius: 4px; + cursor: default; + + &--unavailable { + color: var(--primary-text-color-lighter-48); + font-style: italic; + } + + .quoted-status__header { + display: flex; + align-items: center; + margin-bottom: 6px; + } + + .quoted-status__account { + display: flex; + align-items: center; + gap: 6px; + text-decoration: none; + color: inherit; + + .display-name { + font-size: 14px; + } + } + + .status__content { + font-size: 14px; + } +}