Merge pull request #1072 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						1b0ce85e3d
					
				
					 47 changed files with 743 additions and 164 deletions
				
			
		
							
								
								
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -3,6 +3,39 @@ Changelog
 | 
			
		|||
 | 
			
		||||
All notable changes to this project will be documented in this file.
 | 
			
		||||
 | 
			
		||||
## [2.8.4] - 2019-05-24
 | 
			
		||||
### Fixed
 | 
			
		||||
 | 
			
		||||
- Fix delivery not retrying on some inbox errors that should be retriable ([ThibG](https://github.com/tootsuite/mastodon/pull/10812))
 | 
			
		||||
- Fix unnecessary 5 minute cooldowns on signature verifications in some cases ([ThibG](https://github.com/tootsuite/mastodon/pull/10813))
 | 
			
		||||
- Fix possible race condition when processing statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10815))
 | 
			
		||||
 | 
			
		||||
### Security
 | 
			
		||||
 | 
			
		||||
- Require specific OAuth scopes for specific endpoints of the streaming API, instead of merely requiring a token for all endpoints, and allow using WebSockets protocol negotiation to specify the access token instead of using a query string ([ThibG](https://github.com/tootsuite/mastodon/pull/10818))
 | 
			
		||||
 | 
			
		||||
## [2.8.3] - 2019-05-19
 | 
			
		||||
### Added
 | 
			
		||||
 | 
			
		||||
- Add `og:image:alt` OpenGraph tag ([BenLubar](https://github.com/tootsuite/mastodon/pull/10779))
 | 
			
		||||
- Add clickable area below avatar in statuses in web UI ([Dar13](https://github.com/tootsuite/mastodon/pull/10766))
 | 
			
		||||
- Add crossed-out eye icon on account gallery in web UI ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10715))
 | 
			
		||||
- Add media description tooltip to thumbnails in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10713))
 | 
			
		||||
 | 
			
		||||
### Changed
 | 
			
		||||
 | 
			
		||||
- Change "mark as sensitive" button into a checkbox for clarity ([ThibG](https://github.com/tootsuite/mastodon/pull/10748))
 | 
			
		||||
 | 
			
		||||
### Fixed
 | 
			
		||||
 | 
			
		||||
- Fix bug allowing users to publicly boost their private statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10775), [ThibG](https://github.com/tootsuite/mastodon/pull/10783))
 | 
			
		||||
- Fix performance in formatter by a little ([ThibG](https://github.com/tootsuite/mastodon/pull/10765))
 | 
			
		||||
- Fix some colors in the light theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10754))
 | 
			
		||||
- Fix some colors of the high contrast theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10711))
 | 
			
		||||
- Fix ambivalent active state of poll refresh button in web UI ([MaciekBaron](https://github.com/tootsuite/mastodon/pull/10720))
 | 
			
		||||
- Fix duplicate posting being possible from web UI ([hinaloe](https://github.com/tootsuite/mastodon/pull/10785))
 | 
			
		||||
- Fix "invited by" not showing up in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10791))
 | 
			
		||||
 | 
			
		||||
## [2.8.2] - 2019-05-05
 | 
			
		||||
### Added
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,7 +86,7 @@ RUN apt update && \
 | 
			
		|||
	useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
 | 
			
		||||
	echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
 | 
			
		||||
 | 
			
		||||
# Install masto runtime deps
 | 
			
		||||
# Install mastodon runtime deps
 | 
			
		||||
RUN apt -y --no-install-recommends install \
 | 
			
		||||
	  libssl1.1 libpq5 imagemagick ffmpeg \
 | 
			
		||||
	  libicu60 libprotobuf10 libidn11 libyaml-0-2 \
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +95,7 @@ RUN apt -y --no-install-recommends install \
 | 
			
		|||
	ln -s /opt/mastodon /mastodon && \
 | 
			
		||||
	gem install bundler && \
 | 
			
		||||
	rm -rf /var/cache && \
 | 
			
		||||
	rm -rf /var/lib/apt
 | 
			
		||||
	rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Add tini
 | 
			
		||||
ENV TINI_VERSION="0.18.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -104,11 +104,11 @@ ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tin
 | 
			
		|||
RUN echo "$TINI_SUM tini" | sha256sum -c -
 | 
			
		||||
RUN chmod +x /tini
 | 
			
		||||
 | 
			
		||||
# Copy over masto source, and dependencies from building, and set permissions
 | 
			
		||||
# Copy over mastodon source, and dependencies from building, and set permissions
 | 
			
		||||
COPY --chown=mastodon:mastodon . /opt/mastodon
 | 
			
		||||
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
 | 
			
		||||
 | 
			
		||||
# Run masto services in prod mode
 | 
			
		||||
# Run mastodon services in prod mode
 | 
			
		||||
ENV RAILS_ENV="production"
 | 
			
		||||
ENV NODE_ENV="production"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,6 +46,7 @@ class Settings::PreferencesController < Settings::BaseController
 | 
			
		|||
      :setting_hide_followers_count,
 | 
			
		||||
      :setting_aggregate_reblogs,
 | 
			
		||||
      :setting_show_application,
 | 
			
		||||
      :setting_advanced_layout,
 | 
			
		||||
      :setting_default_content_type,
 | 
			
		||||
      notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
 | 
			
		||||
      interactions: %i(must_be_follower must_be_following)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -257,7 +257,6 @@ export default class MediaGallery extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    sensitive: PropTypes.bool,
 | 
			
		||||
    revealed: PropTypes.bool,
 | 
			
		||||
    standalone: PropTypes.bool,
 | 
			
		||||
    letterbox: PropTypes.bool,
 | 
			
		||||
    fullwidth: PropTypes.bool,
 | 
			
		||||
| 
						 | 
				
			
			@ -268,6 +267,8 @@ export default class MediaGallery extends React.PureComponent {
 | 
			
		|||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    defaultWidth: PropTypes.number,
 | 
			
		||||
    cacheWidth: PropTypes.func,
 | 
			
		||||
    visible: PropTypes.bool,
 | 
			
		||||
    onToggleVisibility: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
| 
						 | 
				
			
			@ -275,13 +276,15 @@ export default class MediaGallery extends React.PureComponent {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    visible: this.props.revealed === undefined ? (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all') : this.props.revealed,
 | 
			
		||||
    visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
 | 
			
		||||
    width: this.props.defaultWidth,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentWillReceiveProps (nextProps) {
 | 
			
		||||
    if (!is(nextProps.media, this.props.media) || nextProps.revealed === true) {
 | 
			
		||||
      this.setState({ visible: nextProps.revealed === undefined ? (displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all') : nextProps.revealed });
 | 
			
		||||
    if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
 | 
			
		||||
      this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
 | 
			
		||||
    } else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
 | 
			
		||||
      this.setState({ visible: nextProps.visible });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -294,8 +297,12 @@ export default class MediaGallery extends React.PureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  handleOpen = () => {
 | 
			
		||||
    if (this.props.onToggleVisibility) {
 | 
			
		||||
      this.props.onToggleVisibility();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.setState({ visible: !this.state.visible });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClick = (index) => {
 | 
			
		||||
    this.props.onOpenMedia(this.props.media, index);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ import NotificationOverlayContainer from 'flavours/glitch/features/notifications
 | 
			
		|||
import classNames from 'classnames';
 | 
			
		||||
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
 | 
			
		||||
import PollContainer from 'flavours/glitch/containers/poll_container';
 | 
			
		||||
import { displayMedia } from 'flavours/glitch/util/initial_state';
 | 
			
		||||
 | 
			
		||||
// We use the component (and not the container) since we do not want
 | 
			
		||||
// to use the progress bar to show download progress
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +39,22 @@ export const textForScreenReader = (intl, status, rebloggedByText = false, expan
 | 
			
		|||
  return values.join(', ');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const defaultMediaVisibility = (status, settings) => {
 | 
			
		||||
  if (!status) {
 | 
			
		||||
    return undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
 | 
			
		||||
    status = status.get('reblog');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text')) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@injectIntl
 | 
			
		||||
export default class Status extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +99,9 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
    isCollapsed: false,
 | 
			
		||||
    autoCollapsed: false,
 | 
			
		||||
    isExpanded: undefined,
 | 
			
		||||
    showMedia: undefined,
 | 
			
		||||
    statusId: undefined,
 | 
			
		||||
    revealBehindCW: undefined,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Avoid checking props that are functions (and whose equality will always
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +123,7 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
  updateOnStates = [
 | 
			
		||||
    'isExpanded',
 | 
			
		||||
    'isCollapsed',
 | 
			
		||||
    'showMedia',
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  //  If our settings have changed to disable collapsed statuses, then we
 | 
			
		||||
| 
						 | 
				
			
			@ -160,6 +181,20 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
 | 
			
		||||
      update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
 | 
			
		||||
      update.statusId = nextProps.status.get('id');
 | 
			
		||||
      updated = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) {
 | 
			
		||||
      update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
 | 
			
		||||
      if (update.revealBehindCW) {
 | 
			
		||||
        update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
 | 
			
		||||
      }
 | 
			
		||||
      updated = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return updated ? update : null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -305,6 +340,10 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleToggleMediaVisibility = () => {
 | 
			
		||||
    this.setState({ showMedia: !this.state.showMedia });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleAccountClick = (e) => {
 | 
			
		||||
    if (this.context.router && e.button === 0) {
 | 
			
		||||
      const id = e.currentTarget.getAttribute('data-id');
 | 
			
		||||
| 
						 | 
				
			
			@ -374,6 +413,9 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
    this.setCollapsed(!this.state.isCollapsed);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleHotkeyToggleSensitive = () => {
 | 
			
		||||
    this.handleToggleMediaVisibility();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleRef = c => {
 | 
			
		||||
    this.node = c;
 | 
			
		||||
| 
						 | 
				
			
			@ -490,7 +532,8 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
              onOpenVideo={this.handleOpenVideo}
 | 
			
		||||
              width={this.props.cachedMediaWidth}
 | 
			
		||||
              cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
              revealed={settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text') ? true : undefined}
 | 
			
		||||
              visible={this.state.showMedia}
 | 
			
		||||
              onToggleVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
            />)}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
        );
 | 
			
		||||
| 
						 | 
				
			
			@ -508,7 +551,8 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
                onOpenMedia={this.props.onOpenMedia}
 | 
			
		||||
                cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
                defaultWidth={this.props.cachedMediaWidth}
 | 
			
		||||
                revealed={settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text') ? true : undefined}
 | 
			
		||||
                visible={this.state.showMedia}
 | 
			
		||||
                onToggleVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
| 
						 | 
				
			
			@ -566,6 +610,7 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
      toggleSpoiler: this.handleExpandedToggle,
 | 
			
		||||
      bookmark: this.handleHotkeyBookmark,
 | 
			
		||||
      toggleCollapse: this.handleHotkeyCollapse,
 | 
			
		||||
      toggleSensitive: this.handleHotkeyToggleSensitive,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const computedClass = classNames('status', `status-${status.get('visibility')}`, {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,6 +71,10 @@ export default class KeyboardShortcuts extends ImmutablePureComponent {
 | 
			
		|||
                <td><kbd>x</kbd></td>
 | 
			
		||||
                <td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td><kbd>h</kbd></td>
 | 
			
		||||
                <td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              {collapseEnabled && (
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td><kbd>shift</kbd>+<kbd>x</kbd></td>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
 | 
			
		|||
    onHeightChange: PropTypes.func,
 | 
			
		||||
    domain: PropTypes.string.isRequired,
 | 
			
		||||
    compact: PropTypes.bool,
 | 
			
		||||
    showMedia: PropTypes.bool,
 | 
			
		||||
    onToggleMediaVisibility: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +146,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
 | 
			
		|||
            preventPlayback={!expanded}
 | 
			
		||||
            onOpenVideo={this.handleOpenVideo}
 | 
			
		||||
            autoplay
 | 
			
		||||
            revealed={settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text') ? true : undefined}
 | 
			
		||||
            visible={this.props.showMedia}
 | 
			
		||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
        mediaIcon = 'video-camera';
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +161,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
 | 
			
		|||
            fullwidth={settings.getIn(['media', 'fullwidth'])}
 | 
			
		||||
            hidden={!expanded}
 | 
			
		||||
            onOpenMedia={this.props.onOpenMedia}
 | 
			
		||||
            revealed={settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text') ? true : undefined}
 | 
			
		||||
            visible={this.props.showMedia}
 | 
			
		||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
        mediaIcon = 'picture-o';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,7 +41,7 @@ import { HotKeys } from 'react-hotkeys';
 | 
			
		|||
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
 | 
			
		||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
 | 
			
		||||
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
 | 
			
		||||
import { textForScreenReader } from 'flavours/glitch/components/status';
 | 
			
		||||
import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
 | 
			
		||||
| 
						 | 
				
			
			@ -134,6 +134,9 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
    isExpanded: undefined,
 | 
			
		||||
    threadExpanded: undefined,
 | 
			
		||||
    statusId: undefined,
 | 
			
		||||
    loadedStatusId: undefined,
 | 
			
		||||
    showMedia: undefined,
 | 
			
		||||
    revealBehindCW: undefined,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
| 
						 | 
				
			
			@ -152,17 +155,31 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  static getDerivedStateFromProps(props, state) {
 | 
			
		||||
    if (state.statusId === props.params.statusId || !props.params.statusId) {
 | 
			
		||||
      return null;
 | 
			
		||||
    let update = {};
 | 
			
		||||
    let updated = false;
 | 
			
		||||
 | 
			
		||||
    if (props.params.statusId && state.statusId !== props.params.statusId) {
 | 
			
		||||
      props.dispatch(fetchStatus(props.params.statusId));
 | 
			
		||||
      update.threadExpanded = undefined;
 | 
			
		||||
      update.statusId = props.params.statusId;
 | 
			
		||||
      updated = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    props.dispatch(fetchStatus(props.params.statusId));
 | 
			
		||||
    const revealBehindCW = props.settings.getIn(['media', 'reveal_behind_cw']);
 | 
			
		||||
    if (revealBehindCW !== state.revealBehindCW) {
 | 
			
		||||
      update.revealBehindCW = revealBehindCW;
 | 
			
		||||
      if (revealBehindCW) update.showMedia = defaultMediaVisibility(props.status, props.settings);
 | 
			
		||||
      updated = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      threadExpanded: undefined,
 | 
			
		||||
      isExpanded: autoUnfoldCW(props.settings, props.status),
 | 
			
		||||
      statusId: props.params.statusId,
 | 
			
		||||
    };
 | 
			
		||||
    if (props.status && state.loadedStatusId !== props.status.get('id')) {
 | 
			
		||||
      update.showMedia = defaultMediaVisibility(props.status, props.settings);
 | 
			
		||||
      update.loadedStatusId = props.status.get('id');
 | 
			
		||||
      update.isExpanded = autoUnfoldCW(props.settings, props.status);
 | 
			
		||||
      updated = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return updated ? update : null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleExpandedToggle = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -171,6 +188,10 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleToggleMediaVisibility = () => {
 | 
			
		||||
    this.setState({ showMedia: !this.state.showMedia });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleModalFavourite = (status) => {
 | 
			
		||||
    this.props.dispatch(favourite(status));
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -304,6 +325,10 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
    this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleHotkeyToggleSensitive = () => {
 | 
			
		||||
    this.handleToggleMediaVisibility();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleHotkeyMoveUp = () => {
 | 
			
		||||
    this.handleMoveUp(this.props.status.get('id'));
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -477,6 +502,7 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
      mention: this.handleHotkeyMention,
 | 
			
		||||
      openProfile: this.handleHotkeyOpenProfile,
 | 
			
		||||
      toggleSpoiler: this.handleExpandedToggle,
 | 
			
		||||
      toggleSensitive: this.handleHotkeyToggleSensitive,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
| 
						 | 
				
			
			@ -505,6 +531,8 @@ export default class Status extends ImmutablePureComponent {
 | 
			
		|||
                  expanded={isExpanded}
 | 
			
		||||
                  onToggleHidden={this.handleExpandedToggle}
 | 
			
		||||
                  domain={domain}
 | 
			
		||||
                  showMedia={this.state.showMedia}
 | 
			
		||||
                  onToggleMediaVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <ActionBar
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,6 +101,7 @@ const keyMap = {
 | 
			
		|||
  toggleSpoiler: 'x',
 | 
			
		||||
  bookmark: 'd',
 | 
			
		||||
  toggleCollapse: 'shift+x',
 | 
			
		||||
  toggleSensitive: 'h',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@connect(mapStateToProps)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import { fromJS } from 'immutable';
 | 
			
		||||
import { fromJS, is } from 'immutable';
 | 
			
		||||
import { throttle } from 'lodash';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen';
 | 
			
		||||
| 
						 | 
				
			
			@ -94,7 +94,6 @@ export default class Video extends React.PureComponent {
 | 
			
		|||
    width: PropTypes.number,
 | 
			
		||||
    height: PropTypes.number,
 | 
			
		||||
    sensitive: PropTypes.bool,
 | 
			
		||||
    revealed: PropTypes.bool,
 | 
			
		||||
    startTime: PropTypes.number,
 | 
			
		||||
    onOpenVideo: PropTypes.func,
 | 
			
		||||
    onCloseVideo: PropTypes.func,
 | 
			
		||||
| 
						 | 
				
			
			@ -102,9 +101,11 @@ export default class Video extends React.PureComponent {
 | 
			
		|||
    fullwidth: PropTypes.bool,
 | 
			
		||||
    detailed: PropTypes.bool,
 | 
			
		||||
    inline: PropTypes.bool,
 | 
			
		||||
    preventPlayback: PropTypes.bool,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    cacheWidth: PropTypes.func,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    visible: PropTypes.bool,
 | 
			
		||||
    onToggleVisibility: PropTypes.func,
 | 
			
		||||
    preventPlayback: PropTypes.bool,
 | 
			
		||||
    blurhash: PropTypes.string,
 | 
			
		||||
    link: PropTypes.node,
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			@ -119,12 +120,12 @@ export default class Video extends React.PureComponent {
 | 
			
		|||
    fullscreen: false,
 | 
			
		||||
    hovered: false,
 | 
			
		||||
    muted: false,
 | 
			
		||||
    revealed: this.props.revealed === undefined ? (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all') : this.props.revealed,
 | 
			
		||||
    revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentWillReceiveProps (nextProps) {
 | 
			
		||||
    if (nextProps.revealed === true) {
 | 
			
		||||
      this.setState({ revealed: true });
 | 
			
		||||
    if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
 | 
			
		||||
      this.setState({ revealed: nextProps.visible });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -305,9 +306,6 @@ export default class Video extends React.PureComponent {
 | 
			
		|||
    if (this.video && this.state.revealed && this.props.preventPlayback && !prevProps.preventPlayback) {
 | 
			
		||||
      this.video.pause();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate (prevProps) {
 | 
			
		||||
    if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
 | 
			
		||||
      this._decode();
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -349,8 +347,12 @@ export default class Video extends React.PureComponent {
 | 
			
		|||
      this.video.pause();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.props.onToggleVisibility) {
 | 
			
		||||
      this.props.onToggleVisibility();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.setState({ revealed: !this.state.revealed });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLoadedData = () => {
 | 
			
		||||
    if (this.props.startTime) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,6 +63,14 @@ const messages = defineMessages({
 | 
			
		|||
  uploadErrorPoll:  { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
 | 
			
		||||
 | 
			
		||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
 | 
			
		||||
  if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
 | 
			
		||||
    routerHistory.push('/statuses/new');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function changeCompose(text) {
 | 
			
		||||
  return {
 | 
			
		||||
    type: COMPOSE_CHANGE,
 | 
			
		||||
| 
						 | 
				
			
			@ -77,9 +85,7 @@ export function replyCompose(status, routerHistory) {
 | 
			
		|||
      status: status,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!getState().getIn(['compose', 'mounted'])) {
 | 
			
		||||
      routerHistory.push('/statuses/new');
 | 
			
		||||
    }
 | 
			
		||||
    ensureComposeIsVisible(getState, routerHistory);
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -102,9 +108,7 @@ export function mentionCompose(account, routerHistory) {
 | 
			
		|||
      account: account,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!getState().getIn(['compose', 'mounted'])) {
 | 
			
		||||
      routerHistory.push('/statuses/new');
 | 
			
		||||
    }
 | 
			
		||||
    ensureComposeIsVisible(getState, routerHistory);
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -115,9 +119,7 @@ export function directCompose(account, routerHistory) {
 | 
			
		|||
      account: account,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!getState().getIn(['compose', 'mounted'])) {
 | 
			
		||||
      routerHistory.push('/statuses/new');
 | 
			
		||||
    }
 | 
			
		||||
    ensureComposeIsVisible(getState, routerHistory);
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ import { evictStatus } from '../storage/modifier';
 | 
			
		|||
 | 
			
		||||
import { deleteFromTimelines } from './timelines';
 | 
			
		||||
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
 | 
			
		||||
import { ensureComposeIsVisible } from './compose';
 | 
			
		||||
 | 
			
		||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
 | 
			
		||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
 | 
			
		||||
| 
						 | 
				
			
			@ -139,7 +140,7 @@ export function redraft(status, raw_text) {
 | 
			
		|||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function deleteStatus(id, router, withRedraft = false) {
 | 
			
		||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
 | 
			
		||||
  return (dispatch, getState) => {
 | 
			
		||||
    let status = getState().getIn(['statuses', id]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -156,10 +157,7 @@ export function deleteStatus(id, router, withRedraft = false) {
 | 
			
		|||
 | 
			
		||||
      if (withRedraft) {
 | 
			
		||||
        dispatch(redraft(status, response.data.text));
 | 
			
		||||
 | 
			
		||||
        if (!getState().getIn(['compose', 'mounted'])) {
 | 
			
		||||
          router.push('/statuses/new');
 | 
			
		||||
        }
 | 
			
		||||
        ensureComposeIsVisible(getState, routerHistory);
 | 
			
		||||
      }
 | 
			
		||||
    }).catch(error => {
 | 
			
		||||
      dispatch(deleteStatusFail(id, error));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,7 +49,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
 | 
			
		|||
    autoFocus: PropTypes.bool,
 | 
			
		||||
    className: PropTypes.string,
 | 
			
		||||
    id: PropTypes.string,
 | 
			
		||||
    searchTokens: ImmutablePropTypes.list,
 | 
			
		||||
    searchTokens: PropTypes.arrayOf(PropTypes.string),
 | 
			
		||||
    maxLength: PropTypes.number,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										20
									
								
								app/javascript/mastodon/components/icon_with_badge.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/javascript/mastodon/components/icon_with_badge.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
 | 
			
		||||
const formatNumber = num => num > 40 ? '40+' : num;
 | 
			
		||||
 | 
			
		||||
const IconWithBadge = ({ id, count, className }) => (
 | 
			
		||||
  <i className='icon-with-badge'>
 | 
			
		||||
    <Icon id={id} fixedWidth className={className} />
 | 
			
		||||
    {count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
 | 
			
		||||
  </i>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
IconWithBadge.propTypes = {
 | 
			
		||||
  id: PropTypes.string.isRequired,
 | 
			
		||||
  count: PropTypes.number.isRequired,
 | 
			
		||||
  className: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default IconWithBadge;
 | 
			
		||||
| 
						 | 
				
			
			@ -244,6 +244,8 @@ class MediaGallery extends React.PureComponent {
 | 
			
		|||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    defaultWidth: PropTypes.number,
 | 
			
		||||
    cacheWidth: PropTypes.func,
 | 
			
		||||
    visible: PropTypes.bool,
 | 
			
		||||
    onToggleVisibility: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
| 
						 | 
				
			
			@ -251,19 +253,25 @@ class MediaGallery extends React.PureComponent {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
 | 
			
		||||
    visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
 | 
			
		||||
    width: this.props.defaultWidth,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentWillReceiveProps (nextProps) {
 | 
			
		||||
    if (!is(nextProps.media, this.props.media)) {
 | 
			
		||||
      this.setState({ visible: !nextProps.sensitive });
 | 
			
		||||
    if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
 | 
			
		||||
      this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
 | 
			
		||||
    } else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
 | 
			
		||||
      this.setState({ visible: nextProps.visible });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleOpen = () => {
 | 
			
		||||
    if (this.props.onToggleVisibility) {
 | 
			
		||||
      this.props.onToggleVisibility();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.setState({ visible: !this.state.visible });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClick = (index) => {
 | 
			
		||||
    this.props.onOpenMedia(this.props.media, index);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,8 @@ import { HotKeys } from 'react-hotkeys';
 | 
			
		|||
import classNames from 'classnames';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
import PollContainer from 'mastodon/containers/poll_container';
 | 
			
		||||
import { displayMedia } from '../initial_state';
 | 
			
		||||
import { is } from 'immutable';
 | 
			
		||||
 | 
			
		||||
// We use the component (and not the container) since we do not want
 | 
			
		||||
// to use the progress bar to show download progress
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +41,18 @@ export const textForScreenReader = (intl, status, rebloggedByText = false) => {
 | 
			
		|||
  return values.join(', ');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const defaultMediaVisibility = (status) => {
 | 
			
		||||
  if (!status) {
 | 
			
		||||
    return undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
 | 
			
		||||
    status = status.get('reblog');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default @injectIntl
 | 
			
		||||
class Status extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -85,6 +99,10 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
    'hidden',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    showMedia: defaultMediaVisibility(this.props.status),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Track height changes we know about to compensate scrolling
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
 | 
			
		||||
| 
						 | 
				
			
			@ -98,11 +116,19 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillReceiveProps (nextProps) {
 | 
			
		||||
    if (!is(nextProps.status, this.props.status) && nextProps.status) {
 | 
			
		||||
      this.setState({ showMedia: defaultMediaVisibility(nextProps.status) });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Compensate height changes
 | 
			
		||||
  componentDidUpdate (prevProps, prevState, snapshot) {
 | 
			
		||||
    const doShowCard  = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
 | 
			
		||||
 | 
			
		||||
    if (doShowCard && !this.didShowCard) {
 | 
			
		||||
      this.didShowCard = true;
 | 
			
		||||
 | 
			
		||||
      if (snapshot !== null && this.props.updateScrollBottom) {
 | 
			
		||||
        if (this.node && this.node.offsetTop < snapshot.top) {
 | 
			
		||||
          this.props.updateScrollBottom(snapshot.height - snapshot.top);
 | 
			
		||||
| 
						 | 
				
			
			@ -122,6 +148,10 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleToggleMediaVisibility = () => {
 | 
			
		||||
    this.setState({ showMedia: !this.state.showMedia });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClick = () => {
 | 
			
		||||
    if (this.props.onClick) {
 | 
			
		||||
      this.props.onClick();
 | 
			
		||||
| 
						 | 
				
			
			@ -136,6 +166,17 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
    this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleExpandClick = (e) => {
 | 
			
		||||
    if (e.button === 0) {
 | 
			
		||||
      if (!this.context.router) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { status } = this.props;
 | 
			
		||||
      this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleAccountClick = (e) => {
 | 
			
		||||
    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
 | 
			
		||||
      const id = e.currentTarget.getAttribute('data-id');
 | 
			
		||||
| 
						 | 
				
			
			@ -198,6 +239,10 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
    this.props.onToggleHidden(this._properStatus());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleHotkeyToggleSensitive = () => {
 | 
			
		||||
    this.handleToggleMediaVisibility();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _properStatus () {
 | 
			
		||||
    const { status } = this.props;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -298,6 +343,8 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
                sensitive={status.get('sensitive')}
 | 
			
		||||
                onOpenVideo={this.handleOpenVideo}
 | 
			
		||||
                cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
                visible={this.state.showMedia}
 | 
			
		||||
                onToggleVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
| 
						 | 
				
			
			@ -313,6 +360,8 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
                onOpenMedia={this.props.onOpenMedia}
 | 
			
		||||
                cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
                defaultWidth={this.props.cachedMediaWidth}
 | 
			
		||||
                visible={this.state.showMedia}
 | 
			
		||||
                onToggleVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
| 
						 | 
				
			
			@ -348,6 +397,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
      moveUp: this.handleHotkeyMoveUp,
 | 
			
		||||
      moveDown: this.handleHotkeyMoveDown,
 | 
			
		||||
      toggleHidden: this.handleHotkeyToggleHidden,
 | 
			
		||||
      toggleSensitive: this.handleHotkeyToggleSensitive,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
| 
						 | 
				
			
			@ -356,7 +406,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
          {prepend}
 | 
			
		||||
 | 
			
		||||
          <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
 | 
			
		||||
            <div className='status__expand' onClick={this.handleClick} role='presentation' />
 | 
			
		||||
            <div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
 | 
			
		||||
            <div className='status__info'>
 | 
			
		||||
              <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ class ActionBar extends React.PureComponent {
 | 
			
		|||
    return (
 | 
			
		||||
      <div className='compose__action-bar'>
 | 
			
		||||
        <div className='compose__action-bar-dropdown'>
 | 
			
		||||
          <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
 | 
			
		||||
          <DropdownMenuContainer items={menu} icon='chevron-down' size={16} direction='right' />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ export default class NavigationBar extends ImmutablePureComponent {
 | 
			
		|||
      <div className='navigation-bar'>
 | 
			
		||||
        <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
 | 
			
		||||
          <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
 | 
			
		||||
          <Avatar account={this.props.account} size={40} />
 | 
			
		||||
          <Avatar account={this.props.account} size={48} />
 | 
			
		||||
        </Permalink>
 | 
			
		||||
 | 
			
		||||
        <div className='navigation-bar__profile'>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,6 +47,10 @@ class SearchPopout extends React.PureComponent {
 | 
			
		|||
export default @injectIntl
 | 
			
		||||
class Search extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    router: PropTypes.object.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    value: PropTypes.string.isRequired,
 | 
			
		||||
    submitted: PropTypes.bool,
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +58,7 @@ class Search extends React.PureComponent {
 | 
			
		|||
    onSubmit: PropTypes.func.isRequired,
 | 
			
		||||
    onClear: PropTypes.func.isRequired,
 | 
			
		||||
    onShow: PropTypes.func.isRequired,
 | 
			
		||||
    openInRoute: PropTypes.bool,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +81,12 @@ class Search extends React.PureComponent {
 | 
			
		|||
  handleKeyUp = (e) => {
 | 
			
		||||
    if (e.key === 'Enter') {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
 | 
			
		||||
      this.props.onSubmit();
 | 
			
		||||
 | 
			
		||||
      if (this.props.openInRoute) {
 | 
			
		||||
        this.context.router.history.push('/search');
 | 
			
		||||
      }
 | 
			
		||||
    } else if (e.key === 'Escape') {
 | 
			
		||||
      document.querySelector('.ui').parentElement.focus();
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,7 +56,7 @@ class FollowRequests extends ImmutablePureComponent {
 | 
			
		|||
    const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Column icon='users' heading={intl.formatMessage(messages.heading)}>
 | 
			
		||||
      <Column icon='user-plus' heading={intl.formatMessage(messages.heading)}>
 | 
			
		||||
        <ColumnBackButtonSlim />
 | 
			
		||||
        <ScrollableList
 | 
			
		||||
          scrollKey='follow_requests'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,12 +9,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		|||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { me, invitesEnabled, version, profile_directory, repository, source_url } from '../../initial_state';
 | 
			
		||||
import { fetchFollowRequests } from 'mastodon/actions/accounts';
 | 
			
		||||
import { changeSetting } from 'mastodon/actions/settings';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import NavigationBar from '../compose/components/navigation_bar';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
import Toggle from 'react-toggle';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
 | 
			
		||||
| 
						 | 
				
			
			@ -41,12 +39,10 @@ const messages = defineMessages({
 | 
			
		|||
const mapStateToProps = state => ({
 | 
			
		||||
  myAccount: state.getIn(['accounts', me]),
 | 
			
		||||
  unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
 | 
			
		||||
  forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = dispatch => ({
 | 
			
		||||
  fetchFollowRequests: () => dispatch(fetchFollowRequests()),
 | 
			
		||||
  changeForceSingleColumn: checked => dispatch(changeSetting(['forceSingleColumn'], checked)),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const badgeDisplay = (number, limit) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -59,10 +55,16 @@ const badgeDisplay = (number, limit) => {
 | 
			
		|||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
 | 
			
		||||
 | 
			
		||||
export default @connect(mapStateToProps, mapDispatchToProps)
 | 
			
		||||
@injectIntl
 | 
			
		||||
class GettingStarted extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    router: PropTypes.object.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    myAccount: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
| 
						 | 
				
			
			@ -71,24 +73,23 @@ class GettingStarted extends ImmutablePureComponent {
 | 
			
		|||
    fetchFollowRequests: PropTypes.func.isRequired,
 | 
			
		||||
    unreadFollowRequests: PropTypes.number,
 | 
			
		||||
    unreadNotifications: PropTypes.number,
 | 
			
		||||
    forceSingleColumn: PropTypes.bool,
 | 
			
		||||
    changeForceSingleColumn: PropTypes.func.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    const { myAccount, fetchFollowRequests } = this.props;
 | 
			
		||||
    const { myAccount, fetchFollowRequests, multiColumn } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
 | 
			
		||||
      this.context.router.history.replace('/timelines/home');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (myAccount.get('locked')) {
 | 
			
		||||
      fetchFollowRequests();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleForceSingleColumnChange = ({ target }) => {
 | 
			
		||||
    this.props.changeForceSingleColumn(target.checked);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { intl, myAccount, multiColumn, unreadFollowRequests, forceSingleColumn } = this.props;
 | 
			
		||||
    const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
 | 
			
		||||
 | 
			
		||||
    const navItems = [];
 | 
			
		||||
    let i = 1;
 | 
			
		||||
| 
						 | 
				
			
			@ -133,7 +134,7 @@ class GettingStarted extends ImmutablePureComponent {
 | 
			
		|||
    height += 48*3;
 | 
			
		||||
 | 
			
		||||
    if (myAccount.get('locked')) {
 | 
			
		||||
      navItems.push(<ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
 | 
			
		||||
      navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
 | 
			
		||||
      height += 48;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -187,11 +188,6 @@ class GettingStarted extends ImmutablePureComponent {
 | 
			
		|||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <label className='navigational-toggle'>
 | 
			
		||||
          <FormattedMessage id='getting_started.use_simple_layout' defaultMessage='Use simple layout' />
 | 
			
		||||
          <Toggle checked={forceSingleColumn} onChange={this.handleForceSingleColumnChange} />
 | 
			
		||||
        </label>
 | 
			
		||||
      </Column>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,6 +60,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
 | 
			
		|||
                <td><kbd>x</kbd></td>
 | 
			
		||||
                <td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td><kbd>h</kbd></td>
 | 
			
		||||
                <td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td><kbd>up</kbd>, <kbd>k</kbd></td>
 | 
			
		||||
                <td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								app/javascript/mastodon/features/search/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/javascript/mastodon/features/search/index.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import SearchContainer from 'mastodon/features/compose/containers/search_container';
 | 
			
		||||
import SearchResultsContainer from 'mastodon/features/compose/containers/search_results_container';
 | 
			
		||||
 | 
			
		||||
const Search = () => (
 | 
			
		||||
  <div className='column search-page'>
 | 
			
		||||
    <SearchContainer />
 | 
			
		||||
 | 
			
		||||
    <div className='drawer__pager'>
 | 
			
		||||
      <div className='drawer__inner darker'>
 | 
			
		||||
        <SearchResultsContainer />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Search;
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +30,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
 | 
			
		|||
    onHeightChange: PropTypes.func,
 | 
			
		||||
    domain: PropTypes.string.isRequired,
 | 
			
		||||
    compact: PropTypes.bool,
 | 
			
		||||
    showMedia: PropTypes.bool,
 | 
			
		||||
    onToggleMediaVisibility: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
| 
						 | 
				
			
			@ -122,6 +124,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
 | 
			
		|||
            inline
 | 
			
		||||
            onOpenVideo={this.handleOpenVideo}
 | 
			
		||||
            sensitive={status.get('sensitive')}
 | 
			
		||||
            visible={this.props.showMedia}
 | 
			
		||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +136,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
 | 
			
		|||
            media={status.get('media_attachments')}
 | 
			
		||||
            height={300}
 | 
			
		||||
            onOpenMedia={this.props.onOpenMedia}
 | 
			
		||||
            visible={this.props.showMedia}
 | 
			
		||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		|||
import { HotKeys } from 'react-hotkeys';
 | 
			
		||||
import { boostModal, deleteModal } from '../../initial_state';
 | 
			
		||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
 | 
			
		||||
import { textForScreenReader } from '../../components/status';
 | 
			
		||||
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
| 
						 | 
				
			
			@ -131,6 +131,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
  state = {
 | 
			
		||||
    fullscreen: false,
 | 
			
		||||
    showMedia: defaultMediaVisibility(this.props.status),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentWillMount () {
 | 
			
		||||
| 
						 | 
				
			
			@ -146,6 +147,14 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
      this._scrolledIntoView = false;
 | 
			
		||||
      this.props.dispatch(fetchStatus(nextProps.params.statusId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!Immutable.is(nextProps.status, this.props.status) && nextProps.status) {
 | 
			
		||||
      this.setState({ showMedia: defaultMediaVisibility(nextProps.status) });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleToggleMediaVisibility = () => {
 | 
			
		||||
    this.setState({ showMedia: !this.state.showMedia });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleFavouriteClick = (status) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -312,6 +321,10 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
    this.handleToggleHidden(this.props.status);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleHotkeyToggleSensitive = () => {
 | 
			
		||||
    this.handleToggleMediaVisibility();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMoveUp = id => {
 | 
			
		||||
    const { status, ancestorsIds, descendantsIds } = this.props;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -432,6 +445,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
      mention: this.handleHotkeyMention,
 | 
			
		||||
      openProfile: this.handleHotkeyOpenProfile,
 | 
			
		||||
      toggleHidden: this.handleHotkeyToggleHidden,
 | 
			
		||||
      toggleSensitive: this.handleHotkeyToggleSensitive,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
| 
						 | 
				
			
			@ -455,6 +469,8 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
                  onOpenMedia={this.handleOpenMedia}
 | 
			
		||||
                  onToggleHidden={this.handleToggleHidden}
 | 
			
		||||
                  domain={domain}
 | 
			
		||||
                  showMedia={this.state.showMedia}
 | 
			
		||||
                  onToggleMediaVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <ActionBar
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,8 @@ import DrawerLoading from './drawer_loading';
 | 
			
		|||
import BundleColumnError from './bundle_column_error';
 | 
			
		||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
import ComposePanel from './compose_panel';
 | 
			
		||||
import NavigationPanel from './navigation_panel';
 | 
			
		||||
 | 
			
		||||
import detectPassiveEvents from 'detect-passive-events';
 | 
			
		||||
import { scrollRight } from '../../../scroll';
 | 
			
		||||
| 
						 | 
				
			
			@ -173,14 +175,22 @@ class ColumnsArea extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
      return (
 | 
			
		||||
        <div className='columns-area__panels'>
 | 
			
		||||
          <div className='columns-area__panels__pane' />
 | 
			
		||||
          <div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
 | 
			
		||||
            <div className='columns-area__panels__pane__inner'>
 | 
			
		||||
              <ComposePanel />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='columns-area__panels__main'>
 | 
			
		||||
            <TabsBar key='tabs' />
 | 
			
		||||
            {content}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='columns-area__panels__pane' />
 | 
			
		||||
          <div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
 | 
			
		||||
            <div className='columns-area__panels__pane__inner'>
 | 
			
		||||
              <NavigationPanel />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {floatingActionButton}
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import SearchContainer from 'mastodon/features/compose/containers/search_container';
 | 
			
		||||
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
 | 
			
		||||
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
 | 
			
		||||
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
const ComposePanel = () => (
 | 
			
		||||
  <div className='compose-panel'>
 | 
			
		||||
    <SearchContainer openInRoute />
 | 
			
		||||
    <NavigationContainer />
 | 
			
		||||
    <ComposeFormContainer />
 | 
			
		||||
 | 
			
		||||
    <div className='flex-spacer' />
 | 
			
		||||
 | 
			
		||||
    <div className='getting-started__footer'>
 | 
			
		||||
      <ul>
 | 
			
		||||
        {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
 | 
			
		||||
        <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>
 | 
			
		||||
        <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
 | 
			
		||||
        <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
 | 
			
		||||
        <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
 | 
			
		||||
        <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
 | 
			
		||||
        <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
 | 
			
		||||
        <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
 | 
			
		||||
        <li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
 | 
			
		||||
      </ul>
 | 
			
		||||
 | 
			
		||||
      <p>
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='getting_started.open_source_notice'
 | 
			
		||||
          defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
 | 
			
		||||
          values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
 | 
			
		||||
        />
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default ComposePanel;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { fetchFollowRequests } from 'mastodon/actions/accounts';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import { NavLink, withRouter } from 'react-router-dom';
 | 
			
		||||
import IconWithBadge from 'mastodon/components/icon_with_badge';
 | 
			
		||||
import { me } from 'mastodon/initial_state';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  locked: state.getIn(['accounts', me, 'locked']),
 | 
			
		||||
  count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @withRouter
 | 
			
		||||
@connect(mapStateToProps)
 | 
			
		||||
class FollowRequestsNavLink extends React.Component {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    dispatch: PropTypes.func.isRequired,
 | 
			
		||||
    locked: PropTypes.bool,
 | 
			
		||||
    count: PropTypes.number.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    const { dispatch, locked } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (locked) {
 | 
			
		||||
      dispatch(fetchFollowRequests());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { locked, count } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!locked || count === 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return <NavLink className='column-link column-link--transparent' to='/follow_requests'><IconWithBadge className='column-link__icon' id='user-plus' count={count} /><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></NavLink>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								app/javascript/mastodon/features/ui/components/list_panel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								app/javascript/mastodon/features/ui/components/list_panel.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { fetchLists } from 'mastodon/actions/lists';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import { createSelector } from 'reselect';
 | 
			
		||||
import { NavLink, withRouter } from 'react-router-dom';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
 | 
			
		||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
 | 
			
		||||
  if (!lists) {
 | 
			
		||||
    return lists;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  lists: getOrderedLists(state),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @withRouter
 | 
			
		||||
@connect(mapStateToProps)
 | 
			
		||||
class ListPanel extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    dispatch: PropTypes.func.isRequired,
 | 
			
		||||
    lists: ImmutablePropTypes.list,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    const { dispatch } = this.props;
 | 
			
		||||
    dispatch(fetchLists());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { lists } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!lists || lists.isEmpty()) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <hr />
 | 
			
		||||
 | 
			
		||||
        {lists.map(list => (
 | 
			
		||||
          <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import { NavLink, withRouter } from 'react-router-dom';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
import NotificationsCounterIcon from './notifications_counter_icon';
 | 
			
		||||
import FollowRequestsNavLink from './follow_requests_nav_link';
 | 
			
		||||
import ListPanel from './list_panel';
 | 
			
		||||
 | 
			
		||||
const NavigationPanel = () => (
 | 
			
		||||
  <div className='navigation-panel'>
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
 | 
			
		||||
    <FollowRequestsNavLink />
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
 | 
			
		||||
    <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
 | 
			
		||||
 | 
			
		||||
    <ListPanel />
 | 
			
		||||
 | 
			
		||||
    <hr />
 | 
			
		||||
 | 
			
		||||
    <a className='column-link column-link--transparent' href='/settings/preferences' target='_blank'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
 | 
			
		||||
    <a className='column-link column-link--transparent' href='/relationships' target='_blank'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default withRouter(NavigationPanel);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,23 +1,9 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
import IconWithBadge from 'mastodon/components/icon_with_badge';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  count: state.getIn(['notifications', 'unread']),
 | 
			
		||||
  id: 'bell',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const formatNumber = num => num > 99 ? '99+' : num;
 | 
			
		||||
 | 
			
		||||
const NotificationsCounterIcon = ({ count }) => (
 | 
			
		||||
  <i className='icon-with-badge'>
 | 
			
		||||
    <Icon id='bell' fixedWidth />
 | 
			
		||||
    {count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
 | 
			
		||||
  </i>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
NotificationsCounterIcon.propTypes = {
 | 
			
		||||
  count: PropTypes.number.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(NotificationsCounterIcon);
 | 
			
		||||
export default connect(mapStateToProps)(IconWithBadge);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,14 +8,12 @@ import Icon from 'mastodon/components/icon';
 | 
			
		|||
import NotificationsCounterIcon from './notifications_counter_icon';
 | 
			
		||||
 | 
			
		||||
export const links = [
 | 
			
		||||
  <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
 | 
			
		||||
 | 
			
		||||
  <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
 | 
			
		||||
 | 
			
		||||
  <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
 | 
			
		||||
  <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export function getIndex (path) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,8 +44,9 @@ import {
 | 
			
		|||
  Mutes,
 | 
			
		||||
  PinnedStatuses,
 | 
			
		||||
  Lists,
 | 
			
		||||
  Search,
 | 
			
		||||
} from './util/async-components';
 | 
			
		||||
import { me } from '../../initial_state';
 | 
			
		||||
import { me, forceSingleColumn } from '../../initial_state';
 | 
			
		||||
import { previewState as previewMediaState } from './components/media_modal';
 | 
			
		||||
import { previewState as previewVideoState } from './components/video_modal';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +63,6 @@ const mapStateToProps = state => ({
 | 
			
		|||
  hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
 | 
			
		||||
  hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
 | 
			
		||||
  dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
 | 
			
		||||
  forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const keyMap = {
 | 
			
		||||
| 
						 | 
				
			
			@ -93,6 +93,7 @@ const keyMap = {
 | 
			
		|||
  goToMuted: 'g m',
 | 
			
		||||
  goToRequests: 'g r',
 | 
			
		||||
  toggleHidden: 'x',
 | 
			
		||||
  toggleSensitive: 'h',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class SwitchingColumnsArea extends React.PureComponent {
 | 
			
		||||
| 
						 | 
				
			
			@ -101,7 +102,6 @@ class SwitchingColumnsArea extends React.PureComponent {
 | 
			
		|||
    children: PropTypes.node,
 | 
			
		||||
    location: PropTypes.object,
 | 
			
		||||
    onLayoutChange: PropTypes.func.isRequired,
 | 
			
		||||
    forceSingleColumn: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
| 
						 | 
				
			
			@ -140,7 +140,7 @@ class SwitchingColumnsArea extends React.PureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { children, forceSingleColumn } = this.props;
 | 
			
		||||
    const { children } = this.props;
 | 
			
		||||
    const { mobile } = this.state;
 | 
			
		||||
    const singleColumn = forceSingleColumn || mobile;
 | 
			
		||||
    const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
 | 
			
		||||
| 
						 | 
				
			
			@ -162,7 +162,7 @@ class SwitchingColumnsArea extends React.PureComponent {
 | 
			
		|||
          <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 | 
			
		||||
          <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 | 
			
		||||
 | 
			
		||||
          <WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} />
 | 
			
		||||
          <WrappedRoute path='/search' component={Search} content={children} />
 | 
			
		||||
 | 
			
		||||
          <WrappedRoute path='/statuses/new' component={Compose} content={children} />
 | 
			
		||||
          <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 | 
			
		||||
| 
						 | 
				
			
			@ -207,7 +207,6 @@ class UI extends React.PureComponent {
 | 
			
		|||
    location: PropTypes.object,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    dropdownMenuIsOpen: PropTypes.bool,
 | 
			
		||||
    forceSingleColumn: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
| 
						 | 
				
			
			@ -456,7 +455,7 @@ class UI extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { draggingOver } = this.state;
 | 
			
		||||
    const { children, isComposing, location, dropdownMenuIsOpen, forceSingleColumn } = this.props;
 | 
			
		||||
    const { children, isComposing, location, dropdownMenuIsOpen } = this.props;
 | 
			
		||||
 | 
			
		||||
    const handlers = {
 | 
			
		||||
      help: this.handleHotkeyToggleHelp,
 | 
			
		||||
| 
						 | 
				
			
			@ -482,7 +481,7 @@ class UI extends React.PureComponent {
 | 
			
		|||
    return (
 | 
			
		||||
      <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
 | 
			
		||||
        <div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
 | 
			
		||||
          <SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange} forceSingleColumn={forceSingleColumn}>
 | 
			
		||||
          <SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}>
 | 
			
		||||
            {children}
 | 
			
		||||
          </SwitchingColumnsArea>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -129,3 +129,7 @@ export function ListEditor () {
 | 
			
		|||
export function ListAdder () {
 | 
			
		||||
  return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Search () {
 | 
			
		||||
  return import(/*webpackChunkName: "features/search" */'../../search');
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import { fromJS } from 'immutable';
 | 
			
		||||
import { fromJS, is } from 'immutable';
 | 
			
		||||
import { throttle } from 'lodash';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
 | 
			
		||||
| 
						 | 
				
			
			@ -102,6 +102,8 @@ class Video extends React.PureComponent {
 | 
			
		|||
    detailed: PropTypes.bool,
 | 
			
		||||
    inline: PropTypes.bool,
 | 
			
		||||
    cacheWidth: PropTypes.func,
 | 
			
		||||
    visible: PropTypes.bool,
 | 
			
		||||
    onToggleVisibility: PropTypes.func,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    blurhash: PropTypes.string,
 | 
			
		||||
    link: PropTypes.node,
 | 
			
		||||
| 
						 | 
				
			
			@ -117,7 +119,7 @@ class Video extends React.PureComponent {
 | 
			
		|||
    fullscreen: false,
 | 
			
		||||
    hovered: false,
 | 
			
		||||
    muted: false,
 | 
			
		||||
    revealed: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
 | 
			
		||||
    revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // hard coded in components.scss
 | 
			
		||||
| 
						 | 
				
			
			@ -280,7 +282,16 @@ class Video extends React.PureComponent {
 | 
			
		|||
    document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate (prevProps) {
 | 
			
		||||
  componentWillReceiveProps (nextProps) {
 | 
			
		||||
    if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
 | 
			
		||||
      this.setState({ revealed: nextProps.visible });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate (prevProps, prevState) {
 | 
			
		||||
    if (prevState.revealed && !this.state.revealed && this.video) {
 | 
			
		||||
      this.video.pause();
 | 
			
		||||
    }
 | 
			
		||||
    if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
 | 
			
		||||
      this._decode();
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -316,12 +327,12 @@ class Video extends React.PureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  toggleReveal = () => {
 | 
			
		||||
    if (this.state.revealed) {
 | 
			
		||||
      this.video.pause();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.props.onToggleVisibility) {
 | 
			
		||||
      this.props.onToggleVisibility();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.setState({ revealed: !this.state.revealed });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLoadedData = () => {
 | 
			
		||||
    if (this.props.startTime) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,5 +20,6 @@ export const version = getMeta('version');
 | 
			
		|||
export const mascot = getMeta('mascot');
 | 
			
		||||
export const profile_directory = getMeta('profile_directory');
 | 
			
		||||
export const isStaff = getMeta('is_staff');
 | 
			
		||||
export const forceSingleColumn = !getMeta('advanced_layout');
 | 
			
		||||
 | 
			
		||||
export default initialState;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,8 +14,6 @@ const initialState = ImmutableMap({
 | 
			
		|||
 | 
			
		||||
  skinTone: 1,
 | 
			
		||||
 | 
			
		||||
  forceSingleColumn: false,
 | 
			
		||||
 | 
			
		||||
  home: ImmutableMap({
 | 
			
		||||
    shows: ImmutableMap({
 | 
			
		||||
      reblog: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -710,7 +710,7 @@
 | 
			
		|||
    white-space: pre-wrap;
 | 
			
		||||
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
      margin-bottom: 2px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1801,7 +1801,12 @@ a.account__display-name {
 | 
			
		|||
      display: flex;
 | 
			
		||||
      justify-content: flex-end;
 | 
			
		||||
 | 
			
		||||
      &--start {
 | 
			
		||||
        justify-content: flex-start;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &__inner {
 | 
			
		||||
        width: 285px;
 | 
			
		||||
        pointer-events: auto;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -1925,6 +1930,7 @@ a.account__display-name {
 | 
			
		|||
  display: block;
 | 
			
		||||
  flex: 1 1 auto;
 | 
			
		||||
  padding: 15px 10px;
 | 
			
		||||
  padding-bottom: 13px;
 | 
			
		||||
  color: $primary-text-color;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
| 
						 | 
				
			
			@ -1949,6 +1955,7 @@ a.account__display-name {
 | 
			
		|||
  &:active {
 | 
			
		||||
    @media screen and (min-width: 631px) {
 | 
			
		||||
      background: lighten($ui-base-color, 14%);
 | 
			
		||||
      border-bottom-color: lighten($ui-base-color, 14%);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1978,11 +1985,21 @@ a.account__display-name {
 | 
			
		|||
    padding: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .search__input,
 | 
			
		||||
  .autosuggest-textarea__textarea {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .search__input {
 | 
			
		||||
    line-height: 18px;
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
    padding-right: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .search__icon .fa {
 | 
			
		||||
    top: 15px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media screen and (min-width: 360px) {
 | 
			
		||||
    padding: 10px 0;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -2038,6 +2055,58 @@ a.account__display-name {
 | 
			
		|||
        margin-top: 10px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .account {
 | 
			
		||||
      padding: 15px 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .notification {
 | 
			
		||||
      &__message {
 | 
			
		||||
        margin-left: 48px + 15px * 2;
 | 
			
		||||
        padding-top: 15px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &__favourite-icon-wrapper {
 | 
			
		||||
        left: -32px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .status {
 | 
			
		||||
        padding-top: 8px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .account {
 | 
			
		||||
        padding-top: 8px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .account__avatar-wrapper {
 | 
			
		||||
        margin-left: 17px;
 | 
			
		||||
        margin-right: 15px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.floating-action-button {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  width: 3.9375rem;
 | 
			
		||||
  height: 3.9375rem;
 | 
			
		||||
  bottom: 1.3125rem;
 | 
			
		||||
  right: 1.3125rem;
 | 
			
		||||
  background: darken($ui-highlight-color, 3%);
 | 
			
		||||
  color: $white;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  font-size: 21px;
 | 
			
		||||
  line-height: 21px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4);
 | 
			
		||||
 | 
			
		||||
  &:hover,
 | 
			
		||||
  &:focus,
 | 
			
		||||
  &:active {
 | 
			
		||||
    background: lighten($ui-highlight-color, 7%);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -2059,12 +2128,41 @@ a.account__display-name {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 600px + (285px * 1) + (10px * 1)) {
 | 
			
		||||
  .columns-area__panels__pane--compositional {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (min-width: 600px + (285px * 1) + (10px * 1)) {
 | 
			
		||||
  .floating-action-button,
 | 
			
		||||
  .tabs-bar__link.optional {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .search-page .search {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 600px + (285px * 2) + (10px * 2)) {
 | 
			
		||||
  .columns-area__panels__pane--navigational {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (min-width: 600px + (285px * 2) + (10px * 2)) {
 | 
			
		||||
  .tabs-bar {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-with-badge {
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  &__badge {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: -13px;
 | 
			
		||||
    left: 9px;
 | 
			
		||||
    top: -13px;
 | 
			
		||||
    background: $ui-highlight-color;
 | 
			
		||||
    border: 2px solid lighten($ui-base-color, 8%);
 | 
			
		||||
| 
						 | 
				
			
			@ -2077,6 +2175,57 @@ a.account__display-name {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.column-link--transparent .icon-with-badge__badge {
 | 
			
		||||
  border-color: darken($ui-base-color, 8%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.compose-panel {
 | 
			
		||||
  width: 285px;
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
 | 
			
		||||
  .search__input {
 | 
			
		||||
    line-height: 18px;
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
    padding-right: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .search__icon .fa {
 | 
			
		||||
    top: 15px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .navigation-bar {
 | 
			
		||||
    padding-top: 20px;
 | 
			
		||||
    padding-bottom: 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .flex-spacer {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .autosuggest-textarea__textarea {
 | 
			
		||||
    max-height: 200px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .compose-form__upload-thumbnail {
 | 
			
		||||
    height: 80px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navigation-panel {
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
 | 
			
		||||
  hr {
 | 
			
		||||
    border: 0;
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    border-top: 1px solid lighten($ui-base-color, 4%);
 | 
			
		||||
    margin: 10px 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.drawer__pager {
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -2127,15 +2276,6 @@ a.account__display-name {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navigational-toggle {
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  color: $dark-text-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pseudo-drawer {
 | 
			
		||||
  background: lighten($ui-base-color, 13%);
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
| 
						 | 
				
			
			@ -2365,9 +2505,31 @@ a.account__display-name {
 | 
			
		|||
  padding: 15px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
  &:hover,
 | 
			
		||||
  &:focus,
 | 
			
		||||
  &:active {
 | 
			
		||||
    background: lighten($ui-base-color, 11%);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:focus {
 | 
			
		||||
    outline: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--transparent {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    color: $ui-secondary-color;
 | 
			
		||||
 | 
			
		||||
    &:hover,
 | 
			
		||||
    &:focus,
 | 
			
		||||
    &:active {
 | 
			
		||||
      background: transparent;
 | 
			
		||||
      color: $primary-text-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.active {
 | 
			
		||||
      color: $ui-highlight-color;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.column-link__icon {
 | 
			
		||||
| 
						 | 
				
			
			@ -5436,34 +5598,6 @@ noscript {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.floating-action-button {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  width: 3.9375rem;
 | 
			
		||||
  height: 3.9375rem;
 | 
			
		||||
  bottom: 1.3125rem;
 | 
			
		||||
  right: 1.3125rem;
 | 
			
		||||
  background: darken($ui-highlight-color, 3%);
 | 
			
		||||
  color: $white;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  font-size: 21px;
 | 
			
		||||
  line-height: 21px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4);
 | 
			
		||||
 | 
			
		||||
  &:hover,
 | 
			
		||||
  &:focus,
 | 
			
		||||
  &:active {
 | 
			
		||||
    background: lighten($ui-highlight-color, 7%);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media screen and (min-width: 630px) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.account__header__content {
 | 
			
		||||
  color: $darker-text-color;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,6 +36,7 @@ class UserSettingsDecorator
 | 
			
		|||
    user.settings['hide_network']        = hide_network_preference if change?('setting_hide_network')
 | 
			
		||||
    user.settings['aggregate_reblogs']   = aggregate_reblogs_preference if change?('setting_aggregate_reblogs')
 | 
			
		||||
    user.settings['show_application']    = show_application_preference if change?('setting_show_application')
 | 
			
		||||
    user.settings['advanced_layout']     = advanced_layout_preference if change?('setting_advanced_layout')
 | 
			
		||||
    user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type')
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -123,6 +124,10 @@ class UserSettingsDecorator
 | 
			
		|||
    boolean_cast_setting 'setting_aggregate_reblogs'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def advanced_layout_preference
 | 
			
		||||
    boolean_cast_setting 'setting_advanced_layout'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def default_content_type_preference
 | 
			
		||||
    settings['setting_default_content_type']
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -104,7 +104,8 @@ class User < ApplicationRecord
 | 
			
		|||
 | 
			
		||||
  delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
 | 
			
		||||
           :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
 | 
			
		||||
           :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :default_content_type, to: :settings, prefix: :setting, allow_nil: false
 | 
			
		||||
           :expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
 | 
			
		||||
           :advanced_layout, :default_content_type, to: :settings, prefix: :setting, allow_nil: false
 | 
			
		||||
 | 
			
		||||
  attr_reader :invite_code
 | 
			
		||||
  attr_writer :external
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,6 +46,7 @@ class InitialStateSerializer < ActiveModel::Serializer
 | 
			
		|||
      store[:display_media]   = object.current_account.user.setting_display_media
 | 
			
		||||
      store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
 | 
			
		||||
      store[:reduce_motion]   = object.current_account.user.setting_reduce_motion
 | 
			
		||||
      store[:advanced_layout] = object.current_account.user.setting_advanced_layout
 | 
			
		||||
      store[:is_staff]        = object.current_account.user.staff?
 | 
			
		||||
      store[:default_content_type] = object.current_account.user.setting_default_content_type
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,6 +46,9 @@
 | 
			
		|||
 | 
			
		||||
  %hr#settings_web/
 | 
			
		||||
 | 
			
		||||
  .fields-group
 | 
			
		||||
    = f.input :setting_advanced_layout, as: :boolean, wrapper: :with_label
 | 
			
		||||
 | 
			
		||||
  .fields-group
 | 
			
		||||
    = f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label
 | 
			
		||||
    = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ cs:
 | 
			
		|||
        password: Použijte alespoň 8 znaků
 | 
			
		||||
        phrase: Shoda bude nalezena bez ohledu na velikost písmen v těle tootu či varování o obsahu
 | 
			
		||||
        scopes: Která API bude aplikaci povoleno používat. Pokud vyberete rozsah nejvyššího stupně, nebudete je muset vybírat jednotlivě.
 | 
			
		||||
        setting_advanced_layout: Pokročilé rozhraní se skládá z několika přizpůsobitelných sloupců
 | 
			
		||||
        setting_aggregate_reblogs: Nezobrazovat nové boosty pro tooty, které byly nedávno boostnuty (ovlivňuje pouze nově přijaté boosty)
 | 
			
		||||
        setting_default_language: Jazyk vašich tootů může být detekován automaticky, není to však vždy přesné
 | 
			
		||||
        setting_display_media_default: Skrývat média označená jako citlivá
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +91,7 @@ cs:
 | 
			
		|||
        otp_attempt: Dvoufázový kód
 | 
			
		||||
        password: Heslo
 | 
			
		||||
        phrase: Klíčové slovo či fráze
 | 
			
		||||
        setting_advanced_layout: Povolit pokročilé webové rozhraní
 | 
			
		||||
        setting_aggregate_reblogs: Seskupovat boosty v časových osách
 | 
			
		||||
        setting_auto_play_gif: Automaticky přehrávat animace GIF
 | 
			
		||||
        setting_boost_modal: Zobrazovat před boostnutím potvrzovací okno
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ en:
 | 
			
		|||
        password: Use at least 8 characters
 | 
			
		||||
        phrase: Will be matched regardless of casing in text or content warning of a toot
 | 
			
		||||
        scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones.
 | 
			
		||||
        setting_advanced_layout: The advanced UI consists of multiple customizable columns
 | 
			
		||||
        setting_aggregate_reblogs: Do not show new boosts for toots that have been recently boosted (only affects newly-received boosts)
 | 
			
		||||
        setting_default_content_type_html: When writing toots, assume they are written in raw HTML, unless specified otherwise
 | 
			
		||||
        setting_default_content_type_markdown: When writing toots, assume they are using Markdown for rich text formatting, unless specified otherwise
 | 
			
		||||
| 
						 | 
				
			
			@ -93,6 +94,7 @@ en:
 | 
			
		|||
        otp_attempt: Two-factor code
 | 
			
		||||
        password: Password
 | 
			
		||||
        phrase: Keyword or phrase
 | 
			
		||||
        setting_advanced_layout: Enable advanced web interface
 | 
			
		||||
        setting_aggregate_reblogs: Group boosts in timelines
 | 
			
		||||
        setting_auto_play_gif: Auto-play animated GIFs
 | 
			
		||||
        setting_boost_modal: Show confirmation dialog before boosting
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ sk:
 | 
			
		|||
        password: Zadaj aspoň osem znakov
 | 
			
		||||
        phrase: Zhoda sa nájde nezávisle od toho, či je text napísaný, veľkými, alebo malými písmenami, či už v tele, alebo v hlavičke
 | 
			
		||||
        scopes: Ktoré API budú povolené aplikácii pre prístup. Ak vyberieš vrcholný stupeň, nemusíš už potom vyberať po jednom.
 | 
			
		||||
        setting_advanced_layout: Pokročilé užívateľské rozhranie sa skladá z viacero prispôsobiteľných stĺpcov
 | 
			
		||||
        setting_aggregate_reblogs: Nezobrazuj nové vyzdvihnutia pre príspevky, ktoré už boli len nedávno povýšené (týka sa iba nanovo získaných povýšení)
 | 
			
		||||
        setting_default_language: Jazyk tvojích príspevkov môže byť zistený automaticky, ale nieje to vždy presné
 | 
			
		||||
        setting_display_media_default: Skry médiá označené ako citlivé
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +91,7 @@ sk:
 | 
			
		|||
        otp_attempt: Dvoj-faktorový overovací (2FA) kód
 | 
			
		||||
        password: Heslo
 | 
			
		||||
        phrase: Kľúčové slovo, alebo fráza
 | 
			
		||||
        setting_advanced_layout: Zapni pokročilé užívateľské rozhranie
 | 
			
		||||
        setting_aggregate_reblogs: Zoskupuj vyzdvihnutia v časovej osi
 | 
			
		||||
        setting_auto_play_gif: Automaticky prehrávaj animované GIFy
 | 
			
		||||
        setting_boost_modal: Zobrazuj potvrdzovacie okno pred povýšením
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +101,7 @@ sk:
 | 
			
		|||
        setting_delete_modal: Zobrazuj potvrdzovacie okno pred vymazaním toot-u
 | 
			
		||||
        setting_display_media: Zobrazovanie médií
 | 
			
		||||
        setting_display_media_default: Štandard
 | 
			
		||||
        setting_display_media_hide_all: Skryť všetky
 | 
			
		||||
        setting_display_media_hide_all: Skry všetky
 | 
			
		||||
        setting_display_media_show_all: Ukáž všetky
 | 
			
		||||
        setting_expand_spoilers: Stále rozbaľ príspevky označené varovaním o obsahu
 | 
			
		||||
        setting_hide_network: Ukri svoju sieť kontaktov
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +114,7 @@ sk:
 | 
			
		|||
        severity: Závažnosť
 | 
			
		||||
        type: Typ importu
 | 
			
		||||
        username: Prezývka
 | 
			
		||||
        username_or_email: Prezívka, alebo email
 | 
			
		||||
        username_or_email: Prezývka, alebo email
 | 
			
		||||
        whole_word: Celé slovo
 | 
			
		||||
      featured_tag:
 | 
			
		||||
        name: Haštag
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,7 @@ defaults: &defaults
 | 
			
		|||
  flavour: 'glitch'
 | 
			
		||||
  skin: 'default'
 | 
			
		||||
  aggregate_reblogs: true
 | 
			
		||||
  advanced_layout: true
 | 
			
		||||
  notification_emails:
 | 
			
		||||
    follow: false
 | 
			
		||||
    reblog: false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ module Mastodon
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    def patch
 | 
			
		||||
      2
 | 
			
		||||
      4
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def pre
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue