diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js
index becbdb88c3..5854482dc5 100644
--- a/app/javascript/mastodon/actions/importer/index.js
+++ b/app/javascript/mastodon/actions/importer/index.js
@@ -69,6 +69,10 @@ export function importFetchedStatuses(statuses) {
processStatus(status.reblog);
}
+ if (status.quote?.quoted_status) {
+ processStatus(status.quote.quoted_status);
+ }
+
if (status.poll?.id) {
pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id]));
}
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 2c583f86d4..330da74000 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -23,12 +23,20 @@ export function normalizeFilterResult(result) {
export function normalizeStatus(status, normalOldStatus) {
const normalStatus = { ...status };
+
normalStatus.account = status.account.id;
if (status.reblog && status.reblog.id) {
normalStatus.reblog = status.reblog.id;
}
+ if (status.quote?.quoted_status ?? status.quote?.quoted_status_id) {
+ normalStatus.quote = {
+ ...status.quote,
+ quoted_status: status.quote.quoted_status?.id ?? status.quote?.quoted_status_id,
+ };
+ }
+
if (status.poll && status.poll.id) {
normalStatus.poll = status.poll.id;
}
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 820b24cd6f..26d9c6d33f 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -5,14 +5,12 @@ import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
-
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
-import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import { ContentWarning } from 'mastodon/components/content_warning';
import { FilterWarning } from 'mastodon/components/filter_warning';
@@ -88,6 +86,7 @@ class Status extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.record,
+ children: PropTypes.node,
previousId: PropTypes.string,
nextInReplyToId: PropTypes.string,
rootId: PropTypes.string,
@@ -115,6 +114,7 @@ class Status extends ImmutablePureComponent {
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
showThread: PropTypes.bool,
+ isQuotedPost: PropTypes.bool,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
@@ -372,7 +372,7 @@ class Status extends ImmutablePureComponent {
};
render () {
- const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props;
+ const { intl, hidden, featured, unfocusable, unread, showThread, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props;
let { status, account, ...other } = this.props;
@@ -543,7 +543,7 @@ class Status extends ImmutablePureComponent {
{!skipPrepend && prepend}
-
+
{(connectReply || connectUp || connectToRoot) &&
}
@@ -576,12 +576,16 @@ class Status extends ImmutablePureComponent {
{...statusContentProps}
/>
+ {children}
+
{media}
{hashtagBar}
>
)}
-
+ {!isQuotedPost &&
+
+ }
diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx
index 3091e2a2a0..390659e9b6 100644
--- a/app/javascript/mastodon/components/status_list.jsx
+++ b/app/javascript/mastodon/components/status_list.jsx
@@ -9,7 +9,7 @@ import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'mastodon/actions/timelines';
import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator';
import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions';
-import StatusContainer from '../containers/status_container';
+import { StatusQuoteManager } from '../components/status_quoted';
import { LoadGap } from './load_gap';
import ScrollableList from './scrollable_list';
@@ -113,7 +113,7 @@ export default class StatusList extends ImmutablePureComponent {
);
default:
return (
-
(
- = ({ isError, children }) => {
+ return (
+
+
+ {children}
+
+ );
+};
+
+type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>;
+
+export const QuotedStatus: React.FC<{ quote: QuoteMap }> = ({ quote }) => {
+ const quotedStatusId = quote.get('quoted_status');
+ const state = quote.get('state');
+ const status = useAppSelector((state) =>
+ quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
+ );
+
+ let quoteError: React.ReactNode | null = null;
+
+ if (state === 'deleted') {
+ quoteError = (
+
+ );
+ } else if (state === 'unauthorized') {
+ quoteError = (
+
+ );
+ } else if (state === 'pending') {
+ quoteError = (
+
+ );
+ } else if (state === 'rejected' || state === 'revoked') {
+ quoteError = (
+
+ );
+ } else if (!status || !quotedStatusId) {
+ quoteError = (
+
+ );
+ }
+
+ if (quoteError) {
+ return {quoteError};
+ }
+
+ return (
+
+
+
+ );
+};
+
+interface StatusQuoteManagerProps {
+ id: string;
+ [key: string]: unknown;
+}
+
+/**
+ * This wrapper component takes a status ID and, if the associated status
+ * is a quote post, it renders the quote into `StatusContainer` as a child.
+ * It passes all other props through to `StatusContainer`.
+ */
+
+export const StatusQuoteManager = (props: StatusQuoteManagerProps) => {
+ const status = useAppSelector((state) => state.statuses.get(props.id));
+ const quote = status?.get('quote') as QuoteMap | undefined;
+
+ if (quote) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+};
diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx
index d516bc3411..c473d311c1 100644
--- a/app/javascript/mastodon/features/account_featured/index.tsx
+++ b/app/javascript/mastodon/features/account_featured/index.tsx
@@ -14,7 +14,7 @@ import { Account } from 'mastodon/components/account';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RemoteHint } from 'mastodon/components/remote_hint';
-import StatusContainer from 'mastodon/containers/status_container';
+import { StatusQuoteManager } from 'mastodon/components/status_quoted';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import Column from 'mastodon/features/ui/components/column';
@@ -142,9 +142,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
/>
{featuredStatusIds.map((statusId) => (
-
diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx
index 00963b2274..86431f62fd 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.jsx
+++ b/app/javascript/mastodon/features/notifications/components/notification.jsx
@@ -20,7 +20,7 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
-import StatusContainer from 'mastodon/containers/status_container';
+import { StatusQuoteManager } from 'mastodon/components/status_quoted';
import { me } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@@ -175,7 +175,7 @@ class Notification extends ImmutablePureComponent {
renderMention (notification) {
return (
-
-
-
-
-
-
- is not yet typed
+
const renderStatuses = (statusIds: string[]) =>
hidePeek(statusIds).map((id) => (
- // @ts-expect-error inferred props are wrong
-
+
));
type SearchType = 'all' | ApiSearchType;
@@ -190,8 +189,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
onClickMore={handleSelectStatuses}
>
{results.statuses.slice(0, INITIAL_DISPLAY).map((id) => (
- // @ts-expect-error inferred props are wrong
-
+
))}
)}
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx
index 75d995b1e0..aa79a82f68 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.tsx
+++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx
@@ -26,6 +26,7 @@ import { IconLogo } from 'mastodon/components/logo';
import MediaGallery from 'mastodon/components/media_gallery';
import { PictureInPicturePlaceholder } from 'mastodon/components/picture_in_picture_placeholder';
import StatusContent from 'mastodon/components/status_content';
+import { QuotedStatus } from 'mastodon/components/status_quoted';
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
import { Audio } from 'mastodon/features/audio';
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
@@ -371,6 +372,10 @@ export const DetailedStatus: React.FC<{
{...(statusContentProps as any)}
/>
+ {status.get('quote') && (
+
+ )}
+
{media}
{hashtagBar}
>
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx
index 7da2df3742..0f02e7b50f 100644
--- a/app/javascript/mastodon/features/status/index.jsx
+++ b/app/javascript/mastodon/features/status/index.jsx
@@ -6,8 +6,6 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom';
-import { createSelector } from '@reduxjs/toolkit';
-import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
@@ -62,7 +60,7 @@ import {
} from '../../actions/statuses';
import ColumnHeader from '../../components/column_header';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
-import StatusContainer from '../../containers/status_container';
+import { StatusQuoteManager } from '../../components/status_quoted';
import { deleteModal } from '../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts';
@@ -477,7 +475,7 @@ class Status extends ImmutablePureComponent {
const { params: { statusId } } = this.props;
return list.map((id, i) => (
- [data-popper-placement] {
}
}
+ &--is-quote {
+ border: none;
+ }
+
&--in-thread {
- $thread-margin: 46px + 10px;
+ --thread-margin: calc(46px + 8px);
border-bottom: 0;
@@ -1508,16 +1512,16 @@ body > [data-popper-placement] {
.hashtag-bar,
.content-warning,
.filter-warning {
- margin-inline-start: $thread-margin;
- width: calc(100% - $thread-margin);
+ margin-inline-start: var(--thread-margin);
+ width: calc(100% - var(--thread-margin));
}
.more-from-author {
- width: calc(100% - $thread-margin + 2px);
+ width: calc(100% - var(--thread-margin) + 2px);
}
.status__content__read-more-button {
- margin-inline-start: $thread-margin;
+ margin-inline-start: var(--thread-margin);
}
}
@@ -1873,6 +1877,41 @@ body > [data-popper-placement] {
}
}
+.status__quote {
+ position: relative;
+ margin-block-start: 16px;
+ margin-inline-start: 56px;
+ border-radius: 8px;
+ color: var(--nested-card-text);
+ background: var(--nested-card-background);
+ border: var(--nested-card-border);
+}
+
+.status__quote--error {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px;
+ font-size: 15px;
+}
+
+.status__quote-icon {
+ position: absolute;
+ inset-block-start: 18px;
+ inset-inline-start: -50px;
+ display: block;
+ width: 26px;
+ height: 26px;
+ padding: 5px;
+ color: #6a49ba;
+ z-index: 10;
+
+ .status__quote--error & {
+ inset-block-start: 50%;
+ transform: translateY(-50%);
+ }
+}
+
.detailed-status__link {
display: inline-flex;
align-items: center;
@@ -2306,11 +2345,6 @@ a.account__display-name {
}
}
-.status__avatar {
- width: 46px;
- height: 46px;
-}
-
.muted {
.status__content,
.status__content p,
@@ -10515,6 +10549,7 @@ noscript {
line-height: 22px;
color: $darker-text-color;
-webkit-line-clamp: 4;
+ line-clamp: 4;
-webkit-box-orient: vertical;
max-height: none;
overflow: hidden;
@@ -10818,9 +10853,9 @@ noscript {
.content-warning {
display: block;
box-sizing: border-box;
- background: rgba($ui-highlight-color, 0.05);
- color: $secondary-text-color;
- border: 1px solid rgba($ui-highlight-color, 0.15);
+ background: var(--nested-card-background);
+ color: var(--nested-card-text);
+ border: var(--nested-card-border);
border-radius: 8px;
padding: 8px (5px + 8px);
position: relative;
diff --git a/app/javascript/styles/mastodon/css_variables.scss b/app/javascript/styles/mastodon/css_variables.scss
index 782e08e283..413efca3f6 100644
--- a/app/javascript/styles/mastodon/css_variables.scss
+++ b/app/javascript/styles/mastodon/css_variables.scss
@@ -27,6 +27,10 @@
--rich-text-container-color: rgba(87, 24, 60, 100%);
--rich-text-text-color: rgba(255, 175, 212, 100%);
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
+ --nested-card-background: color(from #{$ui-highlight-color} srgb r g b / 5%);
+ --nested-card-text: #{$secondary-text-color};
+ --nested-card-border: 1px solid
+ color(from #{$ui-highlight-color} srgb r g b / 15%);
--input-placeholder-color: #{$dark-text-color};
--input-background-color: var(--surface-variant-background-color);
--on-input-color: #{$secondary-text-color};