-
-
+
+
{shareButton}
diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index eaaffcc3a8..35e5749a33 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -24,6 +24,7 @@ export default class StatusList extends ImmutablePureComponent {
prepend: PropTypes.node,
emptyMessage: PropTypes.node,
alwaysPrepend: PropTypes.bool,
+ withCounters: PropTypes.bool,
timelineId: PropTypes.string,
};
@@ -100,6 +101,7 @@ export default class StatusList extends ImmutablePureComponent {
contextType={timelineId}
scrollKey={this.props.scrollKey}
showThread
+ withCounters={this.props.withCounters}
/>
))
) : null;
@@ -114,6 +116,7 @@ export default class StatusList extends ImmutablePureComponent {
onMoveDown={this.handleMoveDown}
contextType={timelineId}
showThread
+ withCounters={this.props.withCounters}
/>
)).concat(scrollableContent);
}
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 26232247d9..826d9f504b 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -164,8 +164,13 @@ class ComposeForm extends ImmutablePureComponent {
selectionStart = selectionEnd;
}
- this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
- this.autosuggestTextarea.textarea.focus();
+ // Because of the wicg-inert polyfill, the activeElement may not be
+ // immediately selectable, we have to wait for observers to run, as
+ // described in https://github.com/WICG/inert#performance-and-gotchas
+ Promise.resolve().then(() => {
+ this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
+ this.autosuggestTextarea.textarea.focus();
+ }).catch(console.error);
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
this.autosuggestTextarea.textarea.focus();
} else if (this.props.spoiler !== prevProps.spoiler) {
diff --git a/app/javascript/mastodon/features/explore/components/story.js b/app/javascript/mastodon/features/explore/components/story.js
new file mode 100644
index 0000000000..5631280297
--- /dev/null
+++ b/app/javascript/mastodon/features/explore/components/story.js
@@ -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 (
+
+
+
{publisher ? publisher : }
+
{title ? title : }
+
{typeof sharedTimes === 'number' ? : }
+
+
+
+ {thumbnail ? (
+
+
+
+
+ ) :
}
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/explore/index.js b/app/javascript/mastodon/features/explore/index.js
new file mode 100644
index 0000000000..ddacf5812b
--- /dev/null
+++ b/app/javascript/mastodon/features/explore/index.js
@@ -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 (
+
+ {layout === 'mobile' ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ {isSearching ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/explore/links.js b/app/javascript/mastodon/features/explore/links.js
new file mode 100644
index 0000000000..6649fb6e47
--- /dev/null
+++ b/app/javascript/mastodon/features/explore/links.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Story from './components/story';
+import LoadingIndicator from '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 (
+
+ {isLoading ? () : links.map(link => (
+
+ ))}
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/explore/results.js b/app/javascript/mastodon/features/explore/results.js
new file mode 100644
index 0000000000..27e8aaa4f2
--- /dev/null
+++ b/app/javascript/mastodon/features/explore/results.js
@@ -0,0 +1,113 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { expandSearch } from '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(
);
+ } else {
+ return list;
+ }
+};
+
+const renderAccounts = (results, onLoadMore) => appendLoadMore('accounts', results.get('accounts').map(item => (
+
+)), onLoadMore);
+
+const renderHashtags = (results, onLoadMore) => appendLoadMore('hashtags', results.get('hashtags').map(item => (
+
+)), onLoadMore);
+
+const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', results.get('statuses').map(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 = (
+
+
+
+ );
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {isLoading ? () : filteredResults}
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/explore/statuses.js b/app/javascript/mastodon/features/explore/statuses.js
new file mode 100644
index 0000000000..4e5530d84c
--- /dev/null
+++ b/app/javascript/mastodon/features/explore/statuses.js
@@ -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 =
;
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/explore/suggestions.js b/app/javascript/mastodon/features/explore/suggestions.js
new file mode 100644
index 0000000000..c094a8d934
--- /dev/null
+++ b/app/javascript/mastodon/features/explore/suggestions.js
@@ -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 (
+
+ {isLoading ? (
) : suggestions.map(suggestion => (
+
+ ))}
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/explore/tags.js b/app/javascript/mastodon/features/explore/tags.js
new file mode 100644
index 0000000000..c0ad9fc6ec
--- /dev/null
+++ b/app/javascript/mastodon/features/explore/tags.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { ImmutableHashtag as Hashtag } from '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 (
+
+ {isLoading ? () : hashtags.map(hashtag => (
+
+ ))}
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/getting_started/containers/trends_container.js b/app/javascript/mastodon/features/getting_started/containers/trends_container.js
index 7a52687808..a73832db75 100644
--- a/app/javascript/mastodon/features/getting_started/containers/trends_container.js
+++ b/app/javascript/mastodon/features/getting_started/containers/trends_container.js
@@ -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);
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
index 0de562ee1d..0cb42b25aa 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
@@ -60,7 +60,7 @@ class Footer extends ImmutablePureComponent {
const { router } = this.context;
if (onClose) {
- onClose();
+ onClose(true);
}
dispatch(replyCompose(status, router.history));
@@ -156,7 +156,7 @@ class Footer extends ImmutablePureComponent {
- {withOpenButton &&
}
+ {withOpenButton &&
}
);
}
diff --git a/app/javascript/mastodon/features/search/index.js b/app/javascript/mastodon/features/search/index.js
deleted file mode 100644
index 76bf70d4bd..0000000000
--- a/app/javascript/mastodon/features/search/index.js
+++ /dev/null
@@ -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 = () => (
-