diff --git a/app/javascript/mastodon/features/compose/components/edit_indicator.jsx b/app/javascript/mastodon/features/compose/components/edit_indicator.jsx index cc37d2d7d8..106ff7bdaa 100644 --- a/app/javascript/mastodon/features/compose/components/edit_indicator.jsx +++ b/app/javascript/mastodon/features/compose/components/edit_indicator.jsx @@ -13,6 +13,7 @@ import { cancelReplyCompose } from 'mastodon/actions/compose'; import { Icon } from 'mastodon/components/icon'; import { IconButton } from 'mastodon/components/icon_button'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; +import { EmbeddedStatusContent } from 'mastodon/features/notifications_v2/components/embedded_status_content'; const messages = defineMessages({ cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, @@ -33,8 +34,6 @@ export const EditIndicator = () => { return null; } - const content = { __html: status.get('contentHtml') }; - return (
@@ -49,7 +48,12 @@ export const EditIndicator = () => {
-
+ {(status.get('poll') || status.get('media_attachments').size > 0) && (
diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.jsx b/app/javascript/mastodon/features/compose/components/reply_indicator.jsx index b7959e211d..cf5bae2e07 100644 --- a/app/javascript/mastodon/features/compose/components/reply_indicator.jsx +++ b/app/javascript/mastodon/features/compose/components/reply_indicator.jsx @@ -9,6 +9,7 @@ import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react' import { Avatar } from 'mastodon/components/avatar'; import { DisplayName } from 'mastodon/components/display_name'; import { Icon } from 'mastodon/components/icon'; +import { EmbeddedStatusContent } from 'mastodon/features/notifications_v2/components/embedded_status_content'; export const ReplyIndicator = () => { const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to'])); @@ -19,8 +20,6 @@ export const ReplyIndicator = () => { return null; } - const content = { __html: status.get('contentHtml') }; - return (
@@ -34,7 +33,12 @@ export const ReplyIndicator = () => { -
+ {(status.get('poll') || status.get('media_attachments').size > 0) && (
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 c7e8172980..561f241d88 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx @@ -1,3 +1,5 @@ +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; import type { List } from 'immutable'; @@ -9,17 +11,25 @@ import { DisplayName } from 'mastodon/components/display_name'; import { Icon } from 'mastodon/components/icon'; import type { Status } from 'mastodon/models/status'; import { useAppSelector } from 'mastodon/store'; +import { EmbeddedStatusContent } from './embedded_status_content'; export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ statusId, }) => { + const history = useHistory(); + const status = useAppSelector( (state) => state.statuses.get(statusId) as Status | undefined, ); + const account = useAppSelector((state) => state.accounts.get(status?.get('account') as string), ); + const handleClick = useCallback(() => { + history.push(`/@${account.acct}/${statusId}`); + }, [statusId, account, history]); + if (!status) { return null; } @@ -27,12 +37,12 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ // Assign status attributes to variables with a forced type, as status is not yet properly typed const contentHtml = status.get('contentHtml') as string; const poll = status.get('poll'); + const language = status.get('language') as string; + const mentions = status.get('mentions'); const mediaAttachmentsSize = ( status.get('media_attachments') as List ).size; - const content = { __html: contentHtml }; - return (
@@ -40,9 +50,12 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
-
{(poll || mediaAttachmentsSize > 0) && ( diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx new file mode 100644 index 0000000000..4e47205e81 --- /dev/null +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx @@ -0,0 +1,144 @@ +import { useCallback, useRef } from 'react'; +import { useHistory } from 'react-router-dom'; + +const handleMentionClick = (history: unknown, mention: unknown, e: Event) => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + history.push(`/@${mention.get('acct')}`); + } +}; + +const handleHashtagClick = (history: unknown, hashtag: string, e: Event) => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + history.push(`/tags/${hashtag.replace(/^#/, '')}`); + } +}; + +export const EmbeddedStatusContent: React.FC<{ + content: string; + mentions: unknown; + language: string; + onClick?: unknown; + className?: string; +}> = ({ content, mentions, language, onClick, className }) => { + const clickCoordinatesRef = useRef(); + const history = useHistory(); + + const handleMouseDown = useCallback( + ({ clientX, clientY }) => { + clickCoordinatesRef.current = [clientX, clientY]; + }, + [clickCoordinatesRef], + ); + + const handleMouseUp = useCallback( + ({ clientX, clientY, target, button }) => { + const [startX, startY] = clickCoordinatesRef.current ?? [0, 0]; + const [deltaX, deltaY] = [ + Math.abs(clientX - startX), + Math.abs(clientY - startY), + ]; + + let element = target; + + while (element) { + if ( + element.localName === 'button' || + element.localName === 'a' || + element.localName === 'label' + ) { + return; + } + + element = element.parentNode; + } + + if (deltaX + deltaY < 5 && button === 0 && onClick) { + onClick(); + } + + clickCoordinatesRef.current = null; + }, + [clickCoordinatesRef, onClick], + ); + + const handleMouseEnter = useCallback(({ currentTarget }) => { + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (const emoji of emojis) { + emoji.src = emoji.getAttribute('data-original'); + } + }, []); + + const handleMouseLeave = useCallback(({ currentTarget }) => { + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (const emoji of emojis) { + emoji.src = emoji.getAttribute('data-static'); + } + }, []); + + const handleContentRef = useCallback( + (node) => { + if (!node) { + return; + } + + const links = node.querySelectorAll('a'); + + for (const link of links) { + if (link.classList.contains('status-link')) { + continue; + } + + link.classList.add('status-link'); + + const mention = mentions.find((item) => link.href === item.get('url')); + + if (mention) { + link.addEventListener( + 'click', + handleMentionClick.bind(null, history, mention), + false, + ); + link.setAttribute('title', `@${mention.get('acct')}`); + link.setAttribute('href', `/@${mention.get('acct')}`); + } else if ( + link.textContent[0] === '#' || + (link.previousSibling && + link.previousSibling.textContent && + link.previousSibling.textContent[ + link.previousSibling.textContent.length - 1 + ] === '#') + ) { + link.addEventListener( + 'click', + handleHashtagClick.bind(null, history, link.text), + false, + ); + link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`); + } else { + link.setAttribute('title', link.href); + link.classList.add('unhandled-link'); + } + } + }, + [mentions, history], + ); + + return ( +
+ ); +}; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 3435d6223b..f0ad6f1551 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -671,9 +671,13 @@ "report.unfollow_explanation": "You are following this account. To not see their posts in your home feed anymore, unfollow them.", "report_notification.attached_statuses": "{count, plural, one {{count} post} other {{count} posts}} attached", "report_notification.categories.legal": "Legal", + "report_notification.categories.legal_sentence": "illegal content", "report_notification.categories.other": "Other", + "report_notification.categories.other_sentence": "other", "report_notification.categories.spam": "Spam", + "report_notification.categories.spam_sentence": "spam", "report_notification.categories.violation": "Rule violation", + "report_notification.categories.violation_sentence": "rule violation", "report_notification.open": "Open report", "search.no_recent_searches": "No recent searches", "search.placeholder": "Search", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 5d69374db5..184d1fc29a 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -10355,8 +10355,8 @@ noscript { } } - &__follow &__icon, - &__follow-request &__icon { + &--follow &__icon, + &--follow-request &__icon { color: $highlight-text-color; } @@ -10447,9 +10447,15 @@ noscript { } &__content { + display: -webkit-box; font-size: 15px; line-height: 22px; color: $dark-text-color; + cursor: pointer; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + max-height: 4 * 22px; + overflow: hidden; p, a {