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
This commit is contained in:
		
							parent
							
								
									ae2198bd95
								
							
						
					
					
						commit
						48c55b6392
					
				
					 6 changed files with 71 additions and 47 deletions
				
			
		|  | @ -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 }); | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -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} | ||||
|         /> | ||||
| 
 | ||||
|         <IconButton title={intl.formatMessage(messages.close)} icon='times' className='announcements__item__dismiss-icon' onClick={this.handleDismissClick} /> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | @ -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 { | |||
|         <img className='announcements__mastodon' alt='' draggable='false' src={mascot || elephantUIPlane} /> | ||||
| 
 | ||||
|         <div className='announcements__container'> | ||||
|           <ReactSwipeableViews index={index} onChangeIndex={this.handleChangeIndex}> | ||||
|           <ReactSwipeableViews animateHeight index={index} onChangeIndex={this.handleChangeIndex}> | ||||
|             {announcements.map(announcement => ( | ||||
|               <Announcement | ||||
|                 key={announcement.get('id')} | ||||
|                 announcement={announcement} | ||||
|                 emojiMap={this.props.emojiMap} | ||||
|                 dismissAnnouncement={this.props.dismissAnnouncement} | ||||
|                 addReaction={this.props.addReaction} | ||||
|                 removeReaction={this.props.removeReaction} | ||||
|                 intl={intl} | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { connect } from 'react-redux'; | ||||
| import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'mastodon/actions/announcements'; | ||||
| import { addReaction, removeReaction } from 'mastodon/actions/announcements'; | ||||
| import Announcements from '../components/announcements'; | ||||
| import { createSelector } from 'reselect'; | ||||
| import { Map as ImmutableMap } from 'immutable'; | ||||
|  | @ -12,8 +12,6 @@ const mapStateToProps = state => ({ | |||
| }); | ||||
| 
 | ||||
| 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)), | ||||
| }); | ||||
|  |  | |||
|  | @ -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 = ( | ||||
|         <button | ||||
|           className={classNames('column-header__button', { 'active': showAnnouncements })} | ||||
|           title={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)} | ||||
|           aria-label={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)} | ||||
|           aria-pressed={showAnnouncements ? 'true' : 'false'} | ||||
|           onClick={this.handleToggleAnnouncementsClick} | ||||
|         > | ||||
|           <IconWithBadge id='bullhorn' count={unreadAnnouncements} /> | ||||
|         </button> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> | ||||
|         <ColumnHeader | ||||
|  | @ -109,13 +142,14 @@ class HomeTimeline extends React.PureComponent { | |||
|           onClick={this.handleHeaderClick} | ||||
|           pinned={pinned} | ||||
|           multiColumn={multiColumn} | ||||
|           extraButton={announcementsButton} | ||||
|         > | ||||
|           <ColumnSettingsContainer /> | ||||
|         </ColumnHeader> | ||||
| 
 | ||||
|         {hasAnnouncements && showAnnouncements && <AnnouncementsContainer />} | ||||
| 
 | ||||
|         <StatusListContainer | ||||
|           prepend={<AnnouncementsContainer />} | ||||
|           alwaysPrepend | ||||
|           trackScroll={!pinned} | ||||
|           scrollKey={`home_timeline-${columnId}`} | ||||
|           onLoadMore={this.handleLoadMore} | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue