Merge pull request #1859 from ClearlyClaire/glitch-soc/features/trends-tab
Port “Explore” tab to glitch-socmain
commit
44486db912
@ -0,0 +1,6 @@
|
||||
export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE';
|
||||
|
||||
export const changeLayout = layout => ({
|
||||
type: APP_LAYOUT_CHANGE,
|
||||
layout,
|
||||
});
|
@ -1,32 +1,139 @@
|
||||
import api from 'flavours/glitch/util/api';
|
||||
import api, { getLinks } 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 TRENDS_STATUSES_EXPAND_REQUEST = 'TRENDS_STATUSES_EXPAND_REQUEST';
|
||||
export const TRENDS_STATUSES_EXPAND_SUCCESS = 'TRENDS_STATUSES_EXPAND_SUCCESS';
|
||||
export const TRENDS_STATUSES_EXPAND_FAIL = 'TRENDS_STATUSES_EXPAND_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) => {
|
||||
if (getState().getIn(['status_lists', 'trending', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchTrendingStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/trends/statuses').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchTrendingStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(err => dispatch(fetchTrendingStatusesFail(err)));
|
||||
};
|
||||
|
||||
export const fetchTrendingStatusesRequest = () => ({
|
||||
type: TRENDS_STATUSES_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const fetchTrendingStatusesSuccess = (statuses, next) => ({
|
||||
type: TRENDS_STATUSES_FETCH_SUCCESS,
|
||||
statuses,
|
||||
next,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const fetchTrendingStatusesFail = error => ({
|
||||
type: TRENDS_STATUSES_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
});
|
||||
|
||||
|
||||
export const expandTrendingStatuses = () => (dispatch, getState) => {
|
||||
const url = getState().getIn(['status_lists', 'trending', 'next'], null);
|
||||
|
||||
if (url === null || getState().getIn(['status_lists', 'trending', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandTrendingStatusesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandTrendingStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandTrendingStatusesFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const expandTrendingStatusesRequest = () => ({
|
||||
type: TRENDS_STATUSES_EXPAND_REQUEST,
|
||||
});
|
||||
|
||||
export const expandTrendingStatusesSuccess = (statuses, next) => ({
|
||||
type: TRENDS_STATUSES_EXPAND_SUCCESS,
|
||||
statuses,
|
||||
next,
|
||||
});
|
||||
|
||||
export const expandTrendingStatusesFail = error => ({
|
||||
type: TRENDS_STATUSES_EXPAND_FAIL,
|
||||
error,
|
||||
});
|
||||
|
@ -0,0 +1,51 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
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';
|
||||
import { showTrends } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
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']) || !showTrends,
|
||||
});
|
||||
|
||||
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' : 'hashtag'}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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 '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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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 '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', ImmutableList()).map(item => (
|
||||
<Account key={`account-${item}`} id={item} />
|
||||
)), onLoadMore);
|
||||
|
||||
const renderHashtags = (results, onLoadMore) => appendLoadMore('hashtags', results.get('hashtags', ImmutableList()).map(item => (
|
||||
<Hashtag key={`tag-${item.get('name')}`} hashtag={item} />
|
||||
)), onLoadMore);
|
||||
|
||||
const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', results.get('statuses', ImmutableList()).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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
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, expandTrendingStatuses } from 'flavours/glitch/actions/trends';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'trending', 'items']),
|
||||
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Statuses extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
statusIds: ImmutablePropTypes.list,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchTrendingStatuses());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(expandTrendingStatuses());
|
||||
}, 300, { leading: true })
|
||||
|
||||
render () {
|
||||
const { isLoading, hasMore, 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={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
withCounters
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import AccountCard from 'flavours/glitch/features/directory/components/account_card';
|
||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchSuggestions, dismissSuggestion } 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));
|
||||
}
|
||||
|
||||
handleDismiss = (accountId) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(dismissSuggestion(accountId));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isLoading, suggestions } = this.props;
|
||||
|
||||
return (
|
||||
<div className='explore__suggestions'>
|
||||
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
||||
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} onDismiss={suggestion.get('source') === 'past_interactions' ? this.handleDismiss : null} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
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);
|
||||
|
@ -1,17 +0,0 @@
|
||||
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;
|
@ -0,0 +1,122 @@
|
||||
.account-card__header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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 new issue