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