diff options
Diffstat (limited to 'app/javascript')
16 files changed, 283 insertions, 60 deletions
diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 882c1709e..b670d25c3 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -33,7 +33,7 @@ export function submitSearch() { dispatch(fetchSearchRequest()); - api(getState).get('/api/v1/search', { + api(getState).get('/api/v2/search', { params: { q: value, resolve: true, diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js new file mode 100644 index 000000000..853e4f60a --- /dev/null +++ b/app/javascript/mastodon/actions/trends.js @@ -0,0 +1,32 @@ +import api from '../api'; + +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 fetchTrends = () => (dispatch, getState) => { + dispatch(fetchTrendsRequest()); + + api(getState) + .get('/api/v1/trends') + .then(({ data }) => dispatch(fetchTrendsSuccess(data))) + .catch(err => dispatch(fetchTrendsFail(err))); +}; + +export const fetchTrendsRequest = () => ({ + type: TRENDS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendsSuccess = trends => ({ + type: TRENDS_FETCH_SUCCESS, + trends, + skipLoading: true, +}); + +export const fetchTrendsFail = error => ({ + type: TRENDS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index fd6858d05..4b433f32c 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -25,6 +25,7 @@ export default class ScrollableList extends PureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, prepend: PropTypes.node, + alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, children: PropTypes.node, }; @@ -140,7 +141,7 @@ export default class ScrollableList extends PureComponent { } render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); @@ -172,8 +173,12 @@ export default class ScrollableList extends PureComponent { ); } else { scrollableArea = ( - <div className='empty-column-indicator' ref={this.setRef}> - {emptyMessage} + <div style={{ flex: '1 1 auto', display: 'flex', flexDirection: 'column' }}> + {alwaysPrepend && prepend} + + <div className='empty-column-indicator' ref={this.setRef}> + {emptyMessage} + </div> </div> ); } diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 0c971ceb0..1c34d0640 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 { hasMore: PropTypes.bool, prepend: PropTypes.node, emptyMessage: PropTypes.node, + alwaysPrepend: PropTypes.bool, }; static defaultProps = { diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index f9ee835bc..d375edbd5 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -127,6 +127,7 @@ export default class CommunityTimeline extends React.PureComponent { <StatusListContainer prepend={headline} + alwaysPrepend trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} timelineId={`community${onlyMedia ? ':media' : ''}`} diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index 84455563c..445bf27bb 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -1,28 +1,82 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, FormattedNumber } from 'react-intl'; import AccountContainer from '../../../containers/account_container'; import StatusContainer from '../../../containers/status_container'; import { Link } from 'react-router-dom'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +const shortNumberFormat = number => { + if (number < 1000) { + return <FormattedNumber value={number} />; + } else { + return <React.Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</React.Fragment>; + } +}; + +const renderHashtag = hashtag => ( + <div className='trends__item' key={hashtag.get('name')}> + <div className='trends__item__name'> + <Link to={`/timelines/tag/${hashtag.get('name')}`}> + #<span>{hashtag.get('name')}</span> + </Link> + + <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} /> + </div> + + <div className='trends__item__current'> + {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))} + </div> + + <div className='trends__item__sparkline'> + <Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}> + <SparklinesCurve style={{ fill: 'none' }} /> + </Sparklines> + </div> + </div> +); export default class SearchResults extends ImmutablePureComponent { static propTypes = { results: ImmutablePropTypes.map.isRequired, + trends: ImmutablePropTypes.list, + fetchTrends: PropTypes.func.isRequired, }; + componentDidMount () { + const { fetchTrends } = this.props; + fetchTrends(); + } + render () { - const { results } = this.props; + const { results, trends } = this.props; let accounts, statuses, hashtags; let count = 0; + if (results.isEmpty()) { + return ( + <div className='search-results'> + <div className='trends'> + <div className='trends__header'> + <i className='fa fa-fire fa-fw' /> + <FormattedMessage id='trends.header' defaultMessage='Trending now' /> + </div> + + {trends && trends.map(hashtag => renderHashtag(hashtag))} + </div> + </div> + ); + } + if (results.get('accounts') && results.get('accounts').size > 0) { count += results.get('accounts').size; accounts = ( <div className='search-results__section'> - <h5><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> + <h5><i className='fa fa-fw fa-users' /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} </div> @@ -33,7 +87,7 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('statuses').size; statuses = ( <div className='search-results__section'> - <h5><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> + <h5><i className='fa fa-fw fa-quote-right' /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} </div> @@ -44,13 +98,9 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('hashtags').size; hashtags = ( <div className='search-results__section'> - <h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> + <h5><i className='fa fa-fw fa-hashtag' /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> - {results.get('hashtags').map(hashtag => ( - <Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}> - #{hashtag} - </Link> - ))} + {results.get('hashtags').map(hashtag => renderHashtag(hashtag))} </div> ); } @@ -58,6 +108,7 @@ export default class SearchResults extends ImmutablePureComponent { return ( <div className='search-results'> <div className='search-results__header'> + <i className='fa fa-search fa-fw' /> <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> </div> diff --git a/app/javascript/mastodon/features/compose/containers/search_results_container.js b/app/javascript/mastodon/features/compose/containers/search_results_container.js index 16d95d417..7273460e2 100644 --- a/app/javascript/mastodon/features/compose/containers/search_results_container.js +++ b/app/javascript/mastodon/features/compose/containers/search_results_container.js @@ -1,8 +1,14 @@ import { connect } from 'react-redux'; import SearchResults from '../components/search_results'; +import { fetchTrends } from '../../../actions/trends'; const mapStateToProps = state => ({ results: state.getIn(['search', 'results']), + trends: state.get('trends'), }); -export default connect(mapStateToProps)(SearchResults); +const mapDispatchToProps = dispatch => ({ + fetchTrends: () => dispatch(fetchTrends()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(SearchResults); diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 19aae0332..d8e9ad9ee 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -101,7 +101,7 @@ export default class Compose extends React.PureComponent { {(multiColumn || isSearchPage) && <SearchContainer /> } <div className='drawer__pager'> - <div className='drawer__inner' onFocus={this.onFocus}> + {!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}> <NavigationContainer onClose={this.onBlur} /> <ComposeFormContainer /> {multiColumn && ( @@ -109,7 +109,7 @@ export default class Compose extends React.PureComponent { <img alt='' draggable='false' src={elephantUIPlane} /> </div> )} - </div> + </div>} <Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}> {({ x }) => ( diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 15cc3e31d..341af582a 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -127,6 +127,7 @@ export default class PublicTimeline extends React.PureComponent { <StatusListContainer prepend={headline} + alwaysPrepend timelineId={`public${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} trackScroll={!pinned} diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 2e53dfa7e..505a88a3f 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -62,31 +62,28 @@ const makeMapStateToProps = () => { if (status) { ancestorsIds = ancestorsIds.withMutations(mutable => { - function addAncestor(id) { - if (id) { - const inReplyTo = state.getIn(['contexts', 'inReplyTos', id]); + let id = status.get('in_reply_to_id'); - mutable.unshift(id); - addAncestor(inReplyTo); - } + while (id) { + mutable.unshift(id); + id = state.getIn(['contexts', 'inReplyTos', id]); } - - addAncestor(status.get('in_reply_to_id')); }); descendantsIds = descendantsIds.withMutations(mutable => { - function addDescendantOf(id) { + const ids = [status.get('id')]; + + while (ids.length > 0) { + let id = ids.shift(); const replies = state.getIn(['contexts', 'replies', id]); if (replies) { replies.forEach(reply => { mutable.push(reply); - addDescendantOf(reply); + ids.unshift(reply); }); } } - - addDescendantOf(status.get('id')); }); } diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 3d9a6a132..019c1f466 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -26,6 +26,7 @@ import height_cache from './height_cache'; import custom_emojis from './custom_emojis'; import lists from './lists'; import listEditor from './list_editor'; +import trends from './trends'; const reducers = { dropdown_menu, @@ -55,6 +56,7 @@ const reducers = { custom_emojis, lists, listEditor, + trends, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js index 56fd7226b..4758defb1 100644 --- a/app/javascript/mastodon/reducers/search.js +++ b/app/javascript/mastodon/reducers/search.js @@ -9,7 +9,7 @@ import { COMPOSE_REPLY, COMPOSE_DIRECT, } from '../actions/compose'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ value: '', @@ -39,7 +39,7 @@ export default function search(state = initialState, action) { return state.set('results', ImmutableMap({ accounts: ImmutableList(action.results.accounts.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)), - hashtags: ImmutableList(action.results.hashtags), + hashtags: fromJS(action.results.hashtags), })).set('submitted', true); default: return state; diff --git a/app/javascript/mastodon/reducers/trends.js b/app/javascript/mastodon/reducers/trends.js new file mode 100644 index 000000000..95cf8f284 --- /dev/null +++ b/app/javascript/mastodon/reducers/trends.js @@ -0,0 +1,13 @@ +import { TRENDS_FETCH_SUCCESS } from '../actions/trends'; +import { fromJS } from 'immutable'; + +const initialState = null; + +export default function trendsReducer(state = initialState, action) { + switch(action.type) { + case TRENDS_FETCH_SUCCESS: + return fromJS(action.trends); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js index ce42271a9..c1854c1cd 100644 --- a/app/javascript/mastodon/service_worker/entry.js +++ b/app/javascript/mastodon/service_worker/entry.js @@ -49,7 +49,7 @@ self.addEventListener('fetch', function(event) { return response; })); - } else if (storageFreeable && process.env.CDN_HOST ? url.host === process.env.CDN_HOST : url.pathname.startsWith('/system/')) { + } else if (storageFreeable && (ATTACHMENT_HOST ? url.host === ATTACHMENT_HOST : url.pathname.startsWith('/system/'))) { event.respondWith(openSystemCache().then(cache => { return cache.match(event.request.url).then(cached => { if (cached === undefined) { diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 2b88b830c..fe304317d 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -132,20 +132,54 @@ .boost-modal, .confirmation-modal, .mute-modal, -.report-modal { +.report-modal, +.embed-modal, +.error-modal, +.onboarding-modal { background: $ui-base-color; } .boost-modal__action-bar, .confirmation-modal__action-bar, -.mute-modal__action-bar { +.mute-modal__action-bar, +.onboarding-modal__paginator, +.error-modal__footer { background: darken($ui-base-color, 6%); + + .onboarding-modal__nav, + .error-modal__nav { + &:hover, + &:focus, + &:active { + background-color: darken($ui-base-color, 12%); + } + } +} + +.display-case__case { + background: $white; +} + +.embed-modal .embed-modal__container .embed-modal__html { + background: $white; + + &:focus { + background: darken($ui-base-color, 6%); + } } .react-toggle-track { background: $ui-secondary-color; } +.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { + background: darken($ui-secondary-color, 10%); +} + +.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { + background: lighten($ui-highlight-color, 10%); +} + // Change the default color used for the text in an empty column or on the error column .empty-column-indicator, .error-column { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index abffa6daa..712b6f813 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3284,6 +3284,15 @@ a.status-card { } .search__icon { + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus { + outline: 0 !important; + } + .fa { position: absolute; top: 10px; @@ -3333,40 +3342,33 @@ a.status-card { .search-results__header { color: $dark-text-color; background: lighten($ui-base-color, 2%); - border-bottom: 1px solid darken($ui-base-color, 4%); - padding: 15px 10px; - font-size: 14px; + padding: 15px; font-weight: 500; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } } .search-results__section { - margin-bottom: 20px; + margin-bottom: 5px; h5 { - position: relative; - - &::before { - content: ""; - display: block; - position: absolute; - left: 0; - right: 0; - top: 50%; - width: 100%; - height: 0; - border-top: 1px solid lighten($ui-base-color, 8%); - } + background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 8%); + cursor: default; + display: flex; + padding: 15px; + font-weight: 500; + font-size: 16px; + color: $dark-text-color; - span { + .fa { display: inline-block; - background: $ui-base-color; - color: $darker-text-color; - font-size: 14px; - font-weight: 500; - padding: 10px; - position: relative; - z-index: 1; - cursor: default; + margin-right: 5px; } } @@ -5209,3 +5211,81 @@ noscript { background: $ui-base-color; } } + +.trends { + &__header { + color: $dark-text-color; + background: lighten($ui-base-color, 2%); + border-bottom: 1px solid darken($ui-base-color, 4%); + font-weight: 500; + padding: 15px; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } + } + + &__item { + display: flex; + align-items: center; + padding: 15px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + + &:last-child { + border-bottom: 0; + } + + &__name { + flex: 1 1 auto; + color: $dark-text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + strong { + font-weight: 500; + } + + a { + color: $darker-text-color; + text-decoration: none; + font-size: 14px; + font-weight: 500; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover, + &:focus, + &:active { + span { + text-decoration: underline; + } + } + } + } + + &__current { + flex: 0 0 auto; + width: 100px; + font-size: 24px; + line-height: 36px; + font-weight: 500; + text-align: center; + color: $secondary-text-color; + } + + &__sparkline { + flex: 0 0 auto; + width: 50px; + + path { + stroke: lighten($highlight-text-color, 6%) !important; + } + } + } +} |