From df6e7198984b42cf8d64e66548742e14264236a0 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 14 Aug 2023 23:42:30 +0200 Subject: [PATCH] Add display of out-of-band hashtags in the web interface (#26492) Co-authored-by: Eugen Rochko --- .../mastodon/components/hashtag_bar.jsx | 50 +++++++++++++++++++ app/javascript/mastodon/components/status.jsx | 3 ++ .../status/components/detailed_status.jsx | 3 ++ app/javascript/mastodon/locales/en.json | 1 + .../styles/mastodon/components.scss | 23 +++++++++ 5 files changed, 80 insertions(+) create mode 100644 app/javascript/mastodon/components/hashtag_bar.jsx diff --git a/app/javascript/mastodon/components/hashtag_bar.jsx b/app/javascript/mastodon/components/hashtag_bar.jsx new file mode 100644 index 000000000..6a39005e1 --- /dev/null +++ b/app/javascript/mastodon/components/hashtag_bar.jsx @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import { useMemo, useState, useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +const domParser = new DOMParser(); + +// About two lines on desktop +const VISIBLE_HASHTAGS = 7; + +export const HashtagBar = ({ hashtags, text }) => { + const renderedHashtags = useMemo(() => { + const body = domParser.parseFromString(text, 'text/html').documentElement; + return [].map.call(body.querySelectorAll('[rel=tag]'), node => node.textContent.toLowerCase()); + }, [text]); + + const invisibleHashtags = useMemo(() => ( + hashtags.filter(hashtag => !renderedHashtags.some(textContent => textContent === `#${hashtag.get('name')}` || textContent === hashtag.get('name'))) + ), [hashtags, renderedHashtags]); + + const [expanded, setExpanded] = useState(false); + const handleClick = useCallback(() => setExpanded(true), []); + + if (invisibleHashtags.isEmpty()) { + return null; + } + + const revealedHashtags = expanded ? invisibleHashtags : invisibleHashtags.take(VISIBLE_HASHTAGS); + + return ( +
+ {revealedHashtags.map(hashtag => ( + + #{hashtag.get('name')} + + ))} + + {!expanded && invisibleHashtags.size > VISIBLE_HASHTAGS && } +
+ ); +}; + +HashtagBar.propTypes = { + hashtags: ImmutablePropTypes.list, + text: PropTypes.string, +}; \ No newline at end of file diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 37951d578..7c34684d7 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -22,6 +22,7 @@ import { displayMedia } from '../initial_state'; import { Avatar } from './avatar'; import { AvatarOverlay } from './avatar_overlay'; import { DisplayName } from './display_name'; +import { HashtagBar } from './hashtag_bar'; import { RelativeTimestamp } from './relative_timestamp'; import StatusActionBar from './status_action_bar'; import StatusContent from './status_content'; @@ -580,6 +581,8 @@ class Status extends ImmutablePureComponent { {media} + + diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 7348905c5..c1815b916 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { AnimatedNumber } from 'mastodon/components/animated_number'; import EditedTimestamp from 'mastodon/components/edited_timestamp'; +import { HashtagBar } from 'mastodon/components/hashtag_bar'; import { Icon } from 'mastodon/components/icon'; import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; @@ -314,6 +315,8 @@ class DetailedStatus extends ImmutablePureComponent { {media} + +
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 5b7b4f6b3..7205bff05 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -300,6 +300,7 @@ "hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today", "hashtag.follow": "Follow hashtag", "hashtag.unfollow": "Unfollow hashtag", + "hashtags.and_other": "…and {count, plural, other {# more}}", "home.actions.go_to_explore": "See what's trending", "home.actions.go_to_suggestions": "Find people to follow", "home.column_settings.basic": "Basic", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 46c7d328c..009d4f71d 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -9256,3 +9256,26 @@ noscript { } } } + +.hashtag-bar { + margin-top: 16px; + display: flex; + flex-wrap: wrap; + font-size: 14px; + gap: 4px; + + a { + display: inline-flex; + border-radius: 4px; + background: rgba($highlight-text-color, 0.2); + color: $highlight-text-color; + padding: 0.4em 0.6em; + text-decoration: none; + + &:hover, + &:focus, + &:active { + background: rgba($highlight-text-color, 0.3); + } + } +}