import api from 'flavours/glitch/util/api';
+import { importFetchedStatuses } from './importer';
-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 TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
+export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS';
+export const TRENDS_TAGS_FETCH_FAIL = 'TRENDS_TAGS_FETCH_FAIL';
-export const fetchTrends = () => (dispatch, getState) => {
- dispatch(fetchTrendsRequest());
+export const TRENDS_LINKS_FETCH_REQUEST = 'TRENDS_LINKS_FETCH_REQUEST';
+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)
+ .get('/api/v1/trends/tags')
+ .then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data)))
+ .catch(err => dispatch(fetchTrendingHashtagsFail(err)));
+};
+
+export const fetchTrendingHashtagsRequest = () => ({
+ type: TRENDS_TAGS_FETCH_REQUEST,
+ skipLoading: true,
+});
+
+export const fetchTrendingHashtagsSuccess = trends => ({
+ type: TRENDS_TAGS_FETCH_SUCCESS,
+ trends,
+ skipLoading: true,
+});
+
+export const fetchTrendingHashtagsFail = error => ({
+ type: TRENDS_TAGS_FETCH_FAIL,
+ error,
+ skipLoading: true,
+ skipAlert: true,
+});
+
+export const fetchTrendingLinks = () => (dispatch, getState) => {
+ dispatch(fetchTrendingLinksRequest());
api(getState)
- .get('/api/v1/trends')
- .then(({ data }) => dispatch(fetchTrendsSuccess(data)))
- .catch(err => dispatch(fetchTrendsFail(err)));
+ .get('/api/v1/trends/links')
+ .then(({ data }) => dispatch(fetchTrendingLinksSuccess(data)))
+ .catch(err => dispatch(fetchTrendingLinksFail(err)));
};
-export const fetchTrendsRequest = () => ({
- type: TRENDS_FETCH_REQUEST,
+export const fetchTrendingLinksRequest = () => ({
+ type: TRENDS_LINKS_FETCH_REQUEST,
skipLoading: true,
});
-export const fetchTrendsSuccess = trends => ({
- type: TRENDS_FETCH_SUCCESS,
+export const fetchTrendingLinksSuccess = trends => ({
+ type: TRENDS_LINKS_FETCH_SUCCESS,
trends,
skipLoading: true,
});
-export const fetchTrendsFail = error => ({
- type: TRENDS_FETCH_FAIL,
+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,
skipLoading: true,
skipAlert: true,
*
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
*/
-const accountsCountRenderer = (displayNumber, pluralReady) => (
+export const accountsCountRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='trends.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}'
onFilter: PropTypes.func,
onAddFilter: PropTypes.func,
withDismiss: PropTypes.bool,
+ withCounters: PropTypes.bool,
showReplyCount: PropTypes.bool,
scrollKey: PropTypes.string,
intl: PropTypes.object.isRequired,
updateOnProps = [
'status',
'showReplyCount',
+ 'withCounters',
'withDismiss',
]
}
render () {
- const { status, intl, withDismiss, showReplyCount, scrollKey } = this.props;
+ const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
const anonymousAccess = !me;
const mutingConversation = status.get('muted');
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
);
- let replyButton = (
- <IconButton
- className='status__action-bar-button'
- title={replyTitle}
- icon={replyIcon}
- onClick={this.handleReplyClick}
- />
- );
- if (showReplyCount) {
- replyButton = (
- <IconButton
- className='status__action-bar-button'
- title={replyTitle}
- icon={replyIcon}
- onClick={this.handleReplyClick}
- counter={status.get('replies_count')}
- obfuscateCount
- />
- );
- }
-
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle = '';
return (
<div className='status__action-bar'>
- {replyButton}
- <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} />
- <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'
+ title={replyTitle}
+ icon={replyIcon}
+ onClick={this.handleReplyClick}
+ counter={showReplyCount ? status.get('replies_count') : undefined}
+ obfuscateCount
+ />
+ <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} 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} counter={withCounters ? status.get('favourites_count') : undefined} />
{shareButton}
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
isPartial: PropTypes.bool,
hasMore: PropTypes.bool,
prepend: PropTypes.node,
- alwaysPrepend: PropTypes.bool,
emptyMessage: PropTypes.node,
+ alwaysPrepend: PropTypes.bool,
+ withCounters: PropTypes.bool,
timelineId: PropTypes.string.isRequired,
regex: PropTypes.string,
};
onMoveDown={this.handleMoveDown}
contextType={timelineId}
scrollKey={this.props.scrollKey}
+ withCounters={this.props.withCounters}
/>
))
) : null;
onMoveDown={this.handleMoveDown}
contextType={timelineId}
scrollKey={this.props.scrollKey}
+ withCounters={this.props.withCounters}
/>
)).concat(scrollableContent);
}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import Blurhash from 'flavours/glitch/components/blurhash';
+import { accountsCountRenderer } from 'flavours/glitch/components/hashtag';
+import ShortNumber from 'flavours/glitch/components/short_number';
+import Skeleton from 'flavours/glitch/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>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/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 'flavours/glitch/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>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Story from './components/story';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
+import { connect } from 'react-redux';
+import { fetchTrendingLinks } from 'flavours/glitch/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>
+ );
+ }
+
+}
--- /dev/null
+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 'flavours/glitch/actions/search';
+import Account from 'flavours/glitch/containers/account_container';
+import Status from 'flavours/glitch/containers/status_container';
+import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
+import { List as ImmutableList } from 'immutable';
+import LoadMore from 'flavours/glitch/components/load_more';
+import LoadingIndicator from 'flavours/glitch/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>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import StatusList from 'flavours/glitch/components/status_list';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { fetchTrendingStatuses } from 'flavours/glitch/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
+ />
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Account from 'flavours/glitch/containers/account_container';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
+import { connect } from 'react-redux';
+import { fetchSuggestions } from 'flavours/glitch/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>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
+import { connect } from 'react-redux';
+import { fetchTrendingHashtags } from 'flavours/glitch/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>
+ );
+ }
+
+}
import { connect } from 'react-redux';
-import { fetchTrends } from 'flavours/glitch/actions/trends';
+import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends';
import Trends from '../components/trends';
const mapStateToProps = state => ({
- trends: state.getIn(['trends', 'items']),
+ trends: state.getIn(['trends', 'tags', 'items']),
});
const mapDispatchToProps = dispatch => ({
- fetchTrends: () => dispatch(fetchTrends()),
+ fetchTrends: () => dispatch(fetchTrendingHashtags()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Trends);
+++ /dev/null
-import React from 'react';
-import SearchContainer from 'flavours/glitch/features/compose/containers/search_container';
-import SearchResultsContainer from 'flavours/glitch/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;
'DIRECTORY': Directory,
};
-const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/search|^\/getting-started|^\/start/);
+const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/explore|^\/getting-started|^\/start/);
const messages = defineMessages({
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
<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>
<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' 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>
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='/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' 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 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 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' 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>,
];
Mutes,
PinnedStatuses,
Lists,
- Search,
GettingStartedMisc,
Directory,
+ Explore,
FollowRecommendations,
} from 'flavours/glitch/util/async-components';
import { HotKeys } from 'react-hotkeys';
<WrappedRoute path='/pinned' component={PinnedStatuses} 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={['/explore', '/search']} component={Explore} content={children} />
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
import {
SEARCH_CHANGE,
SEARCH_CLEAR,
+ SEARCH_FETCH_REQUEST,
+ SEARCH_FETCH_FAIL,
SEARCH_FETCH_SUCCESS,
SEARCH_SHOW,
SEARCH_EXPAND_SUCCESS,
submitted: false,
hidden: false,
results: ImmutableMap(),
+ isLoading: false,
searchTerm: '',
});
case COMPOSE_MENTION:
case COMPOSE_DIRECT:
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:
- return state.set('results', ImmutableMap({
- accounts: ImmutableList(action.results.accounts.map(item => item.id)),
- statuses: ImmutableList(action.results.statuses.map(item => item.id)),
- hashtags: fromJS(action.results.hashtags),
- })).set('submitted', true).set('searchTerm', action.searchTerm);
+ return state.withMutations(map => {
+ map.set('results', ImmutableMap({
+ accounts: ImmutableList(action.results.accounts.map(item => item.id)),
+ statuses: ImmutableList(action.results.statuses.map(item => item.id)),
+ hashtags: fromJS(action.results.hashtags),
+ }));
+
+ map.set('submitted', true);
+ map.set('searchTerm', action.searchTerm);
+ map.set('isLoading', false);
+ });
case SEARCH_EXPAND_SUCCESS:
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));
import {
PINNED_STATUSES_FETCH_SUCCESS,
} from 'flavours/glitch/actions/pin_statuses';
+import {
+ TRENDS_STATUSES_FETCH_REQUEST,
+ TRENDS_STATUSES_FETCH_SUCCESS,
+ TRENDS_STATUSES_FETCH_FAIL,
+} from 'flavours/glitch/actions/trends';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import {
FAVOURITE_SUCCESS,
PIN_SUCCESS,
UNPIN_SUCCESS,
} from 'flavours/glitch/actions/interactions';
+import {
+ ACCOUNT_BLOCK_SUCCESS,
+ ACCOUNT_MUTE_SUCCESS,
+} from 'flavours/glitch/actions/accounts';
const initialState = ImmutableMap({
favourites: ImmutableMap({
loaded: false,
items: ImmutableList(),
}),
+ trending: ImmutableMap({
+ next: null,
+ loaded: false,
+ items: ImmutableList(),
+ }),
});
const normalizeList = (state, listType, statuses, next) => {
return normalizeList(state, 'bookmarks', action.statuses, action.next);
case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
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:
return prependOneToList(state, 'favourites', action.status);
case UNFAVOURITE_SUCCESS:
return prependOneToList(state, 'pins', action.status);
case UNPIN_SUCCESS:
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:
return state;
}
-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 'flavours/glitch/actions/trends';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const initialState = ImmutableMap({
- items: ImmutableList(),
- isLoading: false,
+ tags: ImmutableMap({
+ items: ImmutableList(),
+ isLoading: false,
+ }),
+
+ links: ImmutableMap({
+ items: ImmutableList(),
+ isLoading: false,
+ }),
});
export default function trendsReducer(state = initialState, action) {
switch(action.type) {
- case TRENDS_FETCH_REQUEST:
- return state.set('isLoading', true);
- case TRENDS_FETCH_SUCCESS:
+ case TRENDS_TAGS_FETCH_REQUEST:
+ return state.setIn(['tags', 'isLoading'], true);
+ case TRENDS_TAGS_FETCH_SUCCESS:
+ return state.withMutations(map => {
+ map.setIn(['tags', 'items'], fromJS(action.trends));
+ map.setIn(['tags', 'isLoading'], false);
+ });
+ case TRENDS_TAGS_FETCH_FAIL:
+ 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.set('items', fromJS(action.trends));
- map.set('isLoading', false);
+ map.setIn(['links', 'items'], fromJS(action.trends));
+ map.setIn(['links', 'isLoading'], false);
});
- case TRENDS_FETCH_FAIL:
- return state.set('isLoading', false);
+ case TRENDS_LINKS_FETCH_FAIL:
+ return state.setIn(['links', 'isLoading'], false);
default:
return state;
}
--- /dev/null
+.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;
+ }
+ }
+ }
+}
position: relative;
min-height: 120px;
}
+
+ .scrollable {
+ flex: 1 1 auto;
+ }
}
.scrollable.fullscreen {
@import 'error_boundary';
@import 'single_column';
@import 'announcements';
+@import 'explore';
return import(/* webpackChunkName: "features/glitch/async/list_adder" */'flavours/glitch/features/list_adder');
}
-export function Search () {
- return import(/*webpackChunkName: "features/glitch/async/search" */'flavours/glitch/features/search');
-}
-
export function Tesseract () {
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
}
export function FilterModal () {
return import(/*webpackChunkName: "flavours/glitch/async/filter_modal" */'flavours/glitch/features/ui/components/filter_modal');
}
+
+export function Explore () {
+ return import(/* webpackChunkName: "flavours/glitch/async/explore" */'flavours/glitch/features/explore');
+}