forked from treehouse/mastodon
Merge pull request #1859 from ClearlyClaire/glitch-soc/features/trends-tab
Port “Explore” tab to glitch-socrebase/4.0.0rc1
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')
|
||||
.then(({ data }) => dispatch(fetchTrendsSuccess(data)))
|
||||
.catch(err => dispatch(fetchTrendsFail(err)));
|
||||
.get('/api/v1/trends/tags')
|
||||
.then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data)))
|
||||
.catch(err => dispatch(fetchTrendingHashtagsFail(err)));
|
||||
};
|
||||
|
||||
export const fetchTrendsRequest = () => ({
|
||||
type: TRENDS_FETCH_REQUEST,
|
||||
export const fetchTrendingHashtagsRequest = () => ({
|
||||
type: TRENDS_TAGS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const fetchTrendsSuccess = trends => ({
|
||||
type: TRENDS_FETCH_SUCCESS,
|
||||
export const fetchTrendingHashtagsSuccess = trends => ({
|
||||
type: TRENDS_TAGS_FETCH_SUCCESS,
|
||||
trends,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const fetchTrendsFail = error => ({
|
||||
type: TRENDS_FETCH_FAIL,
|
||||
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/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) => {
|
||||
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,
|
||||
});
|
||||
|
|
|
@ -42,7 +42,7 @@ class SilentErrorBoundary extends React.Component {
|
|||
*
|
||||
* @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}}'
|
||||
|
|
|
@ -99,8 +99,11 @@ class Status extends ImmutablePureComponent {
|
|||
onClick: PropTypes.func,
|
||||
scrollKey: PropTypes.string,
|
||||
deployPictureInPicture: PropTypes.func,
|
||||
usingPiP: PropTypes.bool,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
pictureInPicture: PropTypes.shape({
|
||||
inUse: PropTypes.bool,
|
||||
available: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -126,7 +129,7 @@ class Status extends ImmutablePureComponent {
|
|||
'hidden',
|
||||
'expanded',
|
||||
'unread',
|
||||
'usingPiP',
|
||||
'pictureInPicture',
|
||||
]
|
||||
|
||||
updateOnStates = [
|
||||
|
@ -503,7 +506,7 @@ class Status extends ImmutablePureComponent {
|
|||
hidden,
|
||||
unread,
|
||||
featured,
|
||||
usingPiP,
|
||||
pictureInPicture,
|
||||
...other
|
||||
} = this.props;
|
||||
const { isCollapsed, forceFilter } = this.state;
|
||||
|
@ -595,7 +598,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
attachments = status.get('media_attachments');
|
||||
|
||||
if (usingPiP) {
|
||||
if (pictureInPicture.inUse) {
|
||||
media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />);
|
||||
mediaIcons.push('video-camera');
|
||||
} else if (attachments.size > 0) {
|
||||
|
@ -623,7 +626,7 @@ class Status extends ImmutablePureComponent {
|
|||
width={this.props.cachedMediaWidth}
|
||||
height={110}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
deployPictureInPicture={this.handleDeployPictureInPicture}
|
||||
deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined}
|
||||
sensitive={status.get('sensitive')}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
visible={this.state.showMedia}
|
||||
|
@ -652,7 +655,7 @@ class Status extends ImmutablePureComponent {
|
|||
onOpenVideo={this.handleOpenVideo}
|
||||
width={this.props.cachedMediaWidth}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
deployPictureInPicture={this.handleDeployPictureInPicture}
|
||||
deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>)}
|
||||
|
|
|
@ -70,6 +70,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
onFilter: PropTypes.func,
|
||||
onAddFilter: PropTypes.func,
|
||||
withDismiss: PropTypes.bool,
|
||||
withCounters: PropTypes.bool,
|
||||
showReplyCount: PropTypes.bool,
|
||||
scrollKey: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
@ -80,6 +81,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
updateOnProps = [
|
||||
'status',
|
||||
'showReplyCount',
|
||||
'withCounters',
|
||||
'withDismiss',
|
||||
]
|
||||
|
||||
|
@ -204,7 +206,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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');
|
||||
|
@ -283,27 +285,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
<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 = '';
|
||||
|
@ -323,9 +304,16 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
|
||||
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} />
|
||||
|
||||
|
|
|
@ -22,8 +22,9 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
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,
|
||||
};
|
||||
|
@ -100,6 +101,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
scrollKey={this.props.scrollKey}
|
||||
withCounters={this.props.withCounters}
|
||||
/>
|
||||
))
|
||||
) : null;
|
||||
|
@ -114,6 +116,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
scrollKey={this.props.scrollKey}
|
||||
withCounters={this.props.withCounters}
|
||||
/>
|
||||
)).concat(scrollableContent);
|
||||
}
|
||||
|
|
|
@ -76,12 +76,16 @@ const makeMapStateToProps = () => {
|
|||
}
|
||||
|
||||
return {
|
||||
containerId : props.containerId || props.id, // Should match reblogStatus's id for reblogs
|
||||
status : status,
|
||||
account : account || props.account,
|
||||
settings : state.get('local_settings'),
|
||||
prepend : prepend || props.prepend,
|
||||
usingPiP : state.get('picture_in_picture').statusId === props.id,
|
||||
containerId: props.containerId || props.id, // Should match reblogStatus's id for reblogs
|
||||
status: status,
|
||||
account: account || props.account,
|
||||
settings: state.get('local_settings'),
|
||||
prepend: prepend || props.prepend,
|
||||
|
||||
pictureInPicture: {
|
||||
inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id,
|
||||
available: state.getIn(['meta', 'layout']) !== 'mobile',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { makeGetAccount } from 'flavours/glitch/selectors';
|
|||
import Avatar from 'flavours/glitch/components/avatar';
|
||||
import DisplayName from 'flavours/glitch/components/display_name';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import Button from 'flavours/glitch/components/button';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state';
|
||||
|
@ -29,6 +30,7 @@ const messages = defineMessages({
|
|||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
|
@ -94,6 +96,7 @@ class AccountCard extends ImmutablePureComponent {
|
|||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onDismiss: PropTypes.func,
|
||||
};
|
||||
|
||||
handleMouseEnter = ({ currentTarget }) => {
|
||||
|
@ -138,6 +141,14 @@ class AccountCard extends ImmutablePureComponent {
|
|||
window.open('/settings/profile', '_blank');
|
||||
}
|
||||
|
||||
handleDismiss = (e) => {
|
||||
const { account, onDismiss } = this.props;
|
||||
onDismiss(account.get('id'));
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, intl } = this.props;
|
||||
|
||||
|
@ -163,6 +174,8 @@ class AccountCard extends ImmutablePureComponent {
|
|||
<div className='account-card'>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
|
||||
<div className='account-card__header'>
|
||||
{this.props.onDismiss && <IconButton className='media-modal__close' title={intl.formatMessage(messages.dismissSuggestion)} icon='times' onClick={this.handleDismiss} size={20} />}
|
||||
|
||||
<img
|
||||
src={
|
||||
autoPlayGif ? account.get('header') : account.get('header_static')
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -8,7 +8,7 @@ import { openModal } from 'flavours/glitch/actions/modal';
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me, profile_directory, showTrends } from 'flavours/glitch/util/initial_state';
|
||||
import { me, showTrends } from 'flavours/glitch/util/initial_state';
|
||||
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
|
@ -26,6 +26,7 @@ const messages = defineMessages({
|
|||
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
|
||||
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
|
||||
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||
explore: { id: 'navigation_bar.explore', defaultMessage: 'Explore' },
|
||||
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
|
||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
|
@ -37,7 +38,6 @@ const messages = defineMessages({
|
|||
lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' },
|
||||
misc: { id: 'navigation_bar.misc', defaultMessage: 'Misc' },
|
||||
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
|
@ -122,45 +122,45 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
|||
|
||||
if (multiColumn) {
|
||||
if (!columns.find(item => item.get('id') === 'HOME')) {
|
||||
navItems.push(<ColumnLink key='0' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />);
|
||||
navItems.push(<ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />);
|
||||
}
|
||||
|
||||
if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) {
|
||||
navItems.push(<ColumnLink key='1' icon='bell' text={intl.formatMessage(messages.notifications)} badge={badgeDisplay(unreadNotifications)} to='/notifications' />);
|
||||
navItems.push(<ColumnLink key='notifications' icon='bell' text={intl.formatMessage(messages.notifications)} badge={badgeDisplay(unreadNotifications)} to='/notifications' />);
|
||||
}
|
||||
|
||||
if (!columns.find(item => item.get('id') === 'COMMUNITY')) {
|
||||
navItems.push(<ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />);
|
||||
navItems.push(<ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />);
|
||||
}
|
||||
|
||||
if (!columns.find(item => item.get('id') === 'PUBLIC')) {
|
||||
navItems.push(<ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />);
|
||||
navItems.push(<ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />);
|
||||
}
|
||||
}
|
||||
|
||||
if (showTrends) {
|
||||
navItems.push(<ColumnLink key='explore' icon='hashtag' text={intl.formatMessage(messages.explore)} to='/explore' />);
|
||||
}
|
||||
|
||||
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
|
||||
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />);
|
||||
navItems.push(<ColumnLink key='conversations' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />);
|
||||
}
|
||||
|
||||
if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) {
|
||||
navItems.push(<ColumnLink key='5' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />);
|
||||
navItems.push(<ColumnLink key='bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />);
|
||||
}
|
||||
|
||||
if (myAccount.get('locked') || unreadFollowRequests > 0) {
|
||||
navItems.push(<ColumnLink key='6' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
}
|
||||
|
||||
if (profile_directory) {
|
||||
navItems.push(<ColumnLink key='7' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />);
|
||||
}
|
||||
|
||||
navItems.push(<ColumnLink key='8' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
|
||||
navItems.push(<ColumnLink key='getting_started' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
|
||||
|
||||
listItems = listItems.concat([
|
||||
<div key='9'>
|
||||
<ColumnLink key='10' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
<ColumnLink key='lists' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
{lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list =>
|
||||
<ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
<ColumnLink key={`list-${list.get('id')}`} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
)}
|
||||
</div>,
|
||||
]);
|
||||
|
|
|
@ -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;
|
|
@ -49,7 +49,7 @@ const componentMap = {
|
|||
'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' },
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { limitedFederationMode, version, repository, source_url } from 'flavours/glitch/util/initial_state';
|
||||
import { limitedFederationMode, version, repository, source_url, profile_directory as profileDirectory } from 'flavours/glitch/util/initial_state';
|
||||
import { signOutLink, securityLink, privacyPolicyLink } from 'flavours/glitch/util/backend_links';
|
||||
import { logOut } from 'flavours/glitch/util/log_out';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
@ -54,6 +54,7 @@ class LinkFooter extends React.PureComponent {
|
|||
{((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
|
||||
{!!securityLink && <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>}
|
||||
{!limitedFederationMode && <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>}
|
||||
{profileDirectory && <li><Link to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></Link> · </li>}
|
||||
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
|
||||
<li><a href={privacyPolicyLink} target='_blank'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></a> · </li>
|
||||
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import { profile_directory, showTrends } from 'flavours/glitch/util/initial_state';
|
||||
import { showTrends } from 'flavours/glitch/util/initial_state';
|
||||
import { preferencesLink, relationshipsLink } from 'flavours/glitch/util/backend_links';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
import FollowRequestsNavLink from './follow_requests_nav_link';
|
||||
|
@ -14,11 +14,11 @@ const NavigationPanel = ({ onOpenSettings }) => (
|
|||
<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 />
|
||||
{ showTrends && <NavLink className='column-link column-link--transparent' to='/explore' data-preview-title-id='explore.title' data-preview-icon='hashtag'><Icon className='column-link__icon' id='hashtag' 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>
|
||||
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
|
||||
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
|
||||
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
||||
|
||||
<ListPanel />
|
||||
|
|
|
@ -12,7 +12,7 @@ export const links = [
|
|||
<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='/search' 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>,
|
||||
];
|
||||
|
||||
|
|
|
@ -5,13 +5,14 @@ import LoadingBarContainer from './containers/loading_bar_container';
|
|||
import ModalContainer from './containers/modal_container';
|
||||
import { connect } from 'react-redux';
|
||||
import { Redirect, withRouter } from 'react-router-dom';
|
||||
import { isMobile } from 'flavours/glitch/util/is_mobile';
|
||||
import { layoutFromWindow } from 'flavours/glitch/util/is_mobile';
|
||||
import { debounce } from 'lodash';
|
||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose';
|
||||
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
||||
import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications';
|
||||
import { fetchRules } from 'flavours/glitch/actions/rules';
|
||||
import { clearHeight } from 'flavours/glitch/actions/height_cache';
|
||||
import { changeLayout } from 'flavours/glitch/actions/app';
|
||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
||||
import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
|
||||
import UploadArea from './components/upload_area';
|
||||
|
@ -47,9 +48,9 @@ import {
|
|||
Mutes,
|
||||
PinnedStatuses,
|
||||
Lists,
|
||||
Search,
|
||||
GettingStartedMisc,
|
||||
Directory,
|
||||
Explore,
|
||||
FollowRecommendations,
|
||||
} from 'flavours/glitch/util/async-components';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
@ -66,10 +67,12 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
layout: state.getIn(['meta', 'layout']),
|
||||
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||
layout: state.getIn(['local_settings', 'layout']),
|
||||
layout: state.getIn(['meta', 'layout']),
|
||||
layout_local_setting: state.getIn(['local_settings', 'layout']),
|
||||
isWide: state.getIn(['local_settings', 'stretch']),
|
||||
navbarUnder: state.getIn(['local_settings', 'navbar_under']),
|
||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||
|
@ -120,26 +123,13 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
layout: PropTypes.string,
|
||||
location: PropTypes.object,
|
||||
navbarUnder: PropTypes.bool,
|
||||
onLayoutChange: PropTypes.func.isRequired,
|
||||
mobile: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
mobile: isMobile(window.innerWidth, this.props.layout),
|
||||
};
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.layout !== this.props.layout) {
|
||||
this.setState({ mobile: isMobile(window.innerWidth, nextProps.layout) });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
|
||||
if (this.state.mobile) {
|
||||
if (this.props.mobile) {
|
||||
document.body.classList.toggle('layout-single-column', true);
|
||||
document.body.classList.toggle('layout-multiple-columns', false);
|
||||
} else {
|
||||
|
@ -148,37 +138,14 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
componentDidUpdate (prevProps) {
|
||||
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
|
||||
this.node.handleChildrenContentChange();
|
||||
}
|
||||
|
||||
if (prevState.mobile !== this.state.mobile) {
|
||||
document.body.classList.toggle('layout-single-column', this.state.mobile);
|
||||
document.body.classList.toggle('layout-multiple-columns', !this.state.mobile);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
handleLayoutChange = debounce(() => {
|
||||
// The cached heights are no longer accurate, invalidate
|
||||
this.props.onLayoutChange();
|
||||
}, 500, {
|
||||
trailing: true,
|
||||
})
|
||||
|
||||
handleResize = () => {
|
||||
const mobile = isMobile(window.innerWidth, this.props.layout);
|
||||
|
||||
if (mobile !== this.state.mobile) {
|
||||
this.handleLayoutChange.cancel();
|
||||
this.props.onLayoutChange();
|
||||
this.setState({ mobile });
|
||||
} else {
|
||||
this.handleLayoutChange();
|
||||
if (prevProps.mobile !== this.props.mobile) {
|
||||
document.body.classList.toggle('layout-single-column', this.props.mobile);
|
||||
document.body.classList.toggle('layout-multiple-columns', !this.props.mobile);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,12 +156,11 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { children, navbarUnder } = this.props;
|
||||
const singleColumn = this.state.mobile;
|
||||
const redirect = singleColumn ? <Redirect from='/' to='/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
|
||||
const { children, mobile, navbarUnder } = this.props;
|
||||
const redirect = mobile ? <Redirect from='/' to='/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
|
||||
|
||||
return (
|
||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn} navbarUnder={navbarUnder}>
|
||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile} navbarUnder={navbarUnder}>
|
||||
<WrappedSwitch>
|
||||
{redirect}
|
||||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
||||
|
@ -213,8 +179,8 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<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} />
|
||||
|
@ -256,7 +222,7 @@ class UI extends React.Component {
|
|||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
layout: PropTypes.string,
|
||||
layout_local_setting: PropTypes.string,
|
||||
isWide: PropTypes.bool,
|
||||
systemFontUi: PropTypes.bool,
|
||||
navbarUnder: PropTypes.bool,
|
||||
|
@ -272,6 +238,7 @@ class UI extends React.Component {
|
|||
unreadNotifications: PropTypes.number,
|
||||
showFaviconBadge: PropTypes.bool,
|
||||
moved: PropTypes.map,
|
||||
layout: PropTypes.string.isRequired,
|
||||
firstLaunch: PropTypes.bool,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
@ -293,11 +260,6 @@ class UI extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleLayoutChange = () => {
|
||||
// The cached heights are no longer accurate, invalidate
|
||||
this.props.dispatch(clearHeight());
|
||||
}
|
||||
|
||||
handleDragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -378,8 +340,27 @@ class UI extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
handleLayoutChange = debounce(() => {
|
||||
this.props.dispatch(clearHeight()); // The cached heights are no longer accurate, invalidate
|
||||
}, 500, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
handleResize = () => {
|
||||
const layout = layoutFromWindow(this.props.layout_local_setting);
|
||||
|
||||
if (layout !== this.props.layout) {
|
||||
this.handleLayoutChange.cancel();
|
||||
this.props.dispatch(changeLayout(layout));
|
||||
} else {
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
|
||||
document.addEventListener('dragenter', this.handleDragEnter, false);
|
||||
document.addEventListener('dragover', this.handleDragOver, false);
|
||||
document.addEventListener('drop', this.handleDrop, false);
|
||||
|
@ -403,9 +384,7 @@ class UI extends React.Component {
|
|||
this.props.dispatch(expandNotifications());
|
||||
|
||||
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
||||
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
|
||||
};
|
||||
|
@ -427,6 +406,19 @@ class UI extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.layout_local_setting !== this.props.layout_local_setting) {
|
||||
const layout = layoutFromWindow(nextProps.layout_local_setting);
|
||||
|
||||
if (layout !== this.props.layout) {
|
||||
this.handleLayoutChange.cancel();
|
||||
this.props.dispatch(changeLayout(layout));
|
||||
} else {
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.unreadNotifications != prevProps.unreadNotifications ||
|
||||
this.props.showFaviconBadge != prevProps.showFaviconBadge) {
|
||||
|
@ -446,6 +438,8 @@ class UI extends React.Component {
|
|||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
|
||||
document.removeEventListener('dragenter', this.handleDragEnter);
|
||||
document.removeEventListener('dragover', this.handleDragOver);
|
||||
document.removeEventListener('drop', this.handleDrop);
|
||||
|
@ -576,7 +570,7 @@ class UI extends React.Component {
|
|||
|
||||
render () {
|
||||
const { draggingOver } = this.state;
|
||||
const { children, layout, isWide, navbarUnder, location, dropdownMenuIsOpen, moved } = this.props;
|
||||
const { children, isWide, navbarUnder, location, dropdownMenuIsOpen, layout, moved } = this.props;
|
||||
|
||||
const columnsClass = layout => {
|
||||
switch (layout) {
|
||||
|
@ -632,11 +626,11 @@ class UI extends React.Component {
|
|||
)}}
|
||||
/>
|
||||
</div>)}
|
||||
<SwitchingColumnsArea location={location} layout={layout} navbarUnder={navbarUnder} onLayoutChange={this.handleLayoutChange}>
|
||||
<SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'} navbarUnder={navbarUnder}>
|
||||
{children}
|
||||
</SwitchingColumnsArea>
|
||||
|
||||
<PictureInPicture />
|
||||
{layout !== 'mobile' && <PictureInPicture />}
|
||||
<NotificationsContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
<ModalContainer />
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
||||
import { APP_LAYOUT_CHANGE } from 'flavours/glitch/actions/app';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { layoutFromWindow } from 'flavours/glitch/util/is_mobile';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
streaming_api_base_url: null,
|
||||
access_token: null,
|
||||
layout: layoutFromWindow(),
|
||||
permissions: '0',
|
||||
});
|
||||
|
||||
export default function meta(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case STORE_HYDRATE:
|
||||
return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions']));
|
||||
return state.merge(
|
||||
action.state.get('meta'))
|
||||
.set('permissions', action.state.getIn(['role', 'permissions']))
|
||||
.set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout']))
|
||||
);
|
||||
case APP_LAYOUT_CHANGE:
|
||||
return state.set('layout', action.layout);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
SEARCH_CHANGE,
|
||||
SEARCH_CLEAR,
|
||||
SEARCH_FETCH_REQUEST,
|
||||
SEARCH_FETCH_FAIL,
|
||||
SEARCH_FETCH_SUCCESS,
|
||||
SEARCH_SHOW,
|
||||
SEARCH_EXPAND_SUCCESS,
|
||||
|
@ -17,6 +19,7 @@ const initialState = ImmutableMap({
|
|||
submitted: false,
|
||||
hidden: false,
|
||||
results: ImmutableMap(),
|
||||
isLoading: false,
|
||||
searchTerm: '',
|
||||
});
|
||||
|
||||
|
@ -37,12 +40,24 @@ export default function search(state = initialState, action) {
|
|||
case COMPOSE_MENTION:
|
||||
case COMPOSE_DIRECT:
|
||||
return state.set('hidden', true);
|
||||
case SEARCH_FETCH_REQUEST:
|
||||
return state.withMutations(map => {
|
||||
map.set('isLoading', true);
|
||||
map.set('submitted', 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('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));
|
||||
|
|
|
@ -17,6 +17,14 @@ import {
|
|||
import {
|
||||
PINNED_STATUSES_FETCH_SUCCESS,
|
||||
} from 'flavours/glitch/actions/pin_statuses';
|
||||
import {
|
||||
TRENDS_STATUSES_FETCH_REQUEST,
|
||||
TRENDS_STATUSES_FETCH_SUCCESS,
|
||||
TRENDS_STATUSES_FETCH_FAIL,
|
||||
TRENDS_STATUSES_EXPAND_REQUEST,
|
||||
TRENDS_STATUSES_EXPAND_SUCCESS,
|
||||
TRENDS_STATUSES_EXPAND_FAIL,
|
||||
} from 'flavours/glitch/actions/trends';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import {
|
||||
FAVOURITE_SUCCESS,
|
||||
|
@ -26,6 +34,10 @@ import {
|
|||
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({
|
||||
|
@ -43,6 +55,11 @@ const initialState = ImmutableMap({
|
|||
loaded: false,
|
||||
items: ImmutableList(),
|
||||
}),
|
||||
trending: ImmutableMap({
|
||||
next: null,
|
||||
loaded: false,
|
||||
items: ImmutableList(),
|
||||
}),
|
||||
});
|
||||
|
||||
const normalizeList = (state, listType, statuses, next) => {
|
||||
|
@ -96,6 +113,16 @@ export default function statusLists(state = initialState, action) {
|
|||
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:
|
||||
case TRENDS_STATUSES_EXPAND_REQUEST:
|
||||
return state.setIn(['trending', 'isLoading'], true);
|
||||
case TRENDS_STATUSES_FETCH_FAIL:
|
||||
case TRENDS_STATUSES_EXPAND_FAIL:
|
||||
return state.setIn(['trending', 'isLoading'], false);
|
||||
case TRENDS_STATUSES_FETCH_SUCCESS:
|
||||
return normalizeList(state, 'trending', action.statuses, action.next);
|
||||
case TRENDS_STATUSES_EXPAND_SUCCESS:
|
||||
return appendToList(state, 'trending', action.statuses, action.next);
|
||||
case FAVOURITE_SUCCESS:
|
||||
return prependOneToList(state, 'favourites', action.status);
|
||||
case UNFAVOURITE_SUCCESS:
|
||||
|
@ -110,6 +137,9 @@ export default function statusLists(state = initialState, action) {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -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 '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.set('items', fromJS(action.trends));
|
||||
map.set('isLoading', false);
|
||||
map.setIn(['tags', 'items'], fromJS(action.trends));
|
||||
map.setIn(['tags', 'isLoading'], false);
|
||||
});
|
||||
case TRENDS_FETCH_FAIL:
|
||||
return state.set('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.setIn(['links', 'items'], fromJS(action.trends));
|
||||
map.setIn(['links', 'isLoading'], false);
|
||||
});
|
||||
case TRENDS_LINKS_FETCH_FAIL:
|
||||
return state.setIn(['links', 'isLoading'], false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -863,6 +863,10 @@
|
|||
position: relative;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable.fullscreen {
|
||||
|
@ -1751,3 +1755,4 @@ noscript {
|
|||
@import 'error_boundary';
|
||||
@import 'single_column';
|
||||
@import 'announcements';
|
||||
@import 'explore';
|
||||
|
|
|
@ -158,10 +158,6 @@ export function ListAdder () {
|
|||
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');
|
||||
}
|
||||
|
@ -181,3 +177,7 @@ export function CompareHistoryModal () {
|
|||
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');
|
||||
}
|
||||
|
|
|
@ -3,14 +3,26 @@ import { forceSingleColumn } from 'flavours/glitch/util/initial_state';
|
|||
|
||||
const LAYOUT_BREAKPOINT = 630;
|
||||
|
||||
export function isMobile(width, columns) {
|
||||
switch (columns) {
|
||||
export const isMobile = width => width <= LAYOUT_BREAKPOINT;
|
||||
|
||||
export const layoutFromWindow = (layout_local_setting) => {
|
||||
switch (layout_local_setting) {
|
||||
case 'multiple':
|
||||
return false;
|
||||
return 'multi-column';
|
||||
case 'single':
|
||||
return true;
|
||||
if (isMobile(window.innerWidth)) {
|
||||
return 'mobile';
|
||||
} else {
|
||||
return 'single-column';
|
||||
}
|
||||
default:
|
||||
return forceSingleColumn || width <= LAYOUT_BREAKPOINT;
|
||||
if (isMobile(window.innerWidth)) {
|
||||
return 'mobile';
|
||||
} else if (forceSingleColumn) {
|
||||
return 'single-column';
|
||||
} else {
|
||||
return 'multi-column';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -19,17 +31,13 @@ const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
|||
let userTouching = false;
|
||||
let listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
function touchListener() {
|
||||
const touchListener = () => {
|
||||
userTouching = true;
|
||||
window.removeEventListener('touchstart', touchListener, listenerOptions);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('touchstart', touchListener, listenerOptions);
|
||||
|
||||
export function isUserTouching() {
|
||||
return userTouching;
|
||||
}
|
||||
export const isUserTouching = () => userTouching;
|
||||
|
||||
export function isIOS() {
|
||||
return iOS;
|
||||
};
|
||||
export const isIOS = () => iOS;
|
||||
|
|
Loading…
Reference in New Issue