diff --git a/app/javascript/mastodon/components/content_warning.tsx b/app/javascript/mastodon/components/content_warning.tsx
new file mode 100644
index 00000000000..8498e3ccc66
--- /dev/null
+++ b/app/javascript/mastodon/components/content_warning.tsx
@@ -0,0 +1,30 @@
+import { FormattedMessage } from 'react-intl';
+
+export const ContentWarning: React.FC<{
+ text: string;
+ expanded?: boolean;
+ onClick?: () => void;
+}> = ({ text, expanded, onClick }) => (
+
+
+
+
+
{text}
+
+
+
+
+
+);
diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx
index baec0161173..0dbf5dbf8da 100644
--- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx
+++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useRef } from 'react';
+import { useCallback, useRef, useState } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -9,6 +9,7 @@ import type { List as ImmutableList, RecordOf } from 'immutable';
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
import { Avatar } from 'mastodon/components/avatar';
+import { ContentWarning } from 'mastodon/components/content_warning';
import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon';
import type { Status } from 'mastodon/models/status';
@@ -23,6 +24,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
}) => {
const history = useHistory();
const clickCoordinatesRef = useRef<[number, number] | null>();
+ const [expanded, setExpanded] = useState(false);
const status = useAppSelector(
(state) => state.statuses.get(statusId) as Status | undefined,
@@ -96,12 +98,17 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
[],
);
+ const handleContentWarningClick = useCallback(() => {
+ setExpanded((v) => !v);
+ }, [setExpanded]);
+
if (!status) {
return null;
}
// Assign status attributes to variables with a forced type, as status is not yet properly typed
const contentHtml = status.get('contentHtml') as string;
+ const contentWarning = status.get('spoiler_text') as string;
const poll = status.get('poll');
const language = status.get('language') as string;
const mentions = status.get('mentions') as ImmutableList;
@@ -124,12 +131,22 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
-
+ {contentWarning && (
+
+ )}
+
+ {(!contentWarning || expanded) && (
+
+ )}
{(poll || mediaAttachmentsSize > 0) && (
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 86afa7cd0d7..f4e0323a817 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -193,6 +193,8 @@
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"confirmations.unfollow.title": "Unfollow user?",
+ "content_warning.hide": "Hide post",
+ "content_warning.show": "Show anyway",
"conversation.delete": "Delete conversation",
"conversation.mark_as_read": "Mark as read",
"conversation.open": "View conversation",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 3c938ac4c59..d64901afd00 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -10438,39 +10438,53 @@ noscript {
}
&__embedded-status {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
cursor: pointer;
&__account {
display: flex;
align-items: center;
gap: 4px;
- margin-bottom: 8px;
color: $dark-text-color;
+ font-size: 15px;
+ line-height: 22px;
bdi {
- color: inherit;
+ color: $darker-text-color;
}
}
- .account__avatar {
- opacity: 0.5;
- }
-
&__content {
display: -webkit-box;
font-size: 15px;
line-height: 22px;
- color: $dark-text-color;
+ color: $darker-text-color;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
max-height: 4 * 22px;
overflow: hidden;
+ p {
+ display: none;
+
+ &:first-child {
+ display: initial;
+ }
+ }
+
p,
a {
color: inherit;
}
}
+
+ .reply-indicator__attachments {
+ font-size: 15px;
+ line-height: 22px;
+ color: $dark-text-color;
+ }
}
}
@@ -10740,3 +10754,45 @@ noscript {
}
}
}
+
+.content-warning {
+ display: flex;
+ align-items: stretch;
+ gap: 8px;
+ background: rgba($ui-highlight-color, 0.05);
+ color: $secondary-text-color;
+ border-top: 1px solid;
+ border-bottom: 1px solid;
+ border-color: rgba($ui-highlight-color, 0.15);
+
+ &__body {
+ flex: 1 1 auto;
+ padding: 8px 0;
+ font-size: 15px;
+ line-height: 22px;
+
+ p {
+ margin-bottom: 8px;
+ }
+
+ .link-button {
+ font-size: inherit;
+ line-height: inherit;
+ font-weight: 500;
+ }
+ }
+
+ &__border {
+ background: url('../images/warning-stripes.svg') repeat-y;
+ width: 5px;
+ flex: 0 0 auto;
+
+ &:first-child {
+ border-start-start-radius: 4px;
+ }
+
+ &:last-child {
+ border-start-end-radius: 4px;
+ }
+ }
+}