~cytrogen/masto-fe

bb131c994569a8fbb139054be656f107dda0abe3 — Cytrogen 8 days ago 20a89f1
[feature] Quote post UI
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;
  }
}