diff options
Diffstat (limited to 'app')
31 files changed, 950 insertions, 190 deletions
diff --git a/app/javascript/flavours/glitch/actions/app.js b/app/javascript/flavours/glitch/actions/app.js new file mode 100644 index 000000000..de2d93e29 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/app.js @@ -0,0 +1,6 @@ +export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE'; + +export const changeLayout = layout => ({ + type: APP_LAYOUT_CHANGE, + layout, +}); diff --git a/app/javascript/flavours/glitch/actions/trends.js b/app/javascript/flavours/glitch/actions/trends.js index 1b0ce2b5b..e9aa28dd3 100644 --- a/app/javascript/flavours/glitch/actions/trends.js +++ b/app/javascript/flavours/glitch/actions/trends.js @@ -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, +}); diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index 5bbf32c87..2bb7f02f7 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -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}}' diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index e238456c5..d3b6bd7e9 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -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} />)} diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index c0cd496ce..78964e882 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -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} /> diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js index ac255f4ac..0d843a27d 100644 --- a/app/javascript/flavours/glitch/components/status_list.js +++ b/app/javascript/flavours/glitch/components/status_list.js @@ -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); } diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 0ba2e712c..370308043 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -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', + }, }; }; diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js index 6c554336c..218208c10 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.js +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -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') diff --git a/app/javascript/flavours/glitch/features/explore/components/story.js b/app/javascript/flavours/glitch/features/explore/components/story.js new file mode 100644 index 000000000..8270d3ccb --- /dev/null +++ b/app/javascript/flavours/glitch/features/explore/components/story.js @@ -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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/explore/index.js b/app/javascript/flavours/glitch/features/explore/index.js new file mode 100644 index 000000000..c89463e7e --- /dev/null +++ b/app/javascript/flavours/glitch/features/explore/index.js @@ -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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/explore/links.js b/app/javascript/flavours/glitch/features/explore/links.js new file mode 100644 index 000000000..1d8cd8efc --- /dev/null +++ b/app/javascript/flavours/glitch/features/explore/links.js @@ -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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/explore/results.js b/app/javascript/flavours/glitch/features/explore/results.js new file mode 100644 index 000000000..8eac63c2c --- /dev/null +++ b/app/javascript/flavours/glitch/features/explore/results.js @@ -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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/explore/statuses.js b/app/javascript/flavours/glitch/features/explore/statuses.js new file mode 100644 index 000000000..fe08ce466 --- /dev/null +++ b/app/javascript/flavours/glitch/features/explore/statuses.js @@ -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 + /> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/explore/suggestions.js b/app/javascript/flavours/glitch/features/explore/suggestions.js new file mode 100644 index 000000000..ccd2c950a --- /dev/null +++ b/app/javascript/flavours/glitch/features/explore/suggestions.js @@ -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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/explore/tags.js b/app/javascript/flavours/glitch/features/explore/tags.js new file mode 100644 index 000000000..0ec1eb88b --- /dev/null +++ b/app/javascript/flavours/glitch/features/explore/tags.js @@ -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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js b/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js index 68568d169..d88dbbaf4 100644 --- a/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js +++ b/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js @@ -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); diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js index 56750fd88..8b6a617ff 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.js +++ b/app/javascript/flavours/glitch/features/getting_started/index.js @@ -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' />); - } - - if (profile_directory) { - navItems.push(<ColumnLink key='7' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />); + navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />); } - 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>, ]); diff --git a/app/javascript/flavours/glitch/features/search/index.js b/app/javascript/flavours/glitch/features/search/index.js deleted file mode 100644 index b35c8ed49..000000000 --- a/app/javascript/flavours/glitch/features/search/index.js +++ /dev/null @@ -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; diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index bfb1ae405..048251fa6 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -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' }, diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js index d91f9b8c4..67fa067f9 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js @@ -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> diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js index 2dcd535ca..f4d649456 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js @@ -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 /> diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js index 55cc84f5e..0a7078a9c 100644 --- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js +++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js @@ -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>, ]; diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 2be6d9478..7b547fd5b 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -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 /> diff --git a/app/javascript/flavours/glitch/reducers/meta.js b/app/javascript/flavours/glitch/reducers/meta.js index 0f3ab3b84..0364ec289 100644 --- a/app/javascript/flavours/glitch/reducers/meta.js +++ b/app/javascript/flavours/glitch/reducers/meta.js @@ -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; } diff --git a/app/javascript/flavours/glitch/reducers/search.js b/app/javascript/flavours/glitch/reducers/search.js index c346e958b..4b8913e96 100644 --- a/app/javascript/flavours/glitch/reducers/search.js +++ b/app/javascript/flavours/glitch/reducers/search.js @@ -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)); diff --git a/app/javascript/flavours/glitch/reducers/status_lists.js b/app/javascript/flavours/glitch/reducers/status_lists.js index 241833bfe..ada0484f4 100644 --- a/app/javascript/flavours/glitch/reducers/status_lists.js +++ b/app/javascript/flavours/glitch/reducers/status_lists.js @@ -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; } diff --git a/app/javascript/flavours/glitch/reducers/trends.js b/app/javascript/flavours/glitch/reducers/trends.js index 5cecc8fca..e2bac6199 100644 --- a/app/javascript/flavours/glitch/reducers/trends.js +++ b/app/javascript/flavours/glitch/reducers/trends.js @@ -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.setIn(['tags', 'items'], fromJS(action.trends)); + map.setIn(['tags', 'isLoading'], false); + }); + case TRENDS_TAGS_FETCH_FAIL: + return state.setIn(['tags', 'isLoading'], false); + case TRENDS_LINKS_FETCH_REQUEST: + return state.setIn(['links', 'isLoading'], true); + case TRENDS_LINKS_FETCH_SUCCESS: return state.withMutations(map => { - map.set('items', fromJS(action.trends)); - map.set('isLoading', false); + map.setIn(['links', 'items'], fromJS(action.trends)); + map.setIn(['links', 'isLoading'], false); }); - case TRENDS_FETCH_FAIL: - return state.set('isLoading', false); + case TRENDS_LINKS_FETCH_FAIL: + return state.setIn(['links', 'isLoading'], false); default: return state; } diff --git a/app/javascript/flavours/glitch/styles/components/explore.scss b/app/javascript/flavours/glitch/styles/components/explore.scss new file mode 100644 index 000000000..05df34eff --- /dev/null +++ b/app/javascript/flavours/glitch/styles/components/explore.scss @@ -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; + } + } + } +} diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index b54c3f696..14fbc61b5 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -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'; diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js index 86bb7be36..1ecba2bcb 100644 --- a/app/javascript/flavours/glitch/util/async-components.js +++ b/app/javascript/flavours/glitch/util/async-components.js @@ -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'); +} diff --git a/app/javascript/flavours/glitch/util/is_mobile.js b/app/javascript/flavours/glitch/util/is_mobile.js index 7e584e8fa..c8517f592 100644 --- a/app/javascript/flavours/glitch/util/is_mobile.js +++ b/app/javascript/flavours/glitch/util/is_mobile.js @@ -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; |