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.
This commit is contained in:
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());
api(getState).get('/api/v1/search', {
api(getState).get('/api/v2/search', {
params: {
q: value,
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.
import AccountContainer from 'flavours/glitch/containers/account_container';
import StatusContainer from 'flavours/glitch/containers/status_container';
import Hashtag from 'flavours/glitch/components/hashtag';
// Utils.
import Motion from 'flavours/glitch/util/optional_motion';
@ -98,15 +99,7 @@ export default function DrawerResults ({
<section>
<h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
{hashtags.map(
hashtag => (
<Link
className='hashtag'
key={hashtag}
to={`/timelines/tag/${hashtag}`}
>#{hashtag}</Link>
)
)}
{hashtags.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</section>
) : null}
</div>

View File

@ -9,7 +9,7 @@ import {
COMPOSE_REPLY,
COMPOSE_DIRECT,
} 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({
value: '',
@ -39,7 +39,7 @@ export default function search(state = initialState, action) {
return state.set('results', ImmutableMap({
accounts: ImmutableList(action.results.accounts.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);
default:
return state;

View File

@ -90,16 +90,80 @@
font-weight: 500;
}
.search-results__hashtag {
display: block;
padding: 10px;
color: $secondary-text-color;
text-decoration: none;
.trends {
&__header {
color: $dark-text-color;
background: lighten($ui-base-color, 2%);
border-bottom: 1px solid darken($ui-base-color, 4%);
font-weight: 500;
padding: 15px;
font-size: 16px;
cursor: default;
&:hover,
&:active,
&:focus {
color: lighten($secondary-text-color, 4%);
text-decoration: underline;
.fa {
display: inline-block;
margin-right: 5px;
}
}
&__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>;
}
};