Add explore page to web UI (#17123)
* Add explore page to web UI * Fix not removing loaded statuses from trends on mute/block action
This commit is contained in:
parent
27965ce5ed
commit
d4592bbfcd
22 changed files with 727 additions and 63 deletions
|
@ -1,31 +1,94 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST';
|
export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
|
||||||
export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS';
|
export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS';
|
||||||
export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL';
|
export const TRENDS_TAGS_FETCH_FAIL = 'TRENDS_TAGS_FETCH_FAIL';
|
||||||
|
|
||||||
export const fetchTrends = () => (dispatch, getState) => {
|
export const TRENDS_LINKS_FETCH_REQUEST = 'TRENDS_LINKS_FETCH_REQUEST';
|
||||||
dispatch(fetchTrendsRequest());
|
export const TRENDS_LINKS_FETCH_SUCCESS = 'TRENDS_LINKS_FETCH_SUCCESS';
|
||||||
|
export const TRENDS_LINKS_FETCH_FAIL = 'TRENDS_LINKS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST';
|
||||||
|
export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS';
|
||||||
|
export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const fetchTrendingHashtags = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchTrendingHashtagsRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
.get('/api/v1/trends')
|
.get('/api/v1/trends/tags')
|
||||||
.then(({ data }) => dispatch(fetchTrendsSuccess(data)))
|
.then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data)))
|
||||||
.catch(err => dispatch(fetchTrendsFail(err)));
|
.catch(err => dispatch(fetchTrendingHashtagsFail(err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTrendsRequest = () => ({
|
export const fetchTrendingHashtagsRequest = () => ({
|
||||||
type: TRENDS_FETCH_REQUEST,
|
type: TRENDS_TAGS_FETCH_REQUEST,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchTrendsSuccess = trends => ({
|
export const fetchTrendingHashtagsSuccess = trends => ({
|
||||||
type: TRENDS_FETCH_SUCCESS,
|
type: TRENDS_TAGS_FETCH_SUCCESS,
|
||||||
trends,
|
trends,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchTrendsFail = error => ({
|
export const fetchTrendingHashtagsFail = error => ({
|
||||||
type: TRENDS_FETCH_FAIL,
|
type: TRENDS_TAGS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
skipLoading: true,
|
||||||
|
skipAlert: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingLinks = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchTrendingLinksRequest());
|
||||||
|
|
||||||
|
api(getState)
|
||||||
|
.get('/api/v1/trends/links')
|
||||||
|
.then(({ data }) => dispatch(fetchTrendingLinksSuccess(data)))
|
||||||
|
.catch(err => dispatch(fetchTrendingLinksFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTrendingLinksRequest = () => ({
|
||||||
|
type: TRENDS_LINKS_FETCH_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingLinksSuccess = trends => ({
|
||||||
|
type: TRENDS_LINKS_FETCH_SUCCESS,
|
||||||
|
trends,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingLinksFail = error => ({
|
||||||
|
type: TRENDS_LINKS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
skipLoading: true,
|
||||||
|
skipAlert: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingStatuses = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchTrendingStatusesRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/trends/statuses').then(({ data }) => {
|
||||||
|
dispatch(importFetchedStatuses(data));
|
||||||
|
dispatch(fetchTrendingStatusesSuccess(data));
|
||||||
|
}).catch(err => dispatch(fetchTrendingStatusesFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTrendingStatusesRequest = () => ({
|
||||||
|
type: TRENDS_STATUSES_FETCH_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingStatusesSuccess = statuses => ({
|
||||||
|
type: TRENDS_STATUSES_FETCH_SUCCESS,
|
||||||
|
statuses,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingStatusesFail = error => ({
|
||||||
|
type: TRENDS_STATUSES_FETCH_FAIL,
|
||||||
error,
|
error,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
skipAlert: true,
|
skipAlert: true,
|
||||||
|
|
|
@ -38,7 +38,7 @@ class SilentErrorBoundary extends React.Component {
|
||||||
*
|
*
|
||||||
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
||||||
*/
|
*/
|
||||||
const accountsCountRenderer = (displayNumber, pluralReady) => (
|
export const accountsCountRenderer = (displayNumber, pluralReady) => (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='trends.counter_by_accounts'
|
id='trends.counter_by_accounts'
|
||||||
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking'
|
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking'
|
||||||
|
|
|
@ -77,6 +77,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
onBookmark: PropTypes.func,
|
onBookmark: PropTypes.func,
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
|
withCounters: PropTypes.bool,
|
||||||
scrollKey: PropTypes.string,
|
scrollKey: PropTypes.string,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -226,7 +227,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, relationship, intl, withDismiss, scrollKey } = this.props;
|
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
||||||
|
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
@ -331,8 +332,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||||
|
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
prepend: PropTypes.node,
|
prepend: PropTypes.node,
|
||||||
emptyMessage: PropTypes.node,
|
emptyMessage: PropTypes.node,
|
||||||
alwaysPrepend: PropTypes.bool,
|
alwaysPrepend: PropTypes.bool,
|
||||||
|
withCounters: PropTypes.bool,
|
||||||
timelineId: PropTypes.string,
|
timelineId: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -100,6 +101,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
scrollKey={this.props.scrollKey}
|
scrollKey={this.props.scrollKey}
|
||||||
showThread
|
showThread
|
||||||
|
withCounters={this.props.withCounters}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : null;
|
) : null;
|
||||||
|
@ -114,6 +116,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
showThread
|
showThread
|
||||||
|
withCounters={this.props.withCounters}
|
||||||
/>
|
/>
|
||||||
)).concat(scrollableContent);
|
)).concat(scrollableContent);
|
||||||
}
|
}
|
||||||
|
|
51
app/javascript/mastodon/features/explore/components/story.js
Normal file
51
app/javascript/mastodon/features/explore/components/story.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Blurhash from 'mastodon/components/blurhash';
|
||||||
|
import { accountsCountRenderer } from 'mastodon/components/hashtag';
|
||||||
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export default class Story extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
url: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
publisher: PropTypes.string,
|
||||||
|
sharedTimes: PropTypes.number,
|
||||||
|
thumbnail: PropTypes.string,
|
||||||
|
blurhash: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
thumbnailLoaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleImageLoad = () => this.setState({ thumbnailLoaded: true });
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { url, title, publisher, sharedTimes, thumbnail, blurhash } = this.props;
|
||||||
|
|
||||||
|
const { thumbnailLoaded } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a className='story' href={url} target='blank' rel='noopener'>
|
||||||
|
<div className='story__details'>
|
||||||
|
<div className='story__details__publisher'>{publisher ? publisher : <Skeleton width={50} />}</div>
|
||||||
|
<div className='story__details__title'>{title ? title : <Skeleton />}</div>
|
||||||
|
<div className='story__details__shared'>{typeof sharedTimes === 'number' ? <ShortNumber value={sharedTimes} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='story__thumbnail'>
|
||||||
|
{thumbnail ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
|
||||||
|
<img src={thumbnail} onLoad={this.handleImageLoad} alt='' role='presentation' />
|
||||||
|
</React.Fragment>
|
||||||
|
) : <Skeleton />}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
91
app/javascript/mastodon/features/explore/index.js
Normal file
91
app/javascript/mastodon/features/explore/index.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Column from 'mastodon/components/column';
|
||||||
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
|
import { NavLink, Switch, Route } from 'react-router-dom';
|
||||||
|
import Links from './links';
|
||||||
|
import Tags from './tags';
|
||||||
|
import Statuses from './statuses';
|
||||||
|
import Suggestions from './suggestions';
|
||||||
|
import Search from 'mastodon/features/compose/containers/search_container';
|
||||||
|
import SearchResults from './results';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'explore.title', defaultMessage: 'Explore' },
|
||||||
|
searchResults: { id: 'explore.search_results', defaultMessage: 'Search results' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
layout: state.getIn(['meta', 'layout']),
|
||||||
|
isSearching: state.getIn(['search', 'submitted']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Explore extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
isSearching: PropTypes.bool,
|
||||||
|
layout: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, multiColumn, isSearching, layout } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
|
{layout === 'mobile' ? (
|
||||||
|
<div className='explore__search-header'>
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ColumnHeader
|
||||||
|
icon={isSearching ? 'search' : 'globe'}
|
||||||
|
title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='scrollable scrollable--flex'>
|
||||||
|
{isSearching ? (
|
||||||
|
<SearchResults />
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className='account__section-headline'>
|
||||||
|
<NavLink exact to='/explore'><FormattedMessage id='explore.trending_statuses' defaultMessage='Posts' /></NavLink>
|
||||||
|
<NavLink exact to='/explore/tags'><FormattedMessage id='explore.trending_tags' defaultMessage='Hashtags' /></NavLink>
|
||||||
|
<NavLink exact to='/explore/links'><FormattedMessage id='explore.trending_links' defaultMessage='News' /></NavLink>
|
||||||
|
<NavLink exact to='/explore/suggestions'><FormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch>
|
||||||
|
<Route path='/explore/tags' component={Tags} />
|
||||||
|
<Route path='/explore/links' component={Links} />
|
||||||
|
<Route path='/explore/suggestions' component={Suggestions} />
|
||||||
|
<Route exact path={['/explore', '/explore/posts', '/search']} component={Statuses} componentParams={{ multiColumn }} />
|
||||||
|
</Switch>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
48
app/javascript/mastodon/features/explore/links.js
Normal file
48
app/javascript/mastodon/features/explore/links.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Story from './components/story';
|
||||||
|
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchTrendingLinks } from 'mastodon/actions/trends';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
links: state.getIn(['trends', 'links', 'items']),
|
||||||
|
isLoading: state.getIn(['trends', 'links', 'isLoading']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Links extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
links: ImmutablePropTypes.list,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchTrendingLinks());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, links } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='explore__links'>
|
||||||
|
{isLoading ? (<LoadingIndicator />) : links.map(link => (
|
||||||
|
<Story
|
||||||
|
key={link.get('id')}
|
||||||
|
url={link.get('url')}
|
||||||
|
title={link.get('title')}
|
||||||
|
publisher={link.get('provider_name')}
|
||||||
|
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
|
||||||
|
thumbnail={link.get('image')}
|
||||||
|
blurhash={link.get('blurhash')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
113
app/javascript/mastodon/features/explore/results.js
Normal file
113
app/javascript/mastodon/features/explore/results.js
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { expandSearch } from 'mastodon/actions/search';
|
||||||
|
import Account from 'mastodon/containers/account_container';
|
||||||
|
import Status from 'mastodon/containers/status_container';
|
||||||
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
import LoadMore from 'mastodon/components/load_more';
|
||||||
|
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
isLoading: state.getIn(['search', 'isLoading']),
|
||||||
|
results: state.getIn(['search', 'results']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const appendLoadMore = (id, list, onLoadMore) => {
|
||||||
|
if (list.size >= 5) {
|
||||||
|
return list.push(<LoadMore key={`${id}-load-more`} visible onClick={onLoadMore} />);
|
||||||
|
} else {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAccounts = (results, onLoadMore) => appendLoadMore('accounts', results.get('accounts').map(item => (
|
||||||
|
<Account key={`account-${item}`} id={item} />
|
||||||
|
)), onLoadMore);
|
||||||
|
|
||||||
|
const renderHashtags = (results, onLoadMore) => appendLoadMore('hashtags', results.get('hashtags').map(item => (
|
||||||
|
<Hashtag key={`tag-${item.get('name')}`} hashtag={item} />
|
||||||
|
)), onLoadMore);
|
||||||
|
|
||||||
|
const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', results.get('statuses').map(item => (
|
||||||
|
<Status key={`status-${item}`} id={item} />
|
||||||
|
)), onLoadMore);
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Results extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
results: ImmutablePropTypes.map,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
type: 'all',
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelectAll = () => this.setState({ type: 'all' });
|
||||||
|
handleSelectAccounts = () => this.setState({ type: 'accounts' });
|
||||||
|
handleSelectHashtags = () => this.setState({ type: 'hashtags' });
|
||||||
|
handleSelectStatuses = () => this.setState({ type: 'statuses' });
|
||||||
|
handleLoadMoreAccounts = () => this.loadMore('accounts');
|
||||||
|
handleLoadMoreStatuses = () => this.loadMore('statuses');
|
||||||
|
handleLoadMoreHashtags = () => this.loadMore('hashtags');
|
||||||
|
|
||||||
|
loadMore (type) {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(expandSearch(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, results } = this.props;
|
||||||
|
const { type } = this.state;
|
||||||
|
|
||||||
|
let filteredResults = ImmutableList();
|
||||||
|
|
||||||
|
if (!isLoading) {
|
||||||
|
switch(type) {
|
||||||
|
case 'all':
|
||||||
|
filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts), renderHashtags(results, this.handleLoadMoreHashtags), renderStatuses(results, this.handleLoadMoreStatuses));
|
||||||
|
break;
|
||||||
|
case 'accounts':
|
||||||
|
filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts));
|
||||||
|
break;
|
||||||
|
case 'hashtags':
|
||||||
|
filteredResults = filteredResults.concat(renderHashtags(results, this.handleLoadMoreHashtags));
|
||||||
|
break;
|
||||||
|
case 'statuses':
|
||||||
|
filteredResults = filteredResults.concat(renderStatuses(results, this.handleLoadMoreStatuses));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredResults.size === 0) {
|
||||||
|
filteredResults = (
|
||||||
|
<div className='empty-column-indicator'>
|
||||||
|
<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className='account__section-headline'>
|
||||||
|
<button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
|
||||||
|
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='People' /></button>
|
||||||
|
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
|
||||||
|
<button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='explore__search-results'>
|
||||||
|
{isLoading ? (<LoadingIndicator />) : filteredResults}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
48
app/javascript/mastodon/features/explore/statuses.js
Normal file
48
app/javascript/mastodon/features/explore/statuses.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import StatusList from 'mastodon/components/status_list';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchTrendingStatuses } from 'mastodon/actions/trends';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
statusIds: state.getIn(['status_lists', 'trending', 'items']),
|
||||||
|
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Statuses extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusIds: ImmutablePropTypes.list,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchTrendingStatuses());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, statusIds, multiColumn } = this.props;
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusList
|
||||||
|
trackScroll
|
||||||
|
statusIds={statusIds}
|
||||||
|
scrollKey='explore-statuses'
|
||||||
|
hasMore={false}
|
||||||
|
isLoading={isLoading}
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
withCounters
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
app/javascript/mastodon/features/explore/suggestions.js
Normal file
40
app/javascript/mastodon/features/explore/suggestions.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Account from 'mastodon/containers/account_container';
|
||||||
|
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
suggestions: state.getIn(['suggestions', 'items']),
|
||||||
|
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Suggestions extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
suggestions: ImmutablePropTypes.list,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchSuggestions(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, suggestions } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='explore__links'>
|
||||||
|
{isLoading ? (<LoadingIndicator />) : suggestions.map(suggestion => (
|
||||||
|
<Account key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
app/javascript/mastodon/features/explore/tags.js
Normal file
40
app/javascript/mastodon/features/explore/tags.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
|
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
hashtags: state.getIn(['trends', 'tags', 'items']),
|
||||||
|
isLoadingHashtags: state.getIn(['trends', 'tags', 'isLoading']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Tags extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
hashtags: ImmutablePropTypes.list,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchTrendingHashtags());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, hashtags } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='explore__links'>
|
||||||
|
{isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
|
||||||
|
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchTrends } from 'mastodon/actions/trends';
|
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
|
||||||
import Trends from '../components/trends';
|
import Trends from '../components/trends';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
trends: state.getIn(['trends', 'items']),
|
trends: state.getIn(['trends', 'tags', 'items']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
fetchTrends: () => dispatch(fetchTrends()),
|
fetchTrends: () => dispatch(fetchTrendingHashtags()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Trends);
|
export default connect(mapStateToProps, mapDispatchToProps)(Trends);
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import SearchContainer from 'mastodon/features/compose/containers/search_container';
|
|
||||||
import SearchResultsContainer from 'mastodon/features/compose/containers/search_results_container';
|
|
||||||
|
|
||||||
const Search = () => (
|
|
||||||
<div className='column search-page'>
|
|
||||||
<SearchContainer />
|
|
||||||
|
|
||||||
<div className='drawer__pager'>
|
|
||||||
<div className='drawer__inner darker'>
|
|
||||||
<SearchResultsContainer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Search;
|
|
|
@ -53,7 +53,7 @@ const messages = defineMessages({
|
||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/search|^\/getting-started|^\/start/);
|
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/explore|^\/getting-started|^\/start/);
|
||||||
|
|
||||||
export default @(component => injectIntl(component, { withRef: true }))
|
export default @(component => injectIntl(component, { withRef: true }))
|
||||||
class ColumnsArea extends ImmutablePureComponent {
|
class ColumnsArea extends ImmutablePureComponent {
|
||||||
|
|
|
@ -13,6 +13,7 @@ const NavigationPanel = () => (
|
||||||
<NavLink className='column-link column-link--transparent' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
|
||||||
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
|
||||||
<FollowRequestsNavLink />
|
<FollowRequestsNavLink />
|
||||||
|
<NavLink className='column-link column-link--transparent' to='/explore' data-preview-title-id='explore.title' data-preview-icon='globe'><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='explore.title' defaultMessage='Explore' /></NavLink>
|
||||||
<NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
|
||||||
<NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
|
<NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
|
||||||
<NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
||||||
|
|
|
@ -10,9 +10,9 @@ import NotificationsCounterIcon from './notifications_counter_icon';
|
||||||
export const links = [
|
export const links = [
|
||||||
<NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
<NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
|
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
|
<NavLink className='tabs-bar__link optional' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
|
<NavLink className='tabs-bar__link optional' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
|
<NavLink className='tabs-bar__link' to='/explore' data-preview-title-id='tabs_bar.search' data-preview-icon='search' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
|
<NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,8 @@ import {
|
||||||
Mutes,
|
Mutes,
|
||||||
PinnedStatuses,
|
PinnedStatuses,
|
||||||
Lists,
|
Lists,
|
||||||
Search,
|
|
||||||
Directory,
|
Directory,
|
||||||
|
Explore,
|
||||||
FollowRecommendations,
|
FollowRecommendations,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import { me } from '../../initial_state';
|
import { me } from '../../initial_state';
|
||||||
|
@ -167,8 +167,8 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/start' component={FollowRecommendations} content={children} />
|
<WrappedRoute path='/start' component={FollowRecommendations} content={children} />
|
||||||
<WrappedRoute path='/search' component={Search} content={children} />
|
|
||||||
<WrappedRoute path='/directory' component={Directory} content={children} />
|
<WrappedRoute path='/directory' component={Directory} content={children} />
|
||||||
|
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
|
||||||
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
|
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
|
||||||
|
|
|
@ -138,10 +138,6 @@ export function ListAdder () {
|
||||||
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
|
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Search () {
|
|
||||||
return import(/*webpackChunkName: "features/search" */'../../search');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Tesseract () {
|
export function Tesseract () {
|
||||||
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
|
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
|
||||||
}
|
}
|
||||||
|
@ -161,3 +157,7 @@ export function FollowRecommendations () {
|
||||||
export function CompareHistoryModal () {
|
export function CompareHistoryModal () {
|
||||||
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
|
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Explore () {
|
||||||
|
return import(/* webpackChunkName: "features/explore" */'../../explore');
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {
|
import {
|
||||||
SEARCH_CHANGE,
|
SEARCH_CHANGE,
|
||||||
SEARCH_CLEAR,
|
SEARCH_CLEAR,
|
||||||
|
SEARCH_FETCH_REQUEST,
|
||||||
|
SEARCH_FETCH_FAIL,
|
||||||
SEARCH_FETCH_SUCCESS,
|
SEARCH_FETCH_SUCCESS,
|
||||||
SEARCH_SHOW,
|
SEARCH_SHOW,
|
||||||
SEARCH_EXPAND_SUCCESS,
|
SEARCH_EXPAND_SUCCESS,
|
||||||
|
@ -17,6 +19,7 @@ const initialState = ImmutableMap({
|
||||||
submitted: false,
|
submitted: false,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
results: ImmutableMap(),
|
results: ImmutableMap(),
|
||||||
|
isLoading: false,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,12 +40,22 @@ export default function search(state = initialState, action) {
|
||||||
case COMPOSE_MENTION:
|
case COMPOSE_MENTION:
|
||||||
case COMPOSE_DIRECT:
|
case COMPOSE_DIRECT:
|
||||||
return state.set('hidden', true);
|
return state.set('hidden', true);
|
||||||
|
case SEARCH_FETCH_REQUEST:
|
||||||
|
return state.set('isLoading', true);
|
||||||
|
case SEARCH_FETCH_FAIL:
|
||||||
|
return state.set('isLoading', false);
|
||||||
case SEARCH_FETCH_SUCCESS:
|
case SEARCH_FETCH_SUCCESS:
|
||||||
return state.set('results', ImmutableMap({
|
return state.withMutations(map => {
|
||||||
|
map.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: fromJS(action.results.hashtags),
|
hashtags: fromJS(action.results.hashtags),
|
||||||
})).set('submitted', true).set('searchTerm', action.searchTerm);
|
}));
|
||||||
|
|
||||||
|
map.set('submitted', true);
|
||||||
|
map.set('searchTerm', action.searchTerm);
|
||||||
|
map.set('isLoading', false);
|
||||||
|
});
|
||||||
case SEARCH_EXPAND_SUCCESS:
|
case SEARCH_EXPAND_SUCCESS:
|
||||||
const results = action.searchType === 'hashtags' ? fromJS(action.results.hashtags) : action.results[action.searchType].map(item => item.id);
|
const results = action.searchType === 'hashtags' ? fromJS(action.results.hashtags) : action.results[action.searchType].map(item => item.id);
|
||||||
return state.updateIn(['results', action.searchType], list => list.concat(results));
|
return state.updateIn(['results', action.searchType], list => list.concat(results));
|
||||||
|
|
|
@ -17,6 +17,11 @@ import {
|
||||||
import {
|
import {
|
||||||
PINNED_STATUSES_FETCH_SUCCESS,
|
PINNED_STATUSES_FETCH_SUCCESS,
|
||||||
} from '../actions/pin_statuses';
|
} from '../actions/pin_statuses';
|
||||||
|
import {
|
||||||
|
TRENDS_STATUSES_FETCH_REQUEST,
|
||||||
|
TRENDS_STATUSES_FETCH_SUCCESS,
|
||||||
|
TRENDS_STATUSES_FETCH_FAIL,
|
||||||
|
} from '../actions/trends';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import {
|
import {
|
||||||
FAVOURITE_SUCCESS,
|
FAVOURITE_SUCCESS,
|
||||||
|
@ -26,6 +31,10 @@ import {
|
||||||
PIN_SUCCESS,
|
PIN_SUCCESS,
|
||||||
UNPIN_SUCCESS,
|
UNPIN_SUCCESS,
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
|
import {
|
||||||
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
|
ACCOUNT_MUTE_SUCCESS,
|
||||||
|
} from '../actions/accounts';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
favourites: ImmutableMap({
|
favourites: ImmutableMap({
|
||||||
|
@ -43,6 +52,11 @@ const initialState = ImmutableMap({
|
||||||
loaded: false,
|
loaded: false,
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
}),
|
}),
|
||||||
|
trending: ImmutableMap({
|
||||||
|
next: null,
|
||||||
|
loaded: false,
|
||||||
|
items: ImmutableList(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeList = (state, listType, statuses, next) => {
|
const normalizeList = (state, listType, statuses, next) => {
|
||||||
|
@ -96,6 +110,12 @@ export default function statusLists(state = initialState, action) {
|
||||||
return normalizeList(state, 'bookmarks', action.statuses, action.next);
|
return normalizeList(state, 'bookmarks', action.statuses, action.next);
|
||||||
case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
|
case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
|
||||||
return appendToList(state, 'bookmarks', action.statuses, action.next);
|
return appendToList(state, 'bookmarks', action.statuses, action.next);
|
||||||
|
case TRENDS_STATUSES_FETCH_REQUEST:
|
||||||
|
return state.setIn(['trending', 'isLoading'], true);
|
||||||
|
case TRENDS_STATUSES_FETCH_FAIL:
|
||||||
|
return state.setIn(['trending', 'isLoading'], false);
|
||||||
|
case TRENDS_STATUSES_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, 'trending', action.statuses, action.next);
|
||||||
case FAVOURITE_SUCCESS:
|
case FAVOURITE_SUCCESS:
|
||||||
return prependOneToList(state, 'favourites', action.status);
|
return prependOneToList(state, 'favourites', action.status);
|
||||||
case UNFAVOURITE_SUCCESS:
|
case UNFAVOURITE_SUCCESS:
|
||||||
|
@ -110,6 +130,9 @@ export default function statusLists(state = initialState, action) {
|
||||||
return prependOneToList(state, 'pins', action.status);
|
return prependOneToList(state, 'pins', action.status);
|
||||||
case UNPIN_SUCCESS:
|
case UNPIN_SUCCESS:
|
||||||
return removeOneFromList(state, 'pins', action.status);
|
return removeOneFromList(state, 'pins', action.status);
|
||||||
|
case ACCOUNT_BLOCK_SUCCESS:
|
||||||
|
case ACCOUNT_MUTE_SUCCESS:
|
||||||
|
return state.updateIn(['trending', 'items'], ImmutableList(), list => list.filterNot(statusId => action.statuses.getIn([statusId, 'account']) === action.relationship.id));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,45 @@
|
||||||
import { TRENDS_FETCH_REQUEST, TRENDS_FETCH_SUCCESS, TRENDS_FETCH_FAIL } from '../actions/trends';
|
import {
|
||||||
|
TRENDS_TAGS_FETCH_REQUEST,
|
||||||
|
TRENDS_TAGS_FETCH_SUCCESS,
|
||||||
|
TRENDS_TAGS_FETCH_FAIL,
|
||||||
|
TRENDS_LINKS_FETCH_REQUEST,
|
||||||
|
TRENDS_LINKS_FETCH_SUCCESS,
|
||||||
|
TRENDS_LINKS_FETCH_FAIL,
|
||||||
|
} from 'mastodon/actions/trends';
|
||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
|
tags: ImmutableMap({
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
links: 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:
|
case TRENDS_TAGS_FETCH_REQUEST:
|
||||||
return state.set('isLoading', true);
|
return state.setIn(['tags', 'isLoading'], true);
|
||||||
case TRENDS_FETCH_SUCCESS:
|
case TRENDS_TAGS_FETCH_SUCCESS:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('items', fromJS(action.trends));
|
map.setIn(['tags', 'items'], fromJS(action.trends));
|
||||||
map.set('isLoading', false);
|
map.setIn(['tags', 'isLoading'], false);
|
||||||
});
|
});
|
||||||
case TRENDS_FETCH_FAIL:
|
case TRENDS_TAGS_FETCH_FAIL:
|
||||||
return state.set('isLoading', false);
|
return state.setIn(['tags', 'isLoading'], false);
|
||||||
|
case TRENDS_LINKS_FETCH_REQUEST:
|
||||||
|
return state.setIn(['links', 'isLoading'], true);
|
||||||
|
case TRENDS_LINKS_FETCH_SUCCESS:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.setIn(['links', 'items'], fromJS(action.trends));
|
||||||
|
map.setIn(['links', 'isLoading'], false);
|
||||||
|
});
|
||||||
|
case TRENDS_LINKS_FETCH_FAIL:
|
||||||
|
return state.setIn(['links', 'isLoading'], false);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2797,6 +2797,10 @@ a.account__display-name {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable.fullscreen {
|
.scrollable.fullscreen {
|
||||||
|
@ -7724,3 +7728,122 @@ noscript {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.explore__search-header {
|
||||||
|
background: $ui-base-color;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
.search {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search__input {
|
||||||
|
border-radius: 4px;
|
||||||
|
color: $inverted-text-color;
|
||||||
|
background: $simple-background-color;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $dark-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search .fa {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
color: $dark-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search .fa-times-circle {
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.explore__search-results {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: $primary-text-color;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
background-color: lighten($ui-base-color, 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__details {
|
||||||
|
padding: 0 15px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
&__publisher {
|
||||||
|
color: $darker-text-color;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 19px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__shared {
|
||||||
|
color: $darker-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__thumbnail {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0 15px;
|
||||||
|
position: relative;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview {
|
||||||
|
border-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: fill;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue