From e325443b0270edeaf159f010fb1b1078057a6017 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 7 Aug 2023 09:46:11 +0200 Subject: [PATCH] Change header of hashtag timelines in web UI (#26362) --- .../components/hashtag_header.jsx | 79 +++++++++++++++++++ .../features/hashtag_timeline/index.jsx | 34 ++------ app/javascript/mastodon/locales/en.json | 3 + .../styles/mastodon/components.scss | 30 +++++++ 4 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx new file mode 100644 index 000000000..46050309f --- /dev/null +++ b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx @@ -0,0 +1,79 @@ +import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import Button from 'mastodon/components/button'; +import { ShortNumber } from 'mastodon/components/short_number'; + +const messages = defineMessages({ + followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, + unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' }, +}); + +const usesRenderer = (displayNumber, pluralReady) => ( + {displayNumber}, + }} + /> +); + +const peopleRenderer = (displayNumber, pluralReady) => ( + {displayNumber}, + }} + /> +); + +const usesTodayRenderer = (displayNumber, pluralReady) => ( + {displayNumber}, + }} + /> +); + +export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => { + if (!tag) { + return null; + } + + const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]); + const dividingCircle = {' ยท '}; + + return ( +
+
+

#{tag.get('name')}

+
+ +
+ + {dividingCircle} + + {dividingCircle} + +
+
+ ); +}); + +HashtagHeader.propTypes = { + tag: ImmutablePropTypes.map, + disabled: PropTypes.bool, + onClick: PropTypes.func, + intl: PropTypes.object, +}; \ No newline at end of file diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.jsx b/app/javascript/mastodon/features/hashtag_timeline/index.jsx index b6ed29d8d..d00890cb3 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.jsx +++ b/app/javascript/mastodon/features/hashtag_timeline/index.jsx @@ -1,9 +1,8 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -17,17 +16,12 @@ import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/t import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; -import { Icon } from 'mastodon/components/icon'; import StatusListContainer from '../ui/containers/status_list_container'; +import { HashtagHeader } from './components/hashtag_header'; import ColumnSettingsContainer from './containers/column_settings_container'; -const messages = defineMessages({ - followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, - unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' }, -}); - const mapStateToProps = (state, props) => ({ hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0, tag: state.getIn(['tags', props.params.id]), @@ -48,7 +42,6 @@ class HashtagTimeline extends PureComponent { hasUnread: PropTypes.bool, tag: ImmutablePropTypes.map, multiColumn: PropTypes.bool, - intl: PropTypes.object, }; handlePin = () => { @@ -188,27 +181,11 @@ class HashtagTimeline extends PureComponent { }; render () { - const { hasUnread, columnId, multiColumn, tag, intl } = this.props; + const { hasUnread, columnId, multiColumn, tag } = this.props; const { id, local } = this.props.params; const pinned = !!columnId; const { signedIn } = this.context.identity; - let followButton; - - if (tag) { - const following = tag.get('following'); - - const classes = classNames('column-header__button', { - active: following, - }); - - followButton = ( - - ); - } - return ( {columnId && } } + alwaysPrepend trackScroll={!pinned} scrollKey={`hashtag_timeline-${columnId}`} timelineId={`hashtag:${id}${local ? ':local' : ''}`} @@ -245,4 +223,4 @@ class HashtagTimeline extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(HashtagTimeline)); +export default connect(mapStateToProps)(HashtagTimeline); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 2c901f3bd..5b7b4f6b3 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -295,6 +295,9 @@ "hashtag.column_settings.tag_mode.any": "Any of these", "hashtag.column_settings.tag_mode.none": "None of these", "hashtag.column_settings.tag_toggle": "Include additional tags for this column", + "hashtag.counter_by_accounts": "{count, plural, one {{counter} participant} other {{counter} participants}}", + "hashtag.counter_by_uses": "{count, plural, one {{counter} post} other {{counter} posts}}", + "hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today", "hashtag.follow": "Follow hashtag", "hashtag.unfollow": "Unfollow hashtag", "home.actions.go_to_explore": "See what's trending", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index cd9617c94..049c1a322 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -9231,3 +9231,33 @@ noscript { background: rgba($ui-base-color, 0.85); } } + +.hashtag-header { + border-bottom: 1px solid lighten($ui-base-color, 8%); + padding: 15px; + font-size: 17px; + line-height: 22px; + color: $darker-text-color; + + strong { + font-weight: 700; + } + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + gap: 15px; + + h1 { + color: $primary-text-color; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-size: 22px; + line-height: 33px; + font-weight: 700; + } + } +}