M app/javascript/flavours/glitch/actions/compose.js => app/javascript/flavours/glitch/actions/compose.js +20 -0
@@ 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"]),
M app/javascript/flavours/glitch/actions/importer/index.js => app/javascript/flavours/glitch/actions/importer/index.js +4 -0
@@ 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])));
}
M app/javascript/flavours/glitch/actions/importer/normalizer.js => app/javascript/flavours/glitch/actions/importer/normalizer.js +5 -0
@@ 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;
}
A app/javascript/flavours/glitch/components/quoted_status.jsx => app/javascript/flavours/glitch/components/quoted_status.jsx +63 -0
@@ 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 (
+ <div className='quoted-status quoted-status--unavailable'>
+ <p>This post is unavailable.</p>
+ </div>
+ );
+ }
+
+ const account = status.get('account');
+
+ return (
+ <div className='quoted-status'>
+ <div className='quoted-status__header'>
+ <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='quoted-status__account'>
+ <Avatar account={account} size={18} />
+ <DisplayName account={account} />
+ </Permalink>
+ </div>
+ <StatusContent
+ status={status}
+ expanded
+ collapsible={false}
+ />
+ </div>
+ );
+ }
+
+}
+
+export default connect(makeMapStateToProps)(QuotedStatus);
M app/javascript/flavours/glitch/components/status.jsx => app/javascript/flavours/glitch/components/status.jsx +5 -0
@@ 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') ? (
+ <QuotedStatus statusId={status.get('quote')} />
+ ) : null}
+
{!isCollapsed || !(muted || !settings.getIn(["collapsed", "show_action_bar"])) ? (
<StatusActionBar
status={status}
M app/javascript/flavours/glitch/components/status_action_bar.jsx => app/javascript/flavours/glitch/components/status_action_bar.jsx +11 -0
@@ 31,6 31,7 @@ const messages = defineMessages({
reblog_private: { id: "status.reblog_private", defaultMessage: "Boost with original visibility" },
cancel_reblog_private: { id: "status.cancel_reblog_private", defaultMessage: "Unboost" },
cannot_reblog: { id: "status.cannot_reblog", defaultMessage: "This post cannot be boosted" },
+ quote: { id: "status.quote", defaultMessage: "Quote" },
favourite: { id: "status.favourite", defaultMessage: "Favorite" },
bookmark: { id: "status.bookmark", defaultMessage: "Bookmark" },
open: { id: "status.open", defaultMessage: "Expand this status" },
@@ 63,6 64,7 @@ class StatusActionBar extends ImmutablePureComponent {
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
+ onQuote: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
@@ 130,6 132,14 @@ class StatusActionBar extends ImmutablePureComponent {
}
};
+ handleQuoteClick = () => {
+ 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
/>
<IconButton className={classNames("status__action-bar-button", { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get("reblogged")} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get("reblogs_count") : undefined} />
+ <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.quote)} icon='quote-left' onClick={this.handleQuoteClick} disabled={!publicStatus} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get("favourited")} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get("favourites_count") : undefined} />
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get("bookmarked")} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
M app/javascript/flavours/glitch/containers/status_container.js => app/javascript/flavours/glitch/containers/status_container.js +5 -0
@@ 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));
M app/javascript/flavours/glitch/features/compose/components/compose_form.jsx => app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +2 -0
@@ 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 {
<WarningContainer />
<ReplyIndicatorContainer />
+ <QuoteIndicatorContainer />
<div className={`spoiler-input ${spoiler ? "spoiler-input--visible" : ""}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput
A app/javascript/flavours/glitch/features/compose/containers/quote_indicator_container.js => app/javascript/flavours/glitch/features/compose/containers/quote_indicator_container.js +26 -0
@@ 0,0 1,26 @@
+import { connect } from 'react-redux';
+
+import { cancelQuoteCompose } from 'flavours/glitch/actions/compose';
+
+import ReplyIndicator from '../components/reply_indicator';
+
+const makeMapStateToProps = () => {
+ 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);
M app/javascript/flavours/glitch/reducers/compose.js => app/javascript/flavours/glitch/reducers/compose.js +17 -0
@@ 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);
}
M app/javascript/flavours/glitch/styles/components/status.css => app/javascript/flavours/glitch/styles/components/status.css +35 -0
@@ 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;
+ }
+}