Add trends columns

This commit is contained in:
noellabo 2021-03-24 17:14:38 +09:00
parent 95670ff617
commit cfce94fb24
11 changed files with 155 additions and 5 deletions

View file

@ -10,6 +10,6 @@ class Api::V1::TrendsController < Api::BaseController
private
def set_tags
@tags = TrendingTags.get(limit_param(10))
@tags = TrendingTags.get(limit_param(TrendingTags::LIMIT))
end
end

View file

@ -8,7 +8,7 @@ export const fetchTrends = () => (dispatch, getState) => {
dispatch(fetchTrendsRequest());
api(getState)
.get('/api/v1/trends')
.get('/api/v1/trends', { params: { limit: 20 } })
.then(({ data }) => dispatch(fetchTrendsSuccess(data)))
.catch(err => dispatch(fetchTrendsFail(err)));
};

View file

@ -37,6 +37,7 @@ const messages = defineMessages({
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
group_directory: { id: 'getting_started.group_directory', defaultMessage: 'Group directory' },
profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' },
trends: { id: 'navigation_bar.trends', defaultMessage: 'Trends' },
information_acct: { id: 'navigation_bar.information_acct', defaultMessage: 'Fedibird info' },
hashtag_fedibird: { id: 'navigation_bar.hashtag_fedibird', defaultMessage: 'fedibird' },
});
@ -120,6 +121,12 @@ class GettingStarted extends ImmutablePureComponent {
height += 48;
}
navItems.push(
<ColumnLink key='trends' icon='line-chart' text={intl.formatMessage(messages.trends)} to='/trends' />,
);
height += 48;
navItems.push(
<ColumnLink key='information_acct' icon='info-circle' text={intl.formatMessage(messages.information_acct)} to='/accounts/2' />,
<ColumnLink key='hashtag_fedibird' icon='hashtag' text={intl.formatMessage(messages.hashtag_fedibird)} to='/timelines/tag/fedibird' />,
@ -147,6 +154,12 @@ class GettingStarted extends ImmutablePureComponent {
height += 48;
}
navItems.push(
<ColumnLink key='trends' icon='line-chart' text={intl.formatMessage(messages.trends)} to='/trends' />,
);
height += 48;
navItems.push(
<ColumnLink key='information_acct' icon='info-circle' text={intl.formatMessage(messages.information_acct)} to='/accounts/2' />,
<ColumnLink key='hashtag_fedibird' icon='hashtag' text={intl.formatMessage(messages.hashtag_fedibird)} to='/timelines/tag/fedibird' />,

View file

@ -0,0 +1,122 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchTrends } from '../../actions/trends';
import Column from '../ui/components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSubheading from '../ui/components/column_subheading';
import Icon from 'mastodon/components/icon';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import ScrollableList from 'mastodon/components/scrollable_list';
import Hashtag from 'mastodon/components/hashtag';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
heading: { id: 'trends.heading', defaultMessage: 'Trends' },
subheading: { id: 'trends.trending_now', defaultMessage: 'Trending now' },
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
});
const mapStateToProps = state => ({
trends: state.getIn(['trends', 'items']),
isLoading: state.getIn(['trends', 'isLoading'], true),
});
export default @connect(mapStateToProps)
@injectIntl
class Trends extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
trends: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
isLoading: PropTypes.bool,
};
componentDidMount () {
this.fetchTrends();
this.refreshInterval = setInterval(() => this.fetchTrends(), 900 * 1000);
}
componentWillUnmount () {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
}
handleRefresh = () => {
this.fetchTrends();
}
fetchTrends = () => {
const { dispatch } = this.props;
dispatch(fetchTrends());
}
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('TRENDS', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
render () {
const { intl, trends, columnId, multiColumn, isLoading } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.trends' defaultMessage='No one has trends yet.' />;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader
icon='line-chart'
title={intl.formatMessage(messages.heading)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
showBackButton
extraButton={(
<button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
)}
/>
<ScrollableList
trackScroll={!pinned}
scrollKey={`trends-${columnId}`}
isLoading={isLoading}
emptyMessage={emptyMessage}
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
bindToDocument={!multiColumn}
>
{trends.map(hashtag =>
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />,
)}
</ScrollableList>
</Column>
);
}
}

View file

@ -28,6 +28,7 @@ import {
ListTimeline,
GroupDirectory,
Directory,
Trends,
} from '../../ui/util/async-components';
import Icon from 'mastodon/components/icon';
import ComposePanel from './compose_panel';
@ -54,6 +55,7 @@ const componentMap = {
'LIST': ListTimeline,
'GROUP_DIRECTORY': GroupDirectory,
'DIRECTORY': Directory,
'TRENDS': Trends,
};
const messages = defineMessages({

View file

@ -25,6 +25,7 @@ const NavigationPanel = () => (
<NavLink className='column-link column-link--transparent' to='/circles'><Icon className='column-link__icon' id='user-circle' fixedWidth /><FormattedMessage id='navigation_bar.circles' defaultMessage='Circles' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/group_directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.group_directory' defaultMessage='Group directory' /></NavLink>
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
<NavLink className='column-link column-link--transparent' to='/trends'><Icon className='column-link__icon' id='line-chart' fixedWidth /><FormattedMessage id='navigation_bar.trends' defaultMessage='Trends' /></NavLink>
<ListPanel />
<FavouriteDomainPanel />

View file

@ -57,6 +57,7 @@ import {
GroupDirectory,
Directory,
FollowRecommendations,
Trends,
} from './util/async-components';
import { me } from '../../initial_state';
import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
@ -173,6 +174,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path='/group_directory' component={GroupDirectory} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} />
<WrappedRoute path='/trends' component={Trends} content={children} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />

View file

@ -185,3 +185,7 @@ export function Directory () {
export function FollowRecommendations () {
return import(/* webpackChunkName: "features/follow_recommendations" */'../../follow_recommendations');
}
export function Trends () {
return import(/* webpackChunkName: "features/trends" */'../../trends');
}

View file

@ -203,6 +203,7 @@
"empty_column.mutes": "You haven't muted any users yet.",
"empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
"empty_column.trends": "No one has trends yet.",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
@ -345,6 +346,7 @@
"navigation_bar.short.preferences": "Pref.",
"navigation_bar.short.public_timeline": "FTL",
"navigation_bar.short.search": "Search",
"navigation_bar.trends": "Trends",
"notification.favourite": "{name} favourited your post",
"notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you",
@ -503,6 +505,7 @@
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older posts",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} talking",
"trends.heading": "Trends",
"trends.trending_now": "Trending now",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"units.short.billion": "{count}B",

View file

@ -202,6 +202,7 @@
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
"empty_column.mutes": "まだ誰もミュートしていません。",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.trends": "まだ何もトレンドがありません。",
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
"error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。",
"error.unexpected_crash.explanation_addons": "このページは正しく表示できませんでした。このエラーはブラウザのアドオンや自動翻訳ツールによって引き起こされることがあります。",
@ -345,6 +346,7 @@
"navigation_bar.short.preferences": "設定",
"navigation_bar.short.public_timeline": "連合",
"navigation_bar.short.search": "検索",
"navigation_bar.trends": "トレンド",
"notification.favourite": "{name}さんがあなたの投稿をお気に入りに登録しました",
"notification.follow": "{name}さんにフォローされました",
"notification.follow_request": "{name} さんがあなたにフォローリクエストしました",
@ -503,6 +505,7 @@
"timeline_hint.resources.follows": "フォロー",
"timeline_hint.resources.statuses": "以前の投稿",
"trends.counter_by_accounts": "{counter} 人が投稿",
"trends.heading": "トレンド",
"trends.trending_now": "トレンドタグ",
"ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
"units.short.billion": "{count}B",

View file

@ -86,12 +86,12 @@ class TrendingTags
# Trim older items
redis.zremrangebyrank(KEY, 0, -(LIMIT + 1))
redis.zremrangebyrank(KEY, 0, -(LIMIT*2 + 1))
redis.zremrangebyscore(KEY, '(0.3', '-inf')
end
def get(limit, filtered: true)
tag_ids = redis.zrevrange(KEY, 0, LIMIT - 1).map(&:to_i)
tag_ids = redis.zrevrange(KEY, 0, LIMIT*2 - 1).map(&:to_i)
tags = Tag.where(id: tag_ids)
tags = tags.trendable if filtered
@ -102,7 +102,7 @@ class TrendingTags
def trending?(tag)
rank = redis.zrevrank(KEY, tag.id)
rank.present? && rank < LIMIT
rank.present? && rank < LIMIT*2
end
private