Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change header of hashtag timelines in web UI #26362

Merged
merged 1 commit into from Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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) => (
<FormattedMessage
id='hashtag.counter_by_uses'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);

const peopleRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);

const usesTodayRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses_today'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);

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 = <span aria-hidden>{' · '}</span>;

return (
<div className='hashtag-header'>
<div className='hashtag-header__header'>
<h1>#{tag.get('name')}</h1>
<Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
</div>

<div>
<ShortNumber value={uses} renderer={usesRenderer} />
{dividingCircle}
<ShortNumber value={people} renderer={peopleRenderer} />
{dividingCircle}
<ShortNumber value={tag.getIn(['history', 0, 'uses']) * 1} renderer={usesTodayRenderer} />
</div>
</div>
);
});

HashtagHeader.propTypes = {
tag: ImmutablePropTypes.map,
disabled: PropTypes.bool,
onClick: PropTypes.func,
intl: PropTypes.object,
};
34 changes: 6 additions & 28 deletions 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';
Expand All @@ -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]),
Expand All @@ -48,7 +42,6 @@ class HashtagTimeline extends PureComponent {
hasUnread: PropTypes.bool,
tag: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
intl: PropTypes.object,
};

handlePin = () => {
Expand Down Expand Up @@ -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 = (
<button className={classes} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
</button>
);
}

return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
<ColumnHeader
Expand All @@ -220,13 +197,14 @@ class HashtagTimeline extends PureComponent {
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
extraButton={followButton}
showBackButton
>
{columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader>

<StatusListContainer
prepend={<HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}${local ? ':local' : ''}`}
Expand All @@ -245,4 +223,4 @@ class HashtagTimeline extends PureComponent {

}

export default connect(mapStateToProps)(injectIntl(HashtagTimeline));
export default connect(mapStateToProps)(HashtagTimeline);
3 changes: 3 additions & 0 deletions app/javascript/mastodon/locales/en.json
Expand Up @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions app/javascript/styles/mastodon/components.scss
Expand Up @@ -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;
}
}
}