diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js index 853e4f60a..304bbebef 100644 --- a/app/javascript/mastodon/actions/trends.js +++ b/app/javascript/mastodon/actions/trends.js @@ -1,31 +1,94 @@ import api from '../api'; +import { importFetchedStatuses } from './importer'; -export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; -export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; -export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; +export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; +export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS'; +export const TRENDS_TAGS_FETCH_FAIL = 'TRENDS_TAGS_FETCH_FAIL'; -export const fetchTrends = () => (dispatch, getState) => { - dispatch(fetchTrendsRequest()); +export const TRENDS_LINKS_FETCH_REQUEST = 'TRENDS_LINKS_FETCH_REQUEST'; +export const TRENDS_LINKS_FETCH_SUCCESS = 'TRENDS_LINKS_FETCH_SUCCESS'; +export const TRENDS_LINKS_FETCH_FAIL = 'TRENDS_LINKS_FETCH_FAIL'; + +export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST'; +export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS'; +export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL'; + +export const fetchTrendingHashtags = () => (dispatch, getState) => { + dispatch(fetchTrendingHashtagsRequest()); api(getState) - .get('/api/v1/trends') - .then(({ data }) => dispatch(fetchTrendsSuccess(data))) - .catch(err => dispatch(fetchTrendsFail(err))); + .get('/api/v1/trends/tags') + .then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data))) + .catch(err => dispatch(fetchTrendingHashtagsFail(err))); }; -export const fetchTrendsRequest = () => ({ - type: TRENDS_FETCH_REQUEST, +export const fetchTrendingHashtagsRequest = () => ({ + type: TRENDS_TAGS_FETCH_REQUEST, skipLoading: true, }); -export const fetchTrendsSuccess = trends => ({ - type: TRENDS_FETCH_SUCCESS, +export const fetchTrendingHashtagsSuccess = trends => ({ + type: TRENDS_TAGS_FETCH_SUCCESS, trends, skipLoading: true, }); -export const fetchTrendsFail = error => ({ - type: TRENDS_FETCH_FAIL, +export const fetchTrendingHashtagsFail = error => ({ + type: TRENDS_TAGS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); + +export const fetchTrendingLinks = () => (dispatch, getState) => { + dispatch(fetchTrendingLinksRequest()); + + api(getState) + .get('/api/v1/trends/links') + .then(({ data }) => dispatch(fetchTrendingLinksSuccess(data))) + .catch(err => dispatch(fetchTrendingLinksFail(err))); +}; + +export const fetchTrendingLinksRequest = () => ({ + type: TRENDS_LINKS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendingLinksSuccess = trends => ({ + type: TRENDS_LINKS_FETCH_SUCCESS, + trends, + skipLoading: true, +}); + +export const fetchTrendingLinksFail = error => ({ + type: TRENDS_LINKS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); + +export const fetchTrendingStatuses = () => (dispatch, getState) => { + 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, skipLoading: true, skipAlert: true, diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js index a793a32f5..7f442d189 100644 --- a/app/javascript/mastodon/components/hashtag.js +++ b/app/javascript/mastodon/components/hashtag.js @@ -38,7 +38,7 @@ class SilentErrorBoundary extends React.Component { * * @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} */ -const accountsCountRenderer = (displayNumber, pluralReady) => ( +export const accountsCountRenderer = (displayNumber, pluralReady) => ( - - + + {shareButton} diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index eaaffcc3a..35e5749a3 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/explore/components/story.js b/app/javascript/mastodon/features/explore/components/story.js new file mode 100644 index 000000000..563128029 --- /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 000000000..ddacf5812 --- /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 000000000..6649fb6e4 --- /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 000000000..27e8aaa4f --- /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 000000000..4e5530d84 --- /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 000000000..c094a8d93 --- /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 000000000..c0ad9fc6e --- /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 7a5268780..a73832db7 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/search/index.js b/app/javascript/mastodon/features/search/index.js deleted file mode 100644 index 76bf70d4b..000000000 --- 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 = () => ( -
- - -
-
- -
-
-
-); - -export default Search; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 193637113..db047f5f0 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -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 { diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index 901dbdfcb..a70e5ab61 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -13,6 +13,7 @@ const NavigationPanel = () => ( + diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index a023bcf34..195403fd3 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -10,9 +10,9 @@ import NotificationsCounterIcon from './notifications_counter_icon'; export const links = [ , , - , - , - , + , + , + , , ]; diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 3feffa656..2d0136992 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -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 { - + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 5349bd656..92c683e2f 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -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'); +} diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js index 875b2d92b..23bbe4d99 100644 --- a/app/javascript/mastodon/reducers/search.js +++ b/app/javascript/mastodon/reducers/search.js @@ -1,6 +1,8 @@ import { SEARCH_CHANGE, SEARCH_CLEAR, + SEARCH_FETCH_REQUEST, + SEARCH_FETCH_FAIL, SEARCH_FETCH_SUCCESS, SEARCH_SHOW, SEARCH_EXPAND_SUCCESS, @@ -17,6 +19,7 @@ const initialState = ImmutableMap({ submitted: false, hidden: false, results: ImmutableMap(), + isLoading: false, searchTerm: '', }); @@ -37,12 +40,22 @@ export default function search(state = initialState, action) { case COMPOSE_MENTION: case COMPOSE_DIRECT: 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: - return state.set('results', ImmutableMap({ - accounts: ImmutableList(action.results.accounts.map(item => item.id)), - statuses: ImmutableList(action.results.statuses.map(item => item.id)), - hashtags: fromJS(action.results.hashtags), - })).set('submitted', true).set('searchTerm', action.searchTerm); + return state.withMutations(map => { + map.set('results', ImmutableMap({ + accounts: ImmutableList(action.results.accounts.map(item => item.id)), + statuses: ImmutableList(action.results.statuses.map(item => item.id)), + hashtags: fromJS(action.results.hashtags), + })); + + map.set('submitted', true); + map.set('searchTerm', action.searchTerm); + map.set('isLoading', false); + }); case SEARCH_EXPAND_SUCCESS: const results = action.searchType === 'hashtags' ? fromJS(action.results.hashtags) : action.results[action.searchType].map(item => item.id); return state.updateIn(['results', action.searchType], list => list.concat(results)); diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index 9f8f28dee..49bc94a40 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -17,6 +17,11 @@ import { import { PINNED_STATUSES_FETCH_SUCCESS, } 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 { FAVOURITE_SUCCESS, @@ -26,6 +31,10 @@ import { PIN_SUCCESS, UNPIN_SUCCESS, } from '../actions/interactions'; +import { + ACCOUNT_BLOCK_SUCCESS, + ACCOUNT_MUTE_SUCCESS, +} from '../actions/accounts'; const initialState = ImmutableMap({ favourites: ImmutableMap({ @@ -43,6 +52,11 @@ const initialState = ImmutableMap({ loaded: false, items: ImmutableList(), }), + trending: ImmutableMap({ + next: null, + loaded: false, + items: ImmutableList(), + }), }); 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); case BOOKMARKED_STATUSES_EXPAND_SUCCESS: 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: return prependOneToList(state, 'favourites', action.status); case UNFAVOURITE_SUCCESS: @@ -110,6 +130,9 @@ export default function statusLists(state = initialState, action) { return prependOneToList(state, 'pins', action.status); case UNPIN_SUCCESS: return removeOneFromList(state, 'pins', action.status); + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + return state.updateIn(['trending', 'items'], ImmutableList(), list => list.filterNot(statusId => action.statuses.getIn([statusId, 'account']) === action.relationship.id)); default: return state; } diff --git a/app/javascript/mastodon/reducers/trends.js b/app/javascript/mastodon/reducers/trends.js index 5cecc8fca..3e01bd07d 100644 --- a/app/javascript/mastodon/reducers/trends.js +++ b/app/javascript/mastodon/reducers/trends.js @@ -1,22 +1,45 @@ -import { TRENDS_FETCH_REQUEST, TRENDS_FETCH_SUCCESS, TRENDS_FETCH_FAIL } from '../actions/trends'; +import { + TRENDS_TAGS_FETCH_REQUEST, + TRENDS_TAGS_FETCH_SUCCESS, + TRENDS_TAGS_FETCH_FAIL, + TRENDS_LINKS_FETCH_REQUEST, + TRENDS_LINKS_FETCH_SUCCESS, + TRENDS_LINKS_FETCH_FAIL, +} from 'mastodon/actions/trends'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ - items: ImmutableList(), - isLoading: false, + tags: ImmutableMap({ + items: ImmutableList(), + isLoading: false, + }), + + links: ImmutableMap({ + items: ImmutableList(), + isLoading: false, + }), }); export default function trendsReducer(state = initialState, action) { switch(action.type) { - case TRENDS_FETCH_REQUEST: - return state.set('isLoading', true); - case TRENDS_FETCH_SUCCESS: + case TRENDS_TAGS_FETCH_REQUEST: + return state.setIn(['tags', 'isLoading'], true); + case TRENDS_TAGS_FETCH_SUCCESS: return state.withMutations(map => { - map.set('items', fromJS(action.trends)); - map.set('isLoading', false); + map.setIn(['tags', 'items'], fromJS(action.trends)); + map.setIn(['tags', 'isLoading'], false); }); - case TRENDS_FETCH_FAIL: - return state.set('isLoading', false); + case TRENDS_TAGS_FETCH_FAIL: + return state.setIn(['tags', 'isLoading'], false); + case TRENDS_LINKS_FETCH_REQUEST: + return state.setIn(['links', 'isLoading'], true); + case TRENDS_LINKS_FETCH_SUCCESS: + return state.withMutations(map => { + map.setIn(['links', 'items'], fromJS(action.trends)); + map.setIn(['links', 'isLoading'], false); + }); + case TRENDS_LINKS_FETCH_FAIL: + return state.setIn(['links', 'isLoading'], false); default: return state; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 6d30bea83..647e7ea31 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2797,6 +2797,10 @@ a.account__display-name { position: relative; min-height: 120px; } + + .scrollable { + flex: 1 1 auto; + } } .scrollable.fullscreen { @@ -7724,3 +7728,122 @@ noscript { 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; + } + } + } +}