Add loading indicator for trending tags (#7693)

pull/528/head
Yamagishi Kazutoshi 2018-06-01 21:22:42 +09:00 committed by Eugen Rochko
parent bfa12239e8
commit 69b45350fe
6 changed files with 104 additions and 41 deletions

View File

@ -4,7 +4,7 @@ import { fetchTrends } from '../../../actions/trends';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
results: state.getIn(['search', 'results']), results: state.getIn(['search', 'results']),
trends: state.get('trends'), trends: state.getIn(['trends', 'items']),
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({

View File

@ -0,0 +1,58 @@
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';
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,
};
componentDidMount () {
setTimeout(() => this.props.fetchTrends(), 5000);
}
handleRefreshTrends = () => {
this.props.fetchTrends();
}
render () {
const { intl, trends, loading } = 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'>
<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>
</div>
</h1>
</div>
<div className='getting-started__scrollable'>{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}</div>
</div>
);
}
}

View File

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

View File

@ -11,9 +11,8 @@ import { me } from '../../initial_state';
import { fetchFollowRequests } from '../../actions/accounts'; 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 { fetchTrends } from '../../actions/trends';
import Hashtag from '../../components/hashtag';
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' },
@ -30,7 +29,6 @@ const messages = defineMessages({
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
refresh_trends: { id: 'trends.refresh', defaultMessage: 'Refresh' },
discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' }, discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' },
personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' }, personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
security: { id: 'navigation_bar.security', defaultMessage: 'Security' }, security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
@ -39,12 +37,10 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
myAccount: state.getIn(['accounts', me]), myAccount: state.getIn(['accounts', me]),
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
trends: state.get('trends'),
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
fetchFollowRequests: () => dispatch(fetchFollowRequests()), fetchFollowRequests: () => dispatch(fetchFollowRequests()),
fetchTrends: () => dispatch(fetchTrends()),
}); });
const badgeDisplay = (number, limit) => { const badgeDisplay = (number, limit) => {
@ -69,7 +65,6 @@ export default class GettingStarted extends ImmutablePureComponent {
fetchFollowRequests: PropTypes.func.isRequired, fetchFollowRequests: PropTypes.func.isRequired,
unreadFollowRequests: PropTypes.number, unreadFollowRequests: PropTypes.number,
unreadNotifications: PropTypes.number, unreadNotifications: PropTypes.number,
trends: ImmutablePropTypes.list,
}; };
componentDidMount () { componentDidMount () {
@ -78,16 +73,10 @@ export default class GettingStarted extends ImmutablePureComponent {
if (myAccount.get('locked')) { if (myAccount.get('locked')) {
fetchFollowRequests(); fetchFollowRequests();
} }
setTimeout(() => this.props.fetchTrends(), 5000);
}
handleRefreshTrends = () => {
this.props.fetchTrends();
} }
render () { render () {
const { intl, myAccount, multiColumn, unreadFollowRequests, trends } = this.props; const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
const navItems = []; const navItems = [];
let i = 1; let i = 1;
@ -135,21 +124,7 @@ export default class GettingStarted extends ImmutablePureComponent {
{navItems} {navItems}
</div> </div>
{multiColumn && trends && <div className='getting-started__trends'> {multiColumn && <TrendsContainer />}
<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'>
<button onClick={this.handleRefreshTrends} className='column-header__button' title={intl.formatMessage(messages.refresh_trends)} aria-label={intl.formatMessage(messages.refresh_trends)}><i className='fa fa-refresh' /></button>
</div>
</h1>
</div>
<div className='getting-started__scrollable'>{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}</div>
</div>}
{!multiColumn && <div className='flex-spacer' />} {!multiColumn && <div className='flex-spacer' />}

View File

@ -1036,6 +1036,19 @@
], ],
"path": "app/javascript/mastodon/features/follow_requests/index.json" "path": "app/javascript/mastodon/features/follow_requests/index.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Refresh",
"id": "trends.refresh"
},
{
"defaultMessage": "Trending now",
"id": "trends.header"
}
],
"path": "app/javascript/mastodon/features/getting_started/components/trends.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -1094,10 +1107,6 @@
"defaultMessage": "Lists", "defaultMessage": "Lists",
"id": "navigation_bar.lists" "id": "navigation_bar.lists"
}, },
{
"defaultMessage": "Refresh",
"id": "trends.refresh"
},
{ {
"defaultMessage": "Discover", "defaultMessage": "Discover",
"id": "navigation_bar.discover" "id": "navigation_bar.discover"
@ -1114,10 +1123,6 @@
"defaultMessage": "Getting started", "defaultMessage": "Getting started",
"id": "getting_started.heading" "id": "getting_started.heading"
}, },
{
"defaultMessage": "Trending now",
"id": "trends.header"
},
{ {
"defaultMessage": "Hotkeys", "defaultMessage": "Hotkeys",
"id": "navigation_bar.keyboard_shortcuts" "id": "navigation_bar.keyboard_shortcuts"

View File

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