about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorThibG <thib@sitedethib.com>2020-01-25 16:35:33 +0100
committerEugen Rochko <eugen@zeonfederated.com>2020-01-25 16:35:33 +0100
commit48c55b6392661cde8e28cf076c3d132c22d17a0f (patch)
tree798797f644368abc06e1086770ceb88db26df720 /app/javascript
parentae2198bd955530c61dd1f4cd99f23c7a0c069b6e (diff)
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
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/mastodon/actions/announcements.js18
-rw-r--r--app/javascript/mastodon/features/getting_started/components/announcements.js20
-rw-r--r--app/javascript/mastodon/features/getting_started/containers/announcements_container.js4
-rw-r--r--app/javascript/mastodon/features/home_timeline/index.js40
-rw-r--r--app/javascript/mastodon/reducers/announcements.js28
-rw-r--r--app/javascript/styles/mastodon/components.scss8
6 files changed, 71 insertions, 47 deletions
diff --git a/app/javascript/mastodon/actions/announcements.js b/app/javascript/mastodon/actions/announcements.js
index c65bc052e..64bf5ef91 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 975db0265..8ff1b0b4e 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}
         />
-
-        <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}
diff --git a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js
index b10d1d4ce..8c3fc2e6b 100644
--- a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js
+++ b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js
@@ -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)),
 });
diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js
index b7f9d5095..c7de8c9cb 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 = (
+        <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}
diff --git a/app/javascript/mastodon/reducers/announcements.js b/app/javascript/mastodon/reducers/announcements.js
index aa674e516..1cfb598fb 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 1e1000ff3..e4fafc091 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 {