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
				
			
		
							
								
								
									
										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 { fetchTrends } from 'mastodon/actions/trends';
 | 
			
		||||
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
 | 
			
		||||
import Trends from '../components/trends';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  trends: state.getIn(['trends', 'items']),
 | 
			
		||||
  trends: state.getIn(['trends', 'tags', 'items']),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = dispatch => ({
 | 
			
		||||
  fetchTrends: () => dispatch(fetchTrends()),
 | 
			
		||||
  fetchTrends: () => dispatch(fetchTrendingHashtags()),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps, mapDispatchToProps)(Trends);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import SearchContainer from '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' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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 }))
 | 
			
		||||
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='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
 | 
			
		||||
    <FollowRequestsNavLink />
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' to='/explore' data-preview-title-id='explore.title' data-preview-icon='globe'><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='explore.title' defaultMessage='Explore' /></NavLink>
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,9 +10,9 @@ import NotificationsCounterIcon from './notifications_counter_icon';
 | 
			
		|||
export const links = [
 | 
			
		||||
  <NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link optional' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link optional' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' to='/explore' data-preview-title-id='tabs_bar.search' data-preview-icon='search' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,8 +49,8 @@ import {
 | 
			
		|||
  Mutes,
 | 
			
		||||
  PinnedStatuses,
 | 
			
		||||
  Lists,
 | 
			
		||||
  Search,
 | 
			
		||||
  Directory,
 | 
			
		||||
  Explore,
 | 
			
		||||
  FollowRecommendations,
 | 
			
		||||
} from './util/async-components';
 | 
			
		||||
import { me } from '../../initial_state';
 | 
			
		||||
| 
						 | 
				
			
			@ -167,8 +167,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} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -138,10 +138,6 @@ export function ListAdder () {
 | 
			
		|||
  return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Search () {
 | 
			
		||||
  return import(/*webpackChunkName: "features/search" */'../../search');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Tesseract () {
 | 
			
		||||
  return import(/*webpackChunkName: "tesseract" */'tesseract.js');
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -161,3 +157,7 @@ export function FollowRecommendations () {
 | 
			
		|||
export function CompareHistoryModal () {
 | 
			
		||||
  return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Explore () {
 | 
			
		||||
  return import(/* webpackChunkName: "features/explore" */'../../explore');
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue