Add trends columns
This commit is contained in:
parent
95670ff617
commit
cfce94fb24
11 changed files with 155 additions and 5 deletions
|
@ -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
|
||||
|
|
|
@ -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)));
|
||||
};
|
||||
|
|
|
@ -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' />,
|
||||
|
|
122
app/javascript/mastodon/features/trends/index.js
Normal file
122
app/javascript/mastodon/features/trends/index.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue