Merge remote-tracking branch 'origin/master' into gs-master

This commit is contained in:
David Yip 2018-06-03 19:44:05 -05:00
commit c55263027a
No known key found for this signature in database
GPG Key ID: 7DA0036508FCC0CC
13 changed files with 2 additions and 226 deletions

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::TrendsController < Api::BaseController
before_action :set_tags
respond_to :json
def index
render json: @tags, each_serializer: REST::TagSerializer
end
private
def set_tags
@tags = TrendingTags.get(limit_param(10))
end
end

View File

@ -1,32 +0,0 @@
import api from '../api';
export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST';
export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS';
export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL';
export const fetchTrends = () => (dispatch, getState) => {
dispatch(fetchTrendsRequest());
api(getState)
.get('/api/v1/trends')
.then(({ data }) => dispatch(fetchTrendsSuccess(data)))
.catch(err => dispatch(fetchTrendsFail(err)));
};
export const fetchTrendsRequest = () => ({
type: TRENDS_FETCH_REQUEST,
skipLoading: true,
});
export const fetchTrendsSuccess = trends => ({
type: TRENDS_FETCH_SUCCESS,
trends,
skipLoading: true,
});
export const fetchTrendsFail = error => ({
type: TRENDS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
});

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import AccountContainer from '../../../containers/account_container'; import AccountContainer from '../../../containers/account_container';
@ -11,36 +10,14 @@ export default class SearchResults extends ImmutablePureComponent {
static propTypes = { static propTypes = {
results: ImmutablePropTypes.map.isRequired, results: ImmutablePropTypes.map.isRequired,
trends: ImmutablePropTypes.list,
fetchTrends: PropTypes.func.isRequired,
}; };
componentDidMount () {
const { fetchTrends } = this.props;
fetchTrends();
}
render () { render () {
const { results, trends } = this.props; const { results } = this.props;
let accounts, statuses, hashtags; let accounts, statuses, hashtags;
let count = 0; let count = 0;
if (results.isEmpty()) {
return (
<div className='search-results'>
<div className='trends'>
<div className='trends__header'>
<i className='fa fa-fire fa-fw' />
<FormattedMessage id='trends.header' defaultMessage='Trending now' />
</div>
{trends && trends.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</div>
</div>
);
}
if (results.get('accounts') && results.get('accounts').size > 0) { if (results.get('accounts') && results.get('accounts').size > 0) {
count += results.get('accounts').size; count += results.get('accounts').size;
accounts = ( accounts = (

View File

@ -1,14 +1,8 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SearchResults from '../components/search_results'; import SearchResults from '../components/search_results';
import { fetchTrends } from '../../../actions/trends';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
results: state.getIn(['search', 'results']), results: state.getIn(['search', 'results']),
trends: state.getIn(['trends', 'items']),
}); });
const mapDispatchToProps = dispatch => ({ export default connect(mapStateToProps)(SearchResults);
fetchTrends: () => dispatch(fetchTrends()),
});
export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);

View File

@ -1,71 +0,0 @@
import classNames from 'classnames';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage, defineMessages } from 'react-intl';
import Hashtag from '../../../components/hashtag';
import { Link } from 'react-router-dom';
const messages = defineMessages({
refresh_trends: { id: 'trends.refresh', defaultMessage: 'Refresh' },
});
export default class Trends extends ImmutablePureComponent {
static defaultProps = {
loading: false,
};
static propTypes = {
trends: ImmutablePropTypes.list,
loading: PropTypes.bool.isRequired,
showTrends: PropTypes.bool.isRequired,
fetchTrends: PropTypes.func.isRequired,
toggleTrends: PropTypes.func.isRequired,
};
componentDidMount () {
setTimeout(() => this.props.fetchTrends(), 5000);
}
handleRefreshTrends = () => {
this.props.fetchTrends();
}
handleToggle = () => {
this.props.toggleTrends(!this.props.showTrends);
}
render () {
const { intl, trends, loading, showTrends } = this.props;
if (!trends || trends.size < 1) {
return null;
}
return (
<div className='getting-started__trends'>
<div className='column-header__wrapper'>
<h1 className='column-header'>
<button>
<i className='fa fa-fire fa-fw' />
<FormattedMessage id='trends.header' defaultMessage='Trending now' />
</button>
<div className='column-header__buttons'>
{showTrends && <button onClick={this.handleRefreshTrends} className='column-header__button' title={intl.formatMessage(messages.refresh_trends)} aria-label={intl.formatMessage(messages.refresh_trends)} disabled={loading}><i className={classNames('fa', 'fa-refresh', { 'fa-spin': loading })} /></button>}
<button onClick={this.handleToggle} className='column-header__button'><i className={classNames('fa', showTrends ? 'fa-chevron-down' : 'fa-chevron-up')} /></button>
</div>
</h1>
</div>
{showTrends && <div className='getting-started__scrollable'>
{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
<Link to='/trends' className='load-more'><FormattedMessage id='status.load_more' defaultMessage='Load more' /></Link>
</div>}
</div>
);
}
}

View File

@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import { injectIntl } from 'react-intl';
import { fetchTrends } from '../../../actions/trends';
import Trends from '../components/trends';
import { changeSetting } from '../../../actions/settings';
const mapStateToProps = state => ({
trends: state.getIn(['trends', 'items']),
loading: state.getIn(['trends', 'isLoading']),
showTrends: state.getIn(['settings', 'trends', 'show']),
});
const mapDispatchToProps = dispatch => ({
fetchTrends: () => dispatch(fetchTrends()),
toggleTrends: show => dispatch(changeSetting(['trends', 'show'], show)),
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Trends));

View File

@ -12,7 +12,6 @@ import { fetchFollowRequests } from '../../actions/accounts';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import NavigationBar from '../compose/components/navigation_bar'; import NavigationBar from '../compose/components/navigation_bar';
import TrendsContainer from './containers/trends_container';
const messages = defineMessages({ const messages = defineMessages({
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
@ -132,8 +131,6 @@ export default class GettingStarted extends ImmutablePureComponent {
{navItems} {navItems}
</div> </div>
{multiColumn && <TrendsContainer />}
{!multiColumn && <div className='flex-spacer' />} {!multiColumn && <div className='flex-spacer' />}
<div className='getting-started getting-started__footer'> <div className='getting-started getting-started__footer'>

View File

@ -42,7 +42,6 @@ import {
Mutes, Mutes,
PinnedStatuses, PinnedStatuses,
Lists, Lists,
Trends,
} from './util/async-components'; } from './util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { me } from '../../initial_state'; import { me } from '../../initial_state';
@ -155,7 +154,6 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} /> <WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} />
<WrappedRoute path='/trends' component={Trends} content={children} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} /> <WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />

View File

@ -129,7 +129,3 @@ export function EmbedModal () {
export function ListEditor () { export function ListEditor () {
return import(/* webpackChunkName: "features/list_editor" */'../../list_editor'); return import(/* webpackChunkName: "features/list_editor" */'../../list_editor');
} }
export function Trends () {
return import(/* webpackChunkName: "features/trends" */'../../trends');
}

View File

@ -26,7 +26,6 @@ import height_cache from './height_cache';
import custom_emojis from './custom_emojis'; import custom_emojis from './custom_emojis';
import lists from './lists'; import lists from './lists';
import listEditor from './list_editor'; import listEditor from './list_editor';
import trends from './trends';
const reducers = { const reducers = {
dropdown_menu, dropdown_menu,
@ -56,7 +55,6 @@ const reducers = {
custom_emojis, custom_emojis,
lists, lists,
listEditor, listEditor,
trends,
}; };
export default combineReducers(reducers); export default combineReducers(reducers);

View File

@ -1,23 +0,0 @@
import { TRENDS_FETCH_REQUEST, TRENDS_FETCH_SUCCESS, TRENDS_FETCH_FAIL } from '../actions/trends';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const initialState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
});
export default function trendsReducer(state = initialState, action) {
switch(action.type) {
case TRENDS_FETCH_REQUEST:
return state.set('isLoading', true);
case TRENDS_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('items', fromJS(action.trends));
map.set('isLoading', false);
});
case TRENDS_FETCH_FAIL:
return state.set('isLoading', false);
default:
return state;
}
};

View File

@ -1,9 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class TrendingTags class TrendingTags
KEY = 'trending_tags'
EXPIRE_HISTORY_AFTER = 7.days.seconds EXPIRE_HISTORY_AFTER = 7.days.seconds
THRESHOLD = 5
class << self class << self
def record_use!(tag, account, at_time = Time.now.utc) def record_use!(tag, account, at_time = Time.now.utc)
@ -11,30 +9,10 @@ class TrendingTags
increment_historical_use!(tag.id, at_time) increment_historical_use!(tag.id, at_time)
increment_unique_use!(tag.id, account.id, at_time) increment_unique_use!(tag.id, account.id, at_time)
increment_vote!(tag.id, at_time)
end
def get(limit)
tag_ids = redis.zrevrange(KEY, 0, limit).map(&:to_i)
tags = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h
tag_ids.map { |tag_id| tags[tag_id] }.compact
end end
private private
def increment_vote!(tag_id, at_time)
expected = redis.pfcount("activity:tags:#{tag_id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f
expected = 1.0 if expected.zero?
observed = redis.pfcount("activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts").to_f
if expected > observed || observed < THRESHOLD
redis.zrem(KEY, tag_id.to_s)
else
score = ((observed - expected)**2) / expected
redis.zadd(KEY, score, tag_id.to_s)
end
end
def increment_historical_use!(tag_id, at_time) def increment_historical_use!(tag_id, at_time)
key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}" key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}"
redis.incrby(key, 1) redis.incrby(key, 1)

View File

@ -271,7 +271,6 @@ Rails.application.routes.draw do
resources :favourites, only: [:index] resources :favourites, only: [:index]
resources :bookmarks, only: [:index] resources :bookmarks, only: [:index]
resources :reports, only: [:index, :create] resources :reports, only: [:index, :create]
resources :trends, only: [:index]
namespace :apps do namespace :apps do
get :verify_credentials, to: 'credentials#show' get :verify_credentials, to: 'credentials#show'