From 48c55b6392661cde8e28cf076c3d132c22d17a0f Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 25 Jan 2020 16:35:33 +0100 Subject: [PATCH] Improve announcements design (#12954) * Move announcements above scroll container; add button to temporarily hide them * Remove interface for dismissing announcements * Display number of unread announcements * Count unread announcements accurately * Fix size of announcement box not fitting the currently displayed announcement * Fix announcement box background color to match button color --- .../mastodon/actions/announcements.js | 18 ++++----- .../components/announcements.js | 20 +--------- .../containers/announcements_container.js | 4 +- .../mastodon/features/home_timeline/index.js | 40 +++++++++++++++++-- .../mastodon/reducers/announcements.js | 28 ++++++++++--- .../styles/mastodon/components.scss | 8 +--- 6 files changed, 71 insertions(+), 47 deletions(-) diff --git a/app/javascript/mastodon/actions/announcements.js b/app/javascript/mastodon/actions/announcements.js index c65bc052ec3..64bf5ef91bd 100644 --- a/app/javascript/mastodon/actions/announcements.js +++ b/app/javascript/mastodon/actions/announcements.js @@ -5,7 +5,6 @@ export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST'; export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS'; export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL'; export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE'; -export const ANNOUNCEMENTS_DISMISS = 'ANNOUNCEMENTS_DISMISS'; export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST'; export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS'; @@ -17,6 +16,8 @@ export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REM export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE'; +export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW'; + const noOp = () => {}; export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => { @@ -54,15 +55,6 @@ export const updateAnnouncements = announcement => ({ announcement: normalizeAnnouncement(announcement), }); -export const dismissAnnouncement = announcementId => (dispatch, getState) => { - dispatch({ - type: ANNOUNCEMENTS_DISMISS, - id: announcementId, - }); - - api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`); -}; - export const addReaction = (announcementId, name) => (dispatch, getState) => { dispatch(addReactionRequest(announcementId, name)); @@ -131,3 +123,9 @@ export const updateReaction = reaction => ({ type: ANNOUNCEMENTS_REACTION_UPDATE, reaction, }); + +export function toggleShowAnnouncements() { + return dispatch => { + dispatch({ type: ANNOUNCEMENTS_TOGGLE_SHOW }); + }; +} diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js index 975db026562..8ff1b0b4e80 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.js +++ b/app/javascript/mastodon/features/getting_started/components/announcements.js @@ -277,19 +277,13 @@ class Announcement extends ImmutablePureComponent { static propTypes = { announcement: ImmutablePropTypes.map.isRequired, emojiMap: ImmutablePropTypes.map.isRequired, - dismissAnnouncement: PropTypes.func.isRequired, addReaction: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; - handleDismissClick = () => { - const { dismissAnnouncement, announcement } = this.props; - dismissAnnouncement(announcement.get('id')); - } - render () { - const { announcement, intl } = this.props; + const { announcement } = this.props; const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at')); const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at')); const now = new Date(); @@ -314,8 +308,6 @@ class Announcement extends ImmutablePureComponent { removeReaction={this.props.removeReaction} emojiMap={this.props.emojiMap} /> - - ); } @@ -328,8 +320,6 @@ class Announcements extends ImmutablePureComponent { static propTypes = { announcements: ImmutablePropTypes.list, emojiMap: ImmutablePropTypes.map.isRequired, - fetchAnnouncements: PropTypes.func.isRequired, - dismissAnnouncement: PropTypes.func.isRequired, addReaction: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -339,11 +329,6 @@ class Announcements extends ImmutablePureComponent { index: 0, }; - componentDidMount () { - const { fetchAnnouncements } = this.props; - fetchAnnouncements(); - } - handleChangeIndex = index => { this.setState({ index: index % this.props.announcements.size }); } @@ -369,13 +354,12 @@ class Announcements extends ImmutablePureComponent {
- + {announcements.map(announcement => ( ({ }); const mapDispatchToProps = dispatch => ({ - fetchAnnouncements: () => dispatch(fetchAnnouncements()), - dismissAnnouncement: id => dispatch(dismissAnnouncement(id)), addReaction: (id, name) => dispatch(addReaction(id, name)), removeReaction: (id, name) => dispatch(removeReaction(id, name)), }); diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index b7f9d509524..c7de8c9cbf4 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -9,15 +9,23 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; import { Link } from 'react-router-dom'; +import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements'; import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container'; +import classNames from 'classnames'; +import IconWithBadge from 'mastodon/components/icon_with_badge'; const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, + show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' }, + hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' }, }); const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, isPartial: state.getIn(['timelines', 'home', 'isPartial']), + hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(), + unreadAnnouncements: state.getIn(['announcements', 'unread']).size, + showAnnouncements: state.getIn(['announcements', 'show']), }); export default @connect(mapStateToProps) @@ -32,6 +40,9 @@ class HomeTimeline extends React.PureComponent { isPartial: PropTypes.bool, columnId: PropTypes.string, multiColumn: PropTypes.bool, + hasAnnouncements: PropTypes.bool, + unreadAnnouncements: PropTypes.number, + showAnnouncements: PropTypes.bool, }; handlePin = () => { @@ -62,6 +73,7 @@ class HomeTimeline extends React.PureComponent { } componentDidMount () { + this.props.dispatch(fetchAnnouncements()); this._checkIfReloadNeeded(false, this.props.isPartial); } @@ -94,10 +106,31 @@ class HomeTimeline extends React.PureComponent { } } + handleToggleAnnouncementsClick = (e) => { + e.stopPropagation(); + this.props.dispatch(toggleShowAnnouncements()); + } + render () { - const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props; + const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const pinned = !!columnId; + let announcementsButton = null; + + if (hasAnnouncements) { + announcementsButton = ( + + ); + } + return ( + {hasAnnouncements && showAnnouncements && } + } - alwaysPrepend trackScroll={!pinned} scrollKey={`home_timeline-${columnId}`} onLoadMore={this.handleLoadMore} diff --git a/app/javascript/mastodon/reducers/announcements.js b/app/javascript/mastodon/reducers/announcements.js index aa674e516f5..1cfb598fb98 100644 --- a/app/javascript/mastodon/reducers/announcements.js +++ b/app/javascript/mastodon/reducers/announcements.js @@ -3,18 +3,20 @@ import { ANNOUNCEMENTS_FETCH_SUCCESS, ANNOUNCEMENTS_FETCH_FAIL, ANNOUNCEMENTS_UPDATE, - ANNOUNCEMENTS_DISMISS, ANNOUNCEMENTS_REACTION_UPDATE, ANNOUNCEMENTS_REACTION_ADD_REQUEST, ANNOUNCEMENTS_REACTION_ADD_FAIL, ANNOUNCEMENTS_REACTION_REMOVE_REQUEST, ANNOUNCEMENTS_REACTION_REMOVE_FAIL, + ANNOUNCEMENTS_TOGGLE_SHOW, } from '../actions/announcements'; -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, Set as ImmutableSet, fromJS } from 'immutable'; const initialState = ImmutableMap({ items: ImmutableList(), isLoading: false, + show: true, + unread: ImmutableSet(), }); const updateReaction = (state, id, name, updater) => state.update('items', list => list.map(announcement => { @@ -43,21 +45,35 @@ const addReaction = (state, id, name) => updateReaction(state, id, name, x => x. const removeReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', false).update('count', y => y - 1)); +const addUnread = (state, items) => { + if (state.get('show')) return state; + + const newIds = ImmutableSet(items.map(x => x.get('id'))); + const oldIds = ImmutableSet(state.get('items').map(x => x.get('id'))); + return state.update('unread', unread => unread.union(newIds.subtract(oldIds))); +}; + export default function announcementsReducer(state = initialState, action) { switch(action.type) { + case ANNOUNCEMENTS_TOGGLE_SHOW: + return state.withMutations(map => { + if (!map.get('show')) map.set('unread', ImmutableSet()); + map.set('show', !map.get('show')); + }); case ANNOUNCEMENTS_FETCH_REQUEST: return state.set('isLoading', true); case ANNOUNCEMENTS_FETCH_SUCCESS: return state.withMutations(map => { - map.set('items', fromJS(action.announcements)); + const items = fromJS(action.announcements); + map.set('unread', ImmutableSet()); + addUnread(map, items); + map.set('items', items); map.set('isLoading', false); }); case ANNOUNCEMENTS_FETCH_FAIL: return state.set('isLoading', false); case ANNOUNCEMENTS_UPDATE: - return state.update('items', list => list.unshift(fromJS(action.announcement)).sortBy(announcement => announcement.get('starts_at'))); - case ANNOUNCEMENTS_DISMISS: - return state.update('items', list => list.filterNot(announcement => announcement.get('id') === action.id)); + return addUnread(state, [fromJS(action.announcement)]).update('items', list => list.unshift(fromJS(action.announcement)).sortBy(announcement => announcement.get('starts_at'))); case ANNOUNCEMENTS_REACTION_UPDATE: return updateReactionCount(state, action.reaction); case ANNOUNCEMENTS_REACTION_ADD_REQUEST: diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1e1000ff3c9..e4fafc091a4 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -6631,7 +6631,7 @@ noscript { } .announcements { - background: lighten($ui-base-color, 4%); + background: lighten($ui-base-color, 8%); border-top: 1px solid $ui-base-color; font-size: 13px; display: flex; @@ -6672,12 +6672,6 @@ noscript { font-weight: 500; margin-bottom: 10px; } - - &__dismiss-icon { - position: absolute; - top: 12px; - right: 12px; - } } &__pagination {