Add hashtag trendline support to glitch-soc flavour

Port Mastodon's hashtag stats thing to glitch-soc.
This doesn't change how hashtags are ordered, and doesn't add a trending
hashtags section, but it does change how hashtag searches are rendered,
displaying a trend line alongside each hashtag.
main
Thibaut Girka 2018-08-20 11:29:42 +02:00 committed by ThibG
parent 360fbf1bd4
commit 801919fc9b
6 changed files with 123 additions and 22 deletions

View File

@ -32,7 +32,7 @@ export function submitSearch() {
dispatch(fetchSearchRequest()); dispatch(fetchSearchRequest());
api(getState).get('/api/v1/search', { api(getState).get('/api/v2/search', {
params: { params: {
q: value, q: value,
resolve: true, resolve: true,

View File

@ -0,0 +1,34 @@
import React from 'react';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { Link } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
const Hashtag = ({ hashtag }) => (
<div className='trends__item'>
<div className='trends__item__name'>
<Link to={`/timelines/tag/${hashtag.get('name')}`}>
#<span>{hashtag.get('name')}</span>
</Link>
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
</div>
<div className='trends__item__current'>
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
</div>
<div className='trends__item__sparkline'>
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</div>
</div>
);
Hashtag.propTypes = {
hashtag: ImmutablePropTypes.map.isRequired,
};
export default Hashtag;

View File

@ -12,6 +12,7 @@ import { Link } from 'react-router-dom';
// Components. // Components.
import AccountContainer from 'flavours/glitch/containers/account_container'; import AccountContainer from 'flavours/glitch/containers/account_container';
import StatusContainer from 'flavours/glitch/containers/status_container'; import StatusContainer from 'flavours/glitch/containers/status_container';
import Hashtag from 'flavours/glitch/components/hashtag';
// Utils. // Utils.
import Motion from 'flavours/glitch/util/optional_motion'; import Motion from 'flavours/glitch/util/optional_motion';
@ -98,15 +99,7 @@ export default function DrawerResults ({
<section> <section>
<h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> <h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
{hashtags.map( {hashtags.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
hashtag => (
<Link
className='hashtag'
key={hashtag}
to={`/timelines/tag/${hashtag}`}
>#{hashtag}</Link>
)
)}
</section> </section>
) : null} ) : null}
</div> </div>

View File

@ -9,7 +9,7 @@ import {
COMPOSE_REPLY, COMPOSE_REPLY,
COMPOSE_DIRECT, COMPOSE_DIRECT,
} from 'flavours/glitch/actions/compose'; } from 'flavours/glitch/actions/compose';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
value: '', value: '',
@ -39,7 +39,7 @@ export default function search(state = initialState, action) {
return state.set('results', ImmutableMap({ return state.set('results', ImmutableMap({
accounts: ImmutableList(action.results.accounts.map(item => item.id)), accounts: ImmutableList(action.results.accounts.map(item => item.id)),
statuses: ImmutableList(action.results.statuses.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)),
hashtags: ImmutableList(action.results.hashtags), hashtags: fromJS(action.results.hashtags),
})).set('submitted', true); })).set('submitted', true);
default: default:
return state; return state;

View File

@ -90,16 +90,80 @@
font-weight: 500; font-weight: 500;
} }
.search-results__hashtag { .trends {
display: block; &__header {
padding: 10px; color: $dark-text-color;
color: $secondary-text-color; background: lighten($ui-base-color, 2%);
text-decoration: none; border-bottom: 1px solid darken($ui-base-color, 4%);
font-weight: 500;
padding: 15px;
font-size: 16px;
cursor: default;
&:hover, .fa {
&:active, display: inline-block;
&:focus { margin-right: 5px;
color: lighten($secondary-text-color, 4%); }
text-decoration: underline; }
&__item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:last-child {
border-bottom: 0;
}
&__name {
flex: 1 1 auto;
color: $dark-text-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
strong {
font-weight: 500;
}
a {
color: $darker-text-color;
text-decoration: none;
font-size: 14px;
font-weight: 500;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover,
&:focus,
&:active {
span {
text-decoration: underline;
}
}
}
}
&__current {
flex: 0 0 auto;
width: 100px;
font-size: 24px;
line-height: 36px;
font-weight: 500;
text-align: center;
color: $secondary-text-color;
}
&__sparkline {
flex: 0 0 auto;
width: 50px;
path {
stroke: lighten($highlight-text-color, 6%) !important;
}
}
} }
} }

View File

@ -0,0 +1,10 @@
import React, { Fragment } from 'react';
import { FormattedNumber } from 'react-intl';
export const shortNumberFormat = number => {
if (number < 1000) {
return <FormattedNumber value={number} />;
} else {
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>;
}
};