Merge branch 'master' into glitch-soc/merge-upstream

This commit is contained in:
Thibaut Girka
2018-05-29 13:52:26 +02:00
37 changed files with 553 additions and 93 deletions

View File

@@ -33,7 +33,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,32 @@
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

@@ -25,6 +25,7 @@ export default class ScrollableList extends PureComponent {
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
prepend: PropTypes.node,
alwaysPrepend: PropTypes.bool,
emptyMessage: PropTypes.node,
children: PropTypes.node,
};
@@ -140,7 +141,7 @@ export default class ScrollableList extends PureComponent {
}
render () {
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props;
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
const { fullscreen } = this.state;
const childrenCount = React.Children.count(children);
@@ -172,8 +173,12 @@ export default class ScrollableList extends PureComponent {
);
} else {
scrollableArea = (
<div className='empty-column-indicator' ref={this.setRef}>
{emptyMessage}
<div style={{ flex: '1 1 auto', display: 'flex', flexDirection: 'column' }}>
{alwaysPrepend && prepend}
<div className='empty-column-indicator' ref={this.setRef}>
{emptyMessage}
</div>
</div>
);
}

View File

@@ -24,6 +24,7 @@ export default class StatusList extends ImmutablePureComponent {
hasMore: PropTypes.bool,
prepend: PropTypes.node,
emptyMessage: PropTypes.node,
alwaysPrepend: PropTypes.bool,
};
static defaultProps = {

View File

@@ -127,6 +127,7 @@ export default class CommunityTimeline extends React.PureComponent {
<StatusListContainer
prepend={headline}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`community_timeline-${columnId}`}
timelineId={`community${onlyMedia ? ':media' : ''}`}

View File

@@ -1,28 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, FormattedNumber } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
import { Link } from 'react-router-dom';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
const shortNumberFormat = number => {
if (number < 1000) {
return <FormattedNumber value={number} />;
} else {
return <React.Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</React.Fragment>;
}
};
const renderHashtag = hashtag => (
<div className='trends__item' key={hashtag.get('name')}>
<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>
);
export default class SearchResults extends ImmutablePureComponent {
static propTypes = {
results: ImmutablePropTypes.map.isRequired,
trends: ImmutablePropTypes.list,
fetchTrends: PropTypes.func.isRequired,
};
componentDidMount () {
const { fetchTrends } = this.props;
fetchTrends();
}
render () {
const { results } = this.props;
const { results, trends } = this.props;
let accounts, statuses, hashtags;
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 => renderHashtag(hashtag))}
</div>
</div>
);
}
if (results.get('accounts') && results.get('accounts').size > 0) {
count += results.get('accounts').size;
accounts = (
<div className='search-results__section'>
<h5><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
<h5><i className='fa fa-fw fa-users' /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
</div>
@@ -33,7 +87,7 @@ export default class SearchResults extends ImmutablePureComponent {
count += results.get('statuses').size;
statuses = (
<div className='search-results__section'>
<h5><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
<h5><i className='fa fa-fw fa-quote-right' /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
</div>
@@ -44,13 +98,9 @@ export default class SearchResults extends ImmutablePureComponent {
count += results.get('hashtags').size;
hashtags = (
<div className='search-results__section'>
<h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
<h5><i className='fa fa-fw fa-hashtag' /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
{results.get('hashtags').map(hashtag => (
<Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}>
#{hashtag}
</Link>
))}
{results.get('hashtags').map(hashtag => renderHashtag(hashtag))}
</div>
);
}
@@ -58,6 +108,7 @@ export default class SearchResults extends ImmutablePureComponent {
return (
<div className='search-results'>
<div className='search-results__header'>
<i className='fa fa-search fa-fw' />
<FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} />
</div>

View File

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

View File

@@ -101,7 +101,7 @@ export default class Compose extends React.PureComponent {
{(multiColumn || isSearchPage) && <SearchContainer /> }
<div className='drawer__pager'>
<div className='drawer__inner' onFocus={this.onFocus}>
{!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}>
<NavigationContainer onClose={this.onBlur} />
<ComposeFormContainer />
{multiColumn && (
@@ -109,7 +109,7 @@ export default class Compose extends React.PureComponent {
<img alt='' draggable='false' src={elephantUIPlane} />
</div>
)}
</div>
</div>}
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
{({ x }) => (

View File

@@ -127,6 +127,7 @@ export default class PublicTimeline extends React.PureComponent {
<StatusListContainer
prepend={headline}
alwaysPrepend
timelineId={`public${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
trackScroll={!pinned}

View File

@@ -62,31 +62,28 @@ const makeMapStateToProps = () => {
if (status) {
ancestorsIds = ancestorsIds.withMutations(mutable => {
function addAncestor(id) {
if (id) {
const inReplyTo = state.getIn(['contexts', 'inReplyTos', id]);
let id = status.get('in_reply_to_id');
mutable.unshift(id);
addAncestor(inReplyTo);
}
while (id) {
mutable.unshift(id);
id = state.getIn(['contexts', 'inReplyTos', id]);
}
addAncestor(status.get('in_reply_to_id'));
});
descendantsIds = descendantsIds.withMutations(mutable => {
function addDescendantOf(id) {
const ids = [status.get('id')];
while (ids.length > 0) {
let id = ids.shift();
const replies = state.getIn(['contexts', 'replies', id]);
if (replies) {
replies.forEach(reply => {
mutable.push(reply);
addDescendantOf(reply);
ids.unshift(reply);
});
}
}
addDescendantOf(status.get('id'));
});
}

View File

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

View File

@@ -9,7 +9,7 @@ import {
COMPOSE_REPLY,
COMPOSE_DIRECT,
} from '../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

@@ -0,0 +1,13 @@
import { TRENDS_FETCH_SUCCESS } from '../actions/trends';
import { fromJS } from 'immutable';
const initialState = null;
export default function trendsReducer(state = initialState, action) {
switch(action.type) {
case TRENDS_FETCH_SUCCESS:
return fromJS(action.trends);
default:
return state;
}
};

View File

@@ -49,7 +49,7 @@ self.addEventListener('fetch', function(event) {
return response;
}));
} else if (storageFreeable && process.env.CDN_HOST ? url.host === process.env.CDN_HOST : url.pathname.startsWith('/system/')) {
} else if (storageFreeable && (ATTACHMENT_HOST ? url.host === ATTACHMENT_HOST : url.pathname.startsWith('/system/'))) {
event.respondWith(openSystemCache().then(cache => {
return cache.match(event.request.url).then(cached => {
if (cached === undefined) {