~cytrogen/masto-fe

7e25fd9b0c68a9e96d5842e5af59c630682e21f9 — Christian Schmidt 2 years ago 93c7144
[Glitch] Translate CW, poll options and media descriptions

Port 69057467cba138d2c9e459f565e88ea5979f61b0 to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
M app/javascript/flavours/glitch/actions/importer/normalizer.js => app/javascript/flavours/glitch/actions/importer/normalizer.js +32 -6
@@ 6,7 6,7 @@ import { unescapeHTML } from 'flavours/glitch/utils/html';

const domParser = new DOMParser();

const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
  obj[`:${emoji.shortcode}:`] = emoji;
  return obj;
}, {});


@@ 20,7 20,7 @@ export function searchTextFromRawStatus (status) {
export function normalizeAccount(account) {
  account = { ...account };

  const emojiMap = makeEmojiMap(account);
  const emojiMap = makeEmojiMap(account.emojis);
  const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;

  account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);


@@ 78,7 78,7 @@ export function normalizeStatus(status, normalOldStatus, settings) {
  } else {
    const spoilerText   = normalStatus.spoiler_text || '';
    const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
    const emojiMap      = makeEmojiMap(normalStatus);
    const emojiMap      = makeEmojiMap(normalStatus.emojis);

    normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
    normalStatus.contentHtml  = emojify(normalStatus.content, emojiMap);


@@ 89,22 89,48 @@ export function normalizeStatus(status, normalOldStatus, settings) {
  return normalStatus;
}

export function normalizeStatusTranslation(translation, status) {
  const emojiMap = makeEmojiMap(status.get('emojis').toJS());

  const normalTranslation = {
    detected_source_language: translation.detected_source_language,
    language: translation.language,
    provider: translation.provider,
    contentHtml: emojify(translation.content, emojiMap),
    spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
    spoiler_text: translation.spoiler_text,
  };

  return normalTranslation;
}

export function normalizePoll(poll) {
  const normalPoll = { ...poll };
  const emojiMap = makeEmojiMap(normalPoll);
  const emojiMap = makeEmojiMap(poll.emojis);

  normalPoll.options = poll.options.map((option, index) => ({
    ...option,
    voted: poll.own_votes && poll.own_votes.includes(index),
    title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
    titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
  }));

  return normalPoll;
}

export function normalizePollOptionTranslation(translation, poll) {
  const emojiMap = makeEmojiMap(poll.get('emojis').toJS());

  const normalTranslation = {
    ...translation,
    titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
  };

  return normalTranslation;
}

export function normalizeAnnouncement(announcement) {
  const normalAnnouncement = { ...announcement };
  const emojiMap = makeEmojiMap(normalAnnouncement);
  const emojiMap = makeEmojiMap.emojis(normalAnnouncement);

  normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);


M app/javascript/flavours/glitch/actions/statuses.js => app/javascript/flavours/glitch/actions/statuses.js +2 -1
@@ 344,7 344,8 @@ export const translateStatusFail = (id, error) => ({
  error,
});

export const undoStatusTranslation = id => ({
export const undoStatusTranslation = (id, pollId) => ({
  type: STATUS_TRANSLATE_UNDO,
  id,
  pollId,
});

M app/javascript/flavours/glitch/components/media_attachments.jsx => app/javascript/flavours/glitch/components/media_attachments.jsx +9 -6
@@ 52,8 52,9 @@ export default class MediaAttachments extends ImmutablePureComponent {
  };

  render () {
    const { status, lang, width, height, revealed } = this.props;
    const { status, width, height, revealed } = this.props;
    const mediaAttachments = status.get('media_attachments');
    const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang;

    if (mediaAttachments.size === 0) {
      return null;


@@ 61,14 62,15 @@ export default class MediaAttachments extends ImmutablePureComponent {

    if (mediaAttachments.getIn([0, 'type']) === 'audio') {
      const audio = mediaAttachments.get(0);
      const description = audio.getIn(['translation', 'description']) || audio.get('description');

      return (
        <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
          {Component => (
            <Component
              src={audio.get('url')}
              alt={audio.get('description')}
              lang={lang || status.get('language')}
              alt={description}
              lang={language}
              width={width}
              height={height}
              poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}


@@ 82,6 84,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
      );
    } else if (mediaAttachments.getIn([0, 'type']) === 'video') {
      const video = mediaAttachments.get(0);
      const description = video.getIn(['translation', 'description']) || video.get('description');

      return (
        <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >


@@ 91,8 94,8 @@ export default class MediaAttachments extends ImmutablePureComponent {
              frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
              blurhash={video.get('blurhash')}
              src={video.get('url')}
              alt={video.get('description')}
              lang={lang || status.get('language')}
              alt={description}
              lang={language}
              width={width}
              height={height}
              inline


@@ 109,7 112,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
          {Component => (
            <Component
              media={mediaAttachments}
              lang={lang || status.get('language')}
              lang={language}
              sensitive={status.get('sensitive')}
              defaultWidth={width}
              revealed={revealed}

M app/javascript/flavours/glitch/components/media_gallery.jsx => app/javascript/flavours/glitch/components/media_gallery.jsx +7 -5
@@ 124,10 124,12 @@ class Item extends PureComponent {
      badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
    }

    const description = attachment.getIn(['translation', 'description']) || attachment.get('description');

    if (attachment.get('type') === 'unknown') {
      return (
        <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
            <Blurhash
              hash={attachment.get('blurhash')}
              className='media-gallery__preview'


@@ 166,8 168,8 @@ class Item extends PureComponent {
            src={previewUrl}
            srcSet={srcSet}
            sizes={sizes}
            alt={attachment.get('description')}
            title={attachment.get('description')}
            alt={description}
            title={description}
            lang={lang}
            style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
            onLoad={this.handleImageLoad}


@@ 183,8 185,8 @@ class Item extends PureComponent {
        <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
          <video
            className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
            aria-label={attachment.get('description')}
            title={attachment.get('description')}
            aria-label={description}
            title={description}
            lang={lang}
            role='application'
            src={attachment.get('url')}

M app/javascript/flavours/glitch/components/poll.jsx => app/javascript/flavours/glitch/components/poll.jsx +7 -5
@@ 139,10 139,12 @@ class Poll extends ImmutablePureComponent {
    const active          = !!this.state.selected[`${optionIndex}`];
    const voted           = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));

    let titleEmojified = option.get('title_emojified');
    if (!titleEmojified) {
    const title = option.getIn(['translation', 'title']) || option.get('title');
    let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');

    if (!titleHtml) {
      const emojiMap = makeEmojiMap(poll);
      titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
      titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
    }

    return (


@@ 164,7 166,7 @@ class Poll extends ImmutablePureComponent {
              role={poll.get('multiple') ? 'checkbox' : 'radio'}
              onKeyPress={this.handleOptionKeyPress}
              aria-checked={active}
              aria-label={option.get('title')}
              aria-label={title}
              lang={lang}
              data-index={optionIndex}
            />


@@ 183,7 185,7 @@ class Poll extends ImmutablePureComponent {
          <span
            className='poll__option__text translate'
            lang={lang}
            dangerouslySetInnerHTML={{ __html: titleEmojified }}
            dangerouslySetInnerHTML={{ __html: titleHtml }}
          />

          {!!voted && <span className='poll__voted'>

M app/javascript/flavours/glitch/components/status.jsx => app/javascript/flavours/glitch/components/status.jsx +25 -11
@@ 26,12 26,18 @@ import StatusHeader from './status_header';
import StatusIcons from './status_icons';
import StatusPrepend from './status_prepend';

const domParser = new DOMParser();

export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => {
  const displayName = status.getIn(['account', 'display_name']);

  const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
  const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
  const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;

  const values = [
    displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
    status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
    spoilerText && !expanded ? spoilerText : contentText,
    intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
    status.getIn(['account', 'acct']),
  ];


@@ 391,12 397,14 @@ class Status extends ImmutablePureComponent {

  handleOpenVideo = (options) => {
    const { status } = this.props;
    this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options);
    const lang = status.getIn(['translation', 'language']) || status.get('language');
    this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options);
  };

  handleOpenMedia = (media, index) => {
    const { status } = this.props;
    this.props.onOpenMedia(status.get('id'), media, index, status.get('language'));
    const lang = status.getIn(['translation', 'language']) || status.get('language');
    this.props.onOpenMedia(status.get('id'), media, index, lang);
  };

  handleHotkeyOpenMedia = e => {


@@ 406,10 414,11 @@ class Status extends ImmutablePureComponent {
    e.preventDefault();

    if (status.get('media_attachments').size > 0) {
      const lang = status.getIn(['translation', 'language']) || status.get('language');
      if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
        onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 });
        onOpenVideo(statusId, status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
      } else {
        onOpenMedia(statusId, status.get('media_attachments'), 0);
        onOpenMedia(statusId, status.get('media_attachments'), 0, lang);
      }
    }
  };


@@ 625,6 634,8 @@ class Status extends ImmutablePureComponent {
      media.push(<PictureInPicturePlaceholder />);
      mediaIcons.push('video-camera');
    } else if (attachments.size > 0) {
      const language = status.getIn(['translation', 'language']) || status.get('language');

      if (muted || attachments.some(item => item.get('type') === 'unknown')) {
        media.push(
          <AttachmentList


@@ 634,14 645,15 @@ class Status extends ImmutablePureComponent {
        );
      } else if (attachments.getIn([0, 'type']) === 'audio') {
        const attachment = status.getIn(['media_attachments', 0]);
        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');

        media.push(
          <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
            {Component => (
              <Component
                src={attachment.get('url')}
                alt={attachment.get('description')}
                lang={status.get('language')}
                alt={description}
                lang={language}
                poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
                backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
                foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}


@@ 662,6 674,7 @@ class Status extends ImmutablePureComponent {
        mediaIcons.push('music');
      } else if (attachments.getIn([0, 'type']) === 'video') {
        const attachment = status.getIn(['media_attachments', 0]);
        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');

        media.push(
          <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >


@@ 670,8 683,8 @@ class Status extends ImmutablePureComponent {
              frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
              blurhash={attachment.get('blurhash')}
              src={attachment.get('url')}
              alt={attachment.get('description')}
              lang={status.get('language')}
              alt={description}
              lang={language}
              inline
              sensitive={status.get('sensitive')}
              letterbox={settings.getIn(['media', 'letterbox'])}


@@ 691,7 704,7 @@ class Status extends ImmutablePureComponent {
            {Component => (
              <Component
                media={attachments}
                lang={status.get('language')}
                lang={language}
                sensitive={status.get('sensitive')}
                letterbox={settings.getIn(['media', 'letterbox'])}
                fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}


@@ 724,7 737,8 @@ class Status extends ImmutablePureComponent {
    }

    if (status.get('poll')) {
      contentMedia.push(<PollContainer pollId={status.get('poll')} lang={status.get('language')} />);
      const language = status.getIn(['translation', 'language']) || status.get('language');
      contentMedia.push(<PollContainer pollId={status.get('poll')} lang={language} />);
      contentMediaIcons.push('tasks');
    }


M app/javascript/flavours/glitch/components/status_content.jsx => app/javascript/flavours/glitch/components/status_content.jsx +8 -8
@@ 327,11 327,11 @@ class StatusContent extends PureComponent {
    const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
    const contentLocale = intl.locale.replace(/[_-].*/, '');
    const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
    const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && targetLanguages?.includes(contentLocale);
    const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);

    const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
    const spoilerContent = { __html: status.get('spoilerHtml') };
    const lang = status.get('translation') ? intl.locale : status.get('language');
    const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
    const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
    const language = status.getIn(['translation', 'language']) || status.get('language');
    const classNames = classnames('status__content', {
      'status__content--with-action': parseClick && !disabled,
      'status__content--with-spoiler': status.get('spoiler_text').length > 0,


@@ 396,7 396,7 @@ class StatusContent extends PureComponent {
          <p
            style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
          >
            <span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={lang} />
            <span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
            {' '}
            <button type='button' className='status__content__spoiler-link' onClick={this.handleSpoilerClick} aria-expanded={!hidden}>
              {toggleText}


@@ 414,7 414,7 @@ class StatusContent extends PureComponent {
              className='status__content__text translate'
              onMouseEnter={this.handleMouseEnter}
              onMouseLeave={this.handleMouseLeave}
              lang={lang}
              lang={language}
            />
            {!hidden && translateButton}
            {media}


@@ 439,7 439,7 @@ class StatusContent extends PureComponent {
            tabIndex={0}
            onMouseEnter={this.handleMouseEnter}
            onMouseLeave={this.handleMouseLeave}
            lang={lang}
            lang={language}
          />
          {translateButton}
          {media}


@@ 460,7 460,7 @@ class StatusContent extends PureComponent {
            tabIndex={0}
            onMouseEnter={this.handleMouseEnter}
            onMouseLeave={this.handleMouseLeave}
            lang={lang}
            lang={language}
          />
          {translateButton}
          {media}

M app/javascript/flavours/glitch/containers/status_container.js => app/javascript/flavours/glitch/containers/status_container.js +1 -1
@@ 218,7 218,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({

  onTranslate (status) {
    if (status.get('translation')) {
      dispatch(undoStatusTranslation(status.get('id')));
      dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
    } else {
      dispatch(translateStatus(status.get('id')));
    }

M app/javascript/flavours/glitch/features/status/components/detailed_status.jsx => app/javascript/flavours/glitch/features/status/components/detailed_status.jsx +10 -5
@@ 158,6 158,8 @@ class DetailedStatus extends ImmutablePureComponent {
      outerStyle.height = `${this.state.height}px`;
    }

    const language = status.getIn(['translation', 'language']) || status.get('language');

    if (pictureInPicture.get('inUse')) {
      media.push(<PictureInPicturePlaceholder />);
      mediaIcons.push('video-camera');


@@ 166,12 168,13 @@ class DetailedStatus extends ImmutablePureComponent {
        media.push(<AttachmentList media={status.get('media_attachments')} />);
      } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
        const attachment = status.getIn(['media_attachments', 0]);
        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');

        media.push(
          <Audio
            src={attachment.get('url')}
            alt={attachment.get('description')}
            lang={status.get('language')}
            alt={description}
            lang={language}
            duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
            poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
            backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}


@@ 187,14 190,16 @@ class DetailedStatus extends ImmutablePureComponent {
        mediaIcons.push('music');
      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
        const attachment = status.getIn(['media_attachments', 0]);
        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');

        media.push(
          <Video
            preview={attachment.get('preview_url')}
            frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
            blurhash={attachment.get('blurhash')}
            src={attachment.get('url')}
            alt={attachment.get('description')}
            lang={status.get('language')}
            alt={description}
            lang={language}
            inline
            sensitive={status.get('sensitive')}
            letterbox={settings.getIn(['media', 'letterbox'])}


@@ 213,7 218,7 @@ class DetailedStatus extends ImmutablePureComponent {
            standalone
            sensitive={status.get('sensitive')}
            media={status.get('media_attachments')}
            lang={status.get('language')}
            lang={language}
            letterbox={settings.getIn(['media', 'letterbox'])}
            fullwidth={settings.getIn(['media', 'fullwidth'])}
            hidden={!expanded}

M app/javascript/flavours/glitch/features/status/index.jsx => app/javascript/flavours/glitch/features/status/index.jsx +1 -1
@@ 481,7 481,7 @@ class Status extends ImmutablePureComponent {
    const { dispatch } = this.props;

    if (status.get('translation')) {
      dispatch(undoStatusTranslation(status.get('id')));
      dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
    } else {
      dispatch(translateStatus(status.get('id')));
    }

M app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx => app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx +7 -5
@@ 8,7 8,7 @@ import Audio from 'flavours/glitch/features/audio';
import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';

const mapStateToProps = (state, { statusId }) => ({
  language: state.getIn(['statuses', statusId, 'language']),
  status: state.getIn(['statuses', statusId]),
  accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
});



@@ 17,7 17,7 @@ class AudioModal extends ImmutablePureComponent {
  static propTypes = {
    media: ImmutablePropTypes.map.isRequired,
    statusId: PropTypes.string.isRequired,
    language: PropTypes.string,
    status: ImmutablePropTypes.map.isRequired,
    accountStaticAvatar: PropTypes.string.isRequired,
    options: PropTypes.shape({
      autoPlay: PropTypes.bool,


@@ 31,15 31,17 @@ class AudioModal extends ImmutablePureComponent {
  };

  render () {
    const { media, language, accountStaticAvatar, statusId, onClose } = this.props;
    const { media, status, accountStaticAvatar, onClose } = this.props;
    const options = this.props.options || {};
    const language = status.getIn(['translation', 'language']) || status.get('language');
    const description = media.getIn(['translation', 'description']) || media.get('description');

    return (
      <div className='modal-root__modal audio-modal'>
        <div className='audio-modal__container'>
          <Audio
            src={media.get('url')}
            alt={media.get('description')}
            alt={description}
            lang={language}
            duration={media.getIn(['meta', 'original', 'duration'], 0)}
            height={150}


@@ 52,7 54,7 @@ class AudioModal extends ImmutablePureComponent {
        </div>

        <div className='media-modal__overlay'>
          {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
          {status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
        </div>
      </div>
    );

M app/javascript/flavours/glitch/features/ui/components/media_modal.jsx => app/javascript/flavours/glitch/features/ui/components/media_modal.jsx +4 -3
@@ 147,6 147,7 @@ class MediaModal extends ImmutablePureComponent {
    const content = media.map((image) => {
      const width  = image.getIn(['meta', 'original', 'width']) || null;
      const height = image.getIn(['meta', 'original', 'height']) || null;
      const description = image.getIn(['translation', 'description']) || image.get('description');

      if (image.get('type') === 'image') {
        return (


@@ 155,7 156,7 @@ class MediaModal extends ImmutablePureComponent {
            src={image.get('url')}
            width={width}
            height={height}
            alt={image.get('description')}
            alt={description}
            lang={lang}
            key={image.get('url')}
            onClick={this.toggleNavigation}


@@ 178,7 179,7 @@ class MediaModal extends ImmutablePureComponent {
            volume={volume || 1}
            onCloseVideo={onClose}
            detailed
            alt={image.get('description')}
            alt={description}
            lang={lang}
            key={image.get('url')}
          />


@@ 190,7 191,7 @@ class MediaModal extends ImmutablePureComponent {
            width={width}
            height={height}
            key={image.get('url')}
            alt={image.get('description')}
            alt={description}
            lang={lang}
            onClick={this.toggleNavigation}
          />

M app/javascript/flavours/glitch/features/ui/components/video_modal.jsx => app/javascript/flavours/glitch/features/ui/components/video_modal.jsx +7 -5
@@ 9,7 9,7 @@ import Footer from 'flavours/glitch/features/picture_in_picture/components/foote
import Video from 'flavours/glitch/features/video';

const mapStateToProps = (state, { statusId }) => ({
  language: state.getIn(['statuses', statusId, 'language']),
  status: state.getIn(['statuses', statusId]),
});

class VideoModal extends ImmutablePureComponent {


@@ 17,7 17,7 @@ class VideoModal extends ImmutablePureComponent {
  static propTypes = {
    media: ImmutablePropTypes.map.isRequired,
    statusId: PropTypes.string,
    language: PropTypes.string,
    status: ImmutablePropTypes.map,
    options: PropTypes.shape({
      startTime: PropTypes.number,
      autoPlay: PropTypes.bool,


@@ 38,8 38,10 @@ class VideoModal extends ImmutablePureComponent {
  }

  render () {
    const { media, statusId, language, onClose } = this.props;
    const { media, status, onClose } = this.props;
    const options = this.props.options || {};
    const language = status.getIn(['translation', 'language']) || status.get('language');
    const description = media.getIn(['translation', 'description']) || media.get('description');

    return (
      <div className='modal-root__modal video-modal'>


@@ 55,13 57,13 @@ class VideoModal extends ImmutablePureComponent {
            onCloseVideo={onClose}
            autoFocus
            detailed
            alt={media.get('description')}
            alt={description}
            lang={language}
          />
        </div>

        <div className='media-modal__overlay'>
          {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
          {status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
        </div>
      </div>
    );

M app/javascript/flavours/glitch/reducers/polls.js => app/javascript/flavours/glitch/reducers/polls.js +29 -0
@@ 2,14 2,43 @@ import { Map as ImmutableMap, fromJS } from 'immutable';

import { POLLS_IMPORT } from 'flavours/glitch/actions/importer';

import { normalizePollOptionTranslation } from '../actions/importer/normalizer';
import { STATUS_TRANSLATE_SUCCESS, STATUS_TRANSLATE_UNDO } from '../actions/statuses';

const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll))));

const statusTranslateSuccess = (state, pollTranslation) => {
  return state.withMutations(map => {
    if (pollTranslation) {
      const poll = state.get(pollTranslation.id);

      pollTranslation.options.forEach((item, index) => {
        map.setIn([pollTranslation.id, 'options', index, 'translation'], fromJS(normalizePollOptionTranslation(item, poll)));
      });
    }
  });
};

const statusTranslateUndo = (state, id) => {
  return state.withMutations(map => {
    const options = map.getIn([id, 'options']);

    if (options) {
      options.forEach((item, index) => map.deleteIn([id, 'options', index, 'translation']));
    }
  });
};

const initialState = ImmutableMap();

export default function polls(state = initialState, action) {
  switch(action.type) {
  case POLLS_IMPORT:
    return importPolls(state, action.polls);
  case STATUS_TRANSLATE_SUCCESS:
    return statusTranslateSuccess(state, action.translation.poll);
  case STATUS_TRANSLATE_UNDO:
    return statusTranslateUndo(state, action.pollId);
  default:
    return state;
  }

M app/javascript/flavours/glitch/reducers/statuses.js => app/javascript/flavours/glitch/reducers/statuses.js +24 -2
@@ 25,6 25,7 @@ import {
} from 'flavours/glitch/actions/timelines';

import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { normalizeStatusTranslation } from '../actions/importer/normalizer';

const importStatus = (state, status) => state.set(status.id, fromJS(status));



@@ 39,6 40,27 @@ const deleteStatus = (state, id, references) => {
  return state.delete(id);
};

const statusTranslateSuccess = (state, id, translation) => {
  return state.withMutations(map => {
    map.setIn([id, 'translation'], fromJS(normalizeStatusTranslation(translation, map.get(id))));

    const list = map.getIn([id, 'media_attachments']);
    if (translation.media_attachments && list) {
      translation.media_attachments.forEach(item => {
        const index = list.findIndex(i => i.get('id') === item.id);
        map.setIn([id, 'media_attachments', index, 'translation'], fromJS({ description: item.description }));
      });
    }
  });
};

const statusTranslateUndo = (state, id) => {
  return state.withMutations(map => {
    map.deleteIn([id, 'translation']);
    map.getIn([id, 'media_attachments']).forEach((item, index) => map.deleteIn([id, 'media_attachments', index, 'translation']));
  });
};

const initialState = ImmutableMap();

export default function statuses(state = initialState, action) {


@@ 90,9 112,9 @@ export default function statuses(state = initialState, action) {
  case TIMELINE_DELETE:
    return deleteStatus(state, action.id, action.references);
  case STATUS_TRANSLATE_SUCCESS:
    return state.setIn([action.id, 'translation'], fromJS(action.translation));
    return statusTranslateSuccess(state, action.id, action.translation);
  case STATUS_TRANSLATE_UNDO:
    return state.deleteIn([action.id, 'translation']);
    return statusTranslateUndo(state, action.id);
  default:
    return state;
  }