Add announcements (#12662)
* Add announcements Fix #11006 * Add reactions to announcements * Add admin UI for announcements * Add unit tests * Fix issues - Add `with_dismissed` param to announcements API - Fix end date not being formatted when time range is given - Fix announcement delete causing reactions to send streaming updates - Fix announcements container growing too wide and mascot too small - Fix `all_day` being settable when no time range is given - Change text "Update" to "Announcement" * Fix scheduler unpublishing announcements before they are due * Fix filter params not being passed to announcements filter
This commit is contained in:
		
							parent
							
								
									81cc86bb1f
								
							
						
					
					
						commit
						f52c988e12
					
				
					 65 changed files with 1779 additions and 22 deletions
				
			
		| 
						 | 
				
			
			@ -290,6 +290,7 @@ class EmojiPickerDropdown extends React.PureComponent {
 | 
			
		|||
    onPickEmoji: PropTypes.func.isRequired,
 | 
			
		||||
    onSkinTone: PropTypes.func.isRequired,
 | 
			
		||||
    skinTone: PropTypes.number.isRequired,
 | 
			
		||||
    button: PropTypes.node,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
| 
						 | 
				
			
			@ -350,18 +351,18 @@ class EmojiPickerDropdown extends React.PureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
 | 
			
		||||
    const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
 | 
			
		||||
    const title = intl.formatMessage(messages.emoji);
 | 
			
		||||
    const { active, loading, placement } = this.state;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
 | 
			
		||||
        <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
 | 
			
		||||
          <img
 | 
			
		||||
          {button || <img
 | 
			
		||||
            className={classNames('emojione', { 'pulse-loading': active && loading })}
 | 
			
		||||
            alt='🙂'
 | 
			
		||||
            src={`${assetHost}/emoji/1f602.svg`}
 | 
			
		||||
          />
 | 
			
		||||
          />}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <Overlay show={active} placement={placement} target={this.findTarget}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,395 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import ReactSwipeableViews from 'react-swipeable-views';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import IconButton from 'mastodon/components/icon_button';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage, FormattedDate, FormattedNumber } from 'react-intl';
 | 
			
		||||
import { autoPlayGif } from 'mastodon/initial_state';
 | 
			
		||||
import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
 | 
			
		||||
import { mascot } from 'mastodon/initial_state';
 | 
			
		||||
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  close: { id: 'lightbox.close', defaultMessage: 'Close' },
 | 
			
		||||
  previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
 | 
			
		||||
  next: { id: 'lightbox.next', defaultMessage: 'Next' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class Content extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    router: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    announcement: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  setRef = c => {
 | 
			
		||||
    this.node = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    this._updateLinks();
 | 
			
		||||
    this._updateEmojis();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate () {
 | 
			
		||||
    this._updateLinks();
 | 
			
		||||
    this._updateEmojis();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _updateEmojis () {
 | 
			
		||||
    const node = this.node;
 | 
			
		||||
 | 
			
		||||
    if (!node || autoPlayGif) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const emojis = node.querySelectorAll('.custom-emoji');
 | 
			
		||||
 | 
			
		||||
    for (var i = 0; i < emojis.length; i++) {
 | 
			
		||||
      let emoji = emojis[i];
 | 
			
		||||
 | 
			
		||||
      if (emoji.classList.contains('status-emoji')) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      emoji.classList.add('status-emoji');
 | 
			
		||||
 | 
			
		||||
      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
 | 
			
		||||
      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _updateLinks () {
 | 
			
		||||
    const node = this.node;
 | 
			
		||||
 | 
			
		||||
    if (!node) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const links = node.querySelectorAll('a');
 | 
			
		||||
 | 
			
		||||
    for (var i = 0; i < links.length; ++i) {
 | 
			
		||||
      let link = links[i];
 | 
			
		||||
 | 
			
		||||
      if (link.classList.contains('status-link')) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      link.classList.add('status-link');
 | 
			
		||||
 | 
			
		||||
      let mention = this.props.announcement.get('mentions').find(item => link.href === item.get('url'));
 | 
			
		||||
 | 
			
		||||
      if (mention) {
 | 
			
		||||
        link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
 | 
			
		||||
        link.setAttribute('title', mention.get('acct'));
 | 
			
		||||
      } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
 | 
			
		||||
        link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
 | 
			
		||||
      } else {
 | 
			
		||||
        link.setAttribute('title', link.href);
 | 
			
		||||
        link.classList.add('unhandled-link');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      link.setAttribute('target', '_blank');
 | 
			
		||||
      link.setAttribute('rel', 'noopener noreferrer');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onMentionClick = (mention, e) => {
 | 
			
		||||
    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      this.context.router.history.push(`/accounts/${mention.get('id')}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onHashtagClick = (hashtag, e) => {
 | 
			
		||||
    hashtag = hashtag.replace(/^#/, '');
 | 
			
		||||
 | 
			
		||||
    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      this.context.router.history.push(`/timelines/tag/${hashtag}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleEmojiMouseEnter = ({ target }) => {
 | 
			
		||||
    target.src = target.getAttribute('data-original');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleEmojiMouseLeave = ({ target }) => {
 | 
			
		||||
    target.src = target.getAttribute('data-static');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { announcement } = this.props;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className='announcements__item__content'
 | 
			
		||||
        ref={this.setRef}
 | 
			
		||||
        dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const assetHost = process.env.CDN_HOST || '';
 | 
			
		||||
 | 
			
		||||
class Emoji extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    emoji: PropTypes.string.isRequired,
 | 
			
		||||
    emojiMap: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    hovered: PropTypes.bool.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { emoji, emojiMap, hovered } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (unicodeMapping[emoji]) {
 | 
			
		||||
      const { filename, shortCode } = unicodeMapping[this.props.emoji];
 | 
			
		||||
      const title = shortCode ? `:${shortCode}:` : '';
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <img
 | 
			
		||||
          draggable='false'
 | 
			
		||||
          className='emojione'
 | 
			
		||||
          alt={emoji}
 | 
			
		||||
          title={title}
 | 
			
		||||
          src={`${assetHost}/emoji/${filename}.svg`}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else if (emojiMap.get(emoji)) {
 | 
			
		||||
      const filename  = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
 | 
			
		||||
      const shortCode = `:${emoji}:`;
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <img
 | 
			
		||||
          draggable='false'
 | 
			
		||||
          className='emojione custom-emoji'
 | 
			
		||||
          alt={shortCode}
 | 
			
		||||
          title={shortCode}
 | 
			
		||||
          src={filename}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Reaction extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    announcementId: PropTypes.string.isRequired,
 | 
			
		||||
    reaction: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    addReaction: PropTypes.func.isRequired,
 | 
			
		||||
    removeReaction: PropTypes.func.isRequired,
 | 
			
		||||
    emojiMap: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    hovered: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleClick = () => {
 | 
			
		||||
    const { reaction, announcementId, addReaction, removeReaction } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (reaction.get('me')) {
 | 
			
		||||
      removeReaction(announcementId, reaction.get('name'));
 | 
			
		||||
    } else {
 | 
			
		||||
      addReaction(announcementId, reaction.get('name'));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMouseEnter = () => this.setState({ hovered: true })
 | 
			
		||||
 | 
			
		||||
  handleMouseLeave = () => this.setState({ hovered: false })
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { reaction } = this.props;
 | 
			
		||||
 | 
			
		||||
    let shortCode = reaction.get('name');
 | 
			
		||||
 | 
			
		||||
    if (unicodeMapping[shortCode]) {
 | 
			
		||||
      shortCode = unicodeMapping[shortCode].shortCode;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`}>
 | 
			
		||||
        <span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} /></span>
 | 
			
		||||
        <span className='reactions-bar__item__count'><FormattedNumber value={reaction.get('count')} /></span>
 | 
			
		||||
      </button>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ReactionsBar extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    announcementId: PropTypes.string.isRequired,
 | 
			
		||||
    reactions: ImmutablePropTypes.list.isRequired,
 | 
			
		||||
    addReaction: PropTypes.func.isRequired,
 | 
			
		||||
    removeReaction: PropTypes.func.isRequired,
 | 
			
		||||
    emojiMap: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleEmojiPick = data => {
 | 
			
		||||
    const { addReaction, announcementId } = this.props;
 | 
			
		||||
    addReaction(announcementId, data.native.replace(/:/g, ''));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { reactions } = this.props;
 | 
			
		||||
    const visibleReactions = reactions.filter(x => x.get('count') > 0);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
 | 
			
		||||
        {visibleReactions.map(reaction => (
 | 
			
		||||
          <Reaction
 | 
			
		||||
            key={reaction.get('name')}
 | 
			
		||||
            reaction={reaction}
 | 
			
		||||
            announcementId={this.props.announcementId}
 | 
			
		||||
            addReaction={this.props.addReaction}
 | 
			
		||||
            removeReaction={this.props.removeReaction}
 | 
			
		||||
            emojiMap={this.props.emojiMap}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
 | 
			
		||||
        <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={<Icon id='plus' />} />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 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();
 | 
			
		||||
    const hasTimeRange = startsAt && endsAt;
 | 
			
		||||
    const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
 | 
			
		||||
    const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
 | 
			
		||||
    const skipTime = announcement.get('all_day');
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='announcements__item'>
 | 
			
		||||
        <strong className='announcements__item__range'>
 | 
			
		||||
          <FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
 | 
			
		||||
          {hasTimeRange && <span> · <FormattedDate value={startsAt} hour12={false} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} hour12={false} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
 | 
			
		||||
        </strong>
 | 
			
		||||
 | 
			
		||||
        <Content announcement={announcement} />
 | 
			
		||||
 | 
			
		||||
        <ReactionsBar
 | 
			
		||||
          reactions={announcement.get('reactions')}
 | 
			
		||||
          announcementId={announcement.get('id')}
 | 
			
		||||
          addReaction={this.props.addReaction}
 | 
			
		||||
          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>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default @injectIntl
 | 
			
		||||
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,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    index: 0,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    const { fetchAnnouncements } = this.props;
 | 
			
		||||
    fetchAnnouncements();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleChangeIndex = index => {
 | 
			
		||||
    this.setState({ index: index % this.props.announcements.size });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleNextClick = () => {
 | 
			
		||||
    this.setState({ index: (this.state.index + 1) % this.props.announcements.size });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handlePrevClick = () => {
 | 
			
		||||
    this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { announcements, intl } = this.props;
 | 
			
		||||
    const { index } = this.state;
 | 
			
		||||
 | 
			
		||||
    if (announcements.isEmpty()) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='announcements'>
 | 
			
		||||
        <img className='announcements__mastodon' alt='' draggable='false' src={mascot || elephantUIPlane} />
 | 
			
		||||
 | 
			
		||||
        <div className='announcements__container'>
 | 
			
		||||
          <ReactSwipeableViews 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}
 | 
			
		||||
              />
 | 
			
		||||
            ))}
 | 
			
		||||
          </ReactSwipeableViews>
 | 
			
		||||
 | 
			
		||||
          <div className='announcements__pagination'>
 | 
			
		||||
            <IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.previous)} icon='chevron-left' onClick={this.handlePrevClick} size={13} />
 | 
			
		||||
            <span>{index + 1} / {announcements.size}</span>
 | 
			
		||||
            <IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.next)} icon='chevron-right' onClick={this.handleNextClick} size={13} />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
import { connect } from 'react-redux';
 | 
			
		||||
import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'mastodon/actions/announcements';
 | 
			
		||||
import Announcements from '../components/announcements';
 | 
			
		||||
import { createSelector } from 'reselect';
 | 
			
		||||
import { Map as ImmutableMap } from 'immutable';
 | 
			
		||||
 | 
			
		||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  announcements: state.getIn(['announcements', 'items']),
 | 
			
		||||
  emojiMap: customEmojiMap(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)),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps, mapDispatchToProps)(Announcements);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { connect } from 'react-redux';
 | 
			
		||||
import { fetchTrends } from '../../../actions/trends';
 | 
			
		||||
import { fetchTrends } from 'mastodon/actions/trends';
 | 
			
		||||
import Trends from '../components/trends';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ 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 AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  title: { id: 'column.home', defaultMessage: 'Home' },
 | 
			
		||||
| 
						 | 
				
			
			@ -113,6 +114,8 @@ class HomeTimeline extends React.PureComponent {
 | 
			
		|||
        </ColumnHeader>
 | 
			
		||||
 | 
			
		||||
        <StatusListContainer
 | 
			
		||||
          prepend={<AnnouncementsContainer />}
 | 
			
		||||
          alwaysPrepend
 | 
			
		||||
          trackScroll={!pinned}
 | 
			
		||||
          scrollKey={`home_timeline-${columnId}`}
 | 
			
		||||
          onLoadMore={this.handleLoadMore}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -211,7 +211,6 @@ class MediaModal extends ImmutablePureComponent {
 | 
			
		|||
            style={swipeableViewsStyle}
 | 
			
		||||
            containerStyle={containerStyle}
 | 
			
		||||
            onChangeIndex={this.handleSwipe}
 | 
			
		||||
            onSwitching={this.handleSwitching}
 | 
			
		||||
            index={index}
 | 
			
		||||
          >
 | 
			
		||||
            {content}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue