diff options
author | Starfall <us@starfall.systems> | 2020-11-19 10:02:56 -0600 |
---|---|---|
committer | Starfall <us@starfall.systems> | 2020-11-19 10:02:56 -0600 |
commit | 383bb3804bdbd6caa442283cc96ef0cdbfdb4575 (patch) | |
tree | 54ecc09c325567ab1cfc4af9ba13ad134d9c3c72 /app/javascript/flavours | |
parent | 259470ec37dfc5c3d34ed5456adcd3ab1a622a18 (diff) | |
parent | db01f8b942b72eaa2eacbb144261b002f8079c9c (diff) |
Merge branch 'glitch' into main
Diffstat (limited to 'app/javascript/flavours')
33 files changed, 582 insertions, 108 deletions
diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index eb7087027..7273191b2 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -50,8 +50,9 @@ export const NOTIFICATIONS_SET_VISIBILITY = 'NOTIFICATIONS_SET_VISIBILITY'; export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; -export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; -export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; +export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; +export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; +export const NOTIFICATIONS_DISMISS_BROWSER_PERMISSION = 'NOTIFICATIONS_DISMISS_BROWSER_PERMISSION'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, @@ -371,3 +372,7 @@ export function setBrowserPermission (value) { value, }; } + +export const dismissBrowserPermission = () => ({ + type: NOTIFICATIONS_DISMISS_BROWSER_PERMISSION, +}); diff --git a/app/javascript/flavours/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.js index 5819d5362..c9da7d329 100644 --- a/app/javascript/flavours/glitch/components/column.js +++ b/app/javascript/flavours/glitch/components/column.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; import { scrollTop } from 'flavours/glitch/util/scroll'; export default class Column extends React.PureComponent { @@ -37,9 +37,9 @@ export default class Column extends React.PureComponent { componentDidMount () { if (this.props.bindToDocument) { - document.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } else { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } } diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index e627ea51f..d1aba691c 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -5,9 +5,9 @@ import IconButton from './icon_button'; import Overlay from 'react-overlays/lib/Overlay'; import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; let id = 0; class DropdownMenu extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 96042f07a..890a422d3 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -297,6 +297,7 @@ class MediaGallery extends React.PureComponent { this._setDimensions(); } }, 250, { + leading: true, trailing: true, }); diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index ac423c58d..7782246a6 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -55,7 +55,7 @@ const makeMapStateToProps = () => { let account = undefined; let prepend = undefined; - if (props.featured) { + if (props.featured && status) { account = status.get('account'); prepend = 'featured'; } else if (reblogStatus !== null && typeof reblogStatus === 'object') { diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index 781bd4e03..7457980d2 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -122,7 +122,7 @@ export default class MediaItem extends ImmutablePureComponent { <div className='media-gallery__gifv'> {content} - <span className='media-gallery__gifv__label'>{label}</span> + {label && <span className='media-gallery__gifv__label'>{label}</span>} </div> ); } diff --git a/app/javascript/flavours/glitch/features/audio/index.js b/app/javascript/flavours/glitch/features/audio/index.js index 6d09ac8d2..c050a63a9 100644 --- a/app/javascript/flavours/glitch/features/audio/index.js +++ b/app/javascript/flavours/glitch/features/audio/index.js @@ -252,7 +252,7 @@ class Audio extends React.PureComponent { handleTimeUpdate = () => { this.setState({ currentTime: this.audio.currentTime, - duration: Math.floor(this.audio.duration), + duration: this.audio.duration, }); } @@ -444,14 +444,14 @@ class Audio extends React.PureComponent { <div className='video-player__controls active'> <div className='video-player__buttons-bar'> <div className='video-player__buttons left'> - <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> - <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> + <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}> <div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} /> <span - className={classNames('video-player__volume__handle')} + className='video-player__volume__handle' tabIndex='0' style={{ left: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} /> @@ -460,12 +460,14 @@ class Audio extends React.PureComponent { <span className='video-player__time'> <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span> <span className='video-player__time-sep'>/</span> - <span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span> + <span className='video-player__time-total'>{formatTime(Math.floor(this.state.duration || this.props.duration))}</span> </span> </div> <div className='video-player__buttons right'> - <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} onClick={this.handleDownload}><Icon id='download' fixedWidth /></button> + <a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download> + <Icon id={'download'} fixedWidth /> + </a> </div> </div> </div> diff --git a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js index fa1ae8821..9c23d3f47 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js @@ -6,8 +6,14 @@ import { changeComposeSensitivity } from 'flavours/glitch/actions/compose'; import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; const messages = defineMessages({ - marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, - unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' }, + marked: { + id: 'compose_form.sensitive.marked', + defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}', + }, + unmarked: { + id: 'compose_form.sensitive.unmarked', + defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}', + }, }); const mapStateToProps = state => { @@ -16,6 +22,7 @@ const mapStateToProps = state => { return { active: state.getIn(['compose', 'sensitive']) || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0), disabled: state.getIn(['compose', 'spoiler']), + mediaCount: state.getIn(['compose', 'media_attachments']).size, }; }; @@ -32,16 +39,17 @@ class SensitiveButton extends React.PureComponent { static propTypes = { active: PropTypes.bool, disabled: PropTypes.bool, + mediaCount: PropTypes.number, onClick: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; render () { - const { active, disabled, onClick, intl } = this.props; + const { active, disabled, mediaCount, onClick, intl } = this.props; return ( <div className='compose-form__sensitive-button'> - <label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> + <label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked, { count: mediaCount })}> <input name='mark-sensitive' type='checkbox' @@ -52,7 +60,11 @@ class SensitiveButton extends React.PureComponent { <span className={classNames('checkbox', { active })} /> - <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> + <FormattedMessage + id='compose_form.sensitive.hide' + defaultMessage='{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}' + values={{ count: mediaCount }} + /> </label> </div> ); diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js index 89219d739..5fd904593 100644 --- a/app/javascript/flavours/glitch/features/emoji_picker/index.js +++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js @@ -10,7 +10,7 @@ import { EmojiPicker as EmojiPickerAsync } from 'flavours/glitch/util/async-comp import Overlay from 'react-overlays/lib/Overlay'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji'; import { useSystemEmojiFont } from 'flavours/glitch/util/initial_state'; import { assetHost } from 'flavours/glitch/util/config'; @@ -109,7 +109,7 @@ const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({ let EmojiPicker, Emoji; // load asynchronously const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; class ModifierPickerMenu extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js index 8e77f5a03..73fc05dea 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js @@ -1,25 +1,42 @@ import React from 'react'; import Icon from 'flavours/glitch/components/icon'; import Button from 'flavours/glitch/components/button'; -import { requestBrowserPermission } from 'flavours/glitch/actions/notifications'; +import IconButton from 'flavours/glitch/components/icon_button'; +import { requestBrowserPermission, dismissBrowserPermission } from 'flavours/glitch/actions/notifications'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -export default @connect(() => {}) +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +export default @connect() +@injectIntl class NotificationsPermissionBanner extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, }; handleClick = () => { this.props.dispatch(requestBrowserPermission()); } + handleClose = () => { + this.props.dispatch(dismissBrowserPermission()); + } + render () { + const { intl } = this.props; + return ( <div className='notifications-permission-banner'> + <div className='notifications-permission-banner__close'> + <IconButton icon='times' onClick={this.handleClose} title={intl.formatMessage(messages.close)} /> + </div> + <h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2> <p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' /> }} /></p> <Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button> diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.js index 24adcde25..28526ca88 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.js +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.js @@ -7,12 +7,18 @@ import IconButton from 'flavours/glitch/components/icon_button'; import { Link } from 'react-router-dom'; import Avatar from 'flavours/glitch/components/avatar'; import DisplayName from 'flavours/glitch/components/display_name'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); const mapStateToProps = (state, { accountId }) => ({ account: state.getIn(['accounts', accountId]), }); export default @connect(mapStateToProps) +@injectIntl class Header extends ImmutablePureComponent { static propTypes = { @@ -20,10 +26,11 @@ class Header extends ImmutablePureComponent { statusId: PropTypes.string.isRequired, account: ImmutablePropTypes.map.isRequired, onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, }; render () { - const { account, statusId, onClose } = this.props; + const { account, statusId, onClose, intl } = this.props; return ( <div className='picture-in-picture__header'> @@ -32,7 +39,7 @@ class Header extends ImmutablePureComponent { <DisplayName account={account} /> </Link> - <IconButton icon='times' onClick={onClose} title='Close' /> + <IconButton icon='times' onClick={onClose} title={intl.formatMessage(messages.close)} /> </div> ); } diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js index 8092e862f..12ad426c8 100644 --- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js @@ -78,9 +78,10 @@ class BoostModal extends ImmutablePureComponent { <div className={classNames('status', `status-${status.get('visibility')}`, 'light')}> <div className='boost-modal__status-header'> <div className='boost-modal__status-time'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'> + <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span> + <RelativeTimestamp timestamp={status.get('created_at')} /></a> </div> - <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span> <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'> <div className='status__avatar'> diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index 2de24bea5..729ade212 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -29,7 +29,7 @@ import Icon from 'flavours/glitch/components/icon'; import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; import { scrollRight } from 'flavours/glitch/util/scroll'; const componentMap = { @@ -80,7 +80,7 @@ class ColumnsArea extends ImmutablePureComponent { componentDidMount() { if (!this.props.singleColumn) { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } this.lastIndex = getIndex(this.context.router.history.location.pathname); @@ -97,7 +97,7 @@ class ColumnsArea extends ImmutablePureComponent { componentDidUpdate(prevProps) { if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } this.lastIndex = getIndex(this.context.router.history.location.pathname); this.setState({ shouldAnimate: true }); diff --git a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js index 176e7c487..ea1d7876e 100644 --- a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js @@ -7,11 +7,17 @@ import StatusContent from 'flavours/glitch/components/status_content'; import Avatar from 'flavours/glitch/components/avatar'; import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import DisplayName from 'flavours/glitch/components/display_name'; +import AttachmentList from 'flavours/glitch/components/attachment_list'; import Icon from 'flavours/glitch/components/icon'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import classNames from 'classnames'; const messages = defineMessages({ favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); export default @injectIntl @@ -54,13 +60,25 @@ class FavouriteModal extends ImmutablePureComponent { render () { const { status, intl } = this.props; + const visibilityIconInfo = { + 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, + 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, + 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, + 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) }, + }; + + const visibilityIcon = visibilityIconInfo[status.get('visibility')]; + return ( <div className='modal-root__modal favourite-modal'> <div className='favourite-modal__container'> - <div className='status light'> + <div className={classNames('status', `status-${status.get('visibility')}`, 'light')}> <div className='favourite-modal__status-header'> <div className='favourite-modal__status-time'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'> + <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span> + <RelativeTimestamp timestamp={status.get('created_at')} /> + </a> </div> <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'> @@ -69,10 +87,18 @@ class FavouriteModal extends ImmutablePureComponent { </div> <DisplayName account={status.get('account')} /> + </a> </div> <StatusContent status={status} /> + + {status.get('media_attachments').size > 0 && ( + <AttachmentList + compact + media={status.get('media_attachments')} + /> + )} </div> </div> diff --git a/app/javascript/flavours/glitch/features/ui/components/image_loader.js b/app/javascript/flavours/glitch/features/ui/components/image_loader.js index 5e1cf75af..c6f16a792 100644 --- a/app/javascript/flavours/glitch/features/ui/components/image_loader.js +++ b/app/javascript/flavours/glitch/features/ui/components/image_loader.js @@ -13,6 +13,7 @@ export default class ImageLoader extends React.PureComponent { width: PropTypes.number, height: PropTypes.number, onClick: PropTypes.func, + zoomButtonHidden: PropTypes.bool, } static defaultProps = { @@ -151,6 +152,9 @@ export default class ImageLoader extends React.PureComponent { alt={alt} src={src} onClick={onClick} + width={width} + height={height} + zoomButtonHidden={this.props.zoomButtonHidden} /> )} </div> diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index aa6554107..e37df7208 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -35,23 +35,39 @@ class MediaModal extends ImmutablePureComponent { state = { index: null, navigationHidden: false, + zoomButtonHidden: false, }; handleSwipe = (index) => { this.setState({ index: index % this.props.media.size }); } + handleTransitionEnd = () => { + this.setState({ + zoomButtonHidden: false, + }); + } + handleNextClick = () => { - this.setState({ index: (this.getIndex() + 1) % this.props.media.size }); + this.setState({ + index: (this.getIndex() + 1) % this.props.media.size, + zoomButtonHidden: true, + }); } handlePrevClick = () => { - this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size }); + this.setState({ + index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size, + zoomButtonHidden: true, + }); } handleChangeIndex = (e) => { const index = Number(e.currentTarget.getAttribute('data-index')); - this.setState({ index: index % this.props.media.size }); + this.setState({ + index: index % this.props.media.size, + zoomButtonHidden: true, + }); } handleKeyDown = (e) => { @@ -128,6 +144,7 @@ class MediaModal extends ImmutablePureComponent { alt={image.get('description')} key={image.get('url')} onClick={this.toggleNavigation} + zoomButtonHidden={this.state.zoomButtonHidden} /> ); } else if (image.get('type') === 'video') { @@ -191,6 +208,7 @@ class MediaModal extends ImmutablePureComponent { style={swipeableViewsStyle} containerStyle={containerStyle} onChangeIndex={this.handleSwipe} + onTransitionEnd={this.handleTransitionEnd} index={index} > {content} diff --git a/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js b/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js index 3f6562bc9..caeeced64 100644 --- a/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js +++ b/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js @@ -1,8 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; +import IconButton from 'flavours/glitch/components/icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + compress: { id: 'lightbox.compress', defaultMessage: 'Compress image view box' }, + expand: { id: 'lightbox.expand', defaultMessage: 'Expand image view box' }, +}); const MIN_SCALE = 1; const MAX_SCALE = 4; +const NAV_BAR_HEIGHT = 66; const getMidpoint = (p1, p2) => ({ x: (p1.clientX + p2.clientX) / 2, @@ -14,7 +22,77 @@ const getDistance = (p1, p2) => const clamp = (min, max, value) => Math.min(max, Math.max(min, value)); -export default class ZoomableImage extends React.PureComponent { +// Normalizing mousewheel speed across browsers +// copy from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js +const normalizeWheel = event => { + // Reasonable defaults + const PIXEL_STEP = 10; + const LINE_HEIGHT = 40; + const PAGE_HEIGHT = 800; + + let sX = 0, + sY = 0, // spinX, spinY + pX = 0, + pY = 0; // pixelX, pixelY + + // Legacy + if ('detail' in event) { + sY = event.detail; + } + if ('wheelDelta' in event) { + sY = -event.wheelDelta / 120; + } + if ('wheelDeltaY' in event) { + sY = -event.wheelDeltaY / 120; + } + if ('wheelDeltaX' in event) { + sX = -event.wheelDeltaX / 120; + } + + // side scrolling on FF with DOMMouseScroll + if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) { + sX = sY; + sY = 0; + } + + pX = sX * PIXEL_STEP; + pY = sY * PIXEL_STEP; + + if ('deltaY' in event) { + pY = event.deltaY; + } + if ('deltaX' in event) { + pX = event.deltaX; + } + + if ((pX || pY) && event.deltaMode) { + if (event.deltaMode === 1) { // delta in LINE units + pX *= LINE_HEIGHT; + pY *= LINE_HEIGHT; + } else { // delta in PAGE units + pX *= PAGE_HEIGHT; + pY *= PAGE_HEIGHT; + } + } + + // Fall-back if spin cannot be determined + if (pX && !sX) { + sX = (pX < 1) ? -1 : 1; + } + if (pY && !sY) { + sY = (pY < 1) ? -1 : 1; + } + + return { + spinX: sX, + spinY: sY, + pixelX: pX, + pixelY: pY, + }; +}; + +export default @injectIntl +class ZoomableImage extends React.PureComponent { static propTypes = { alt: PropTypes.string, @@ -22,6 +100,8 @@ export default class ZoomableImage extends React.PureComponent { width: PropTypes.number, height: PropTypes.number, onClick: PropTypes.func, + zoomButtonHidden: PropTypes.bool, + intl: PropTypes.object.isRequired, } static defaultProps = { @@ -32,6 +112,26 @@ export default class ZoomableImage extends React.PureComponent { state = { scale: MIN_SCALE, + zoomMatrix: { + type: null, // 'width' 'height' + fullScreen: null, // bool + rate: null, // full screen scale rate + clientWidth: null, + clientHeight: null, + offsetWidth: null, + offsetHeight: null, + clientHeightFixed: null, + scrollTop: null, + scrollLeft: null, + translateX: null, + translateY: null, + }, + zoomState: 'expand', // 'expand' 'compress' + navigationHidden: false, + dragPosition: { top: 0, left: 0, x: 0, y: 0 }, + dragged: false, + lockScroll: { x: 0, y: 0 }, + lockTranslate: { x: 0, y: 0 }, } removers = []; @@ -49,17 +149,105 @@ export default class ZoomableImage extends React.PureComponent { // https://www.chromestatus.com/features/5093566007214080 this.container.addEventListener('touchmove', handler, { passive: false }); this.removers.push(() => this.container.removeEventListener('touchend', handler)); + + handler = this.mouseDownHandler; + this.container.addEventListener('mousedown', handler); + this.removers.push(() => this.container.removeEventListener('mousedown', handler)); + + handler = this.mouseWheelHandler; + this.container.addEventListener('wheel', handler); + this.removers.push(() => this.container.removeEventListener('wheel', handler)); + // Old Chrome + this.container.addEventListener('mousewheel', handler); + this.removers.push(() => this.container.removeEventListener('mousewheel', handler)); + // Old Firefox + this.container.addEventListener('DOMMouseScroll', handler); + this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler)); + + this.initZoomMatrix(); } componentWillUnmount () { this.removeEventListeners(); } + componentDidUpdate () { + this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' }); + + if (this.state.scale === MIN_SCALE) { + this.container.style.removeProperty('cursor'); + } + } + + UNSAFE_componentWillReceiveProps () { + // reset when slide to next image + if (this.props.zoomButtonHidden) { + this.setState({ + scale: MIN_SCALE, + lockTranslate: { x: 0, y: 0 }, + }, () => { + this.container.scrollLeft = 0; + this.container.scrollTop = 0; + }); + } + } + removeEventListeners () { this.removers.forEach(listeners => listeners()); this.removers = []; } + mouseWheelHandler = e => { + e.preventDefault(); + + const event = normalizeWheel(e); + + if (this.state.zoomMatrix.type === 'width') { + // full width, scroll vertical + this.container.scrollTop = Math.max(this.container.scrollTop + event.pixelY, this.state.lockScroll.y); + } else { + // full height, scroll horizontal + this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelY, this.state.lockScroll.x); + } + + // lock horizontal scroll + this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelX, this.state.lockScroll.x); + } + + mouseDownHandler = e => { + this.container.style.cursor = 'grabbing'; + this.container.style.userSelect = 'none'; + + this.setState({ dragPosition: { + left: this.container.scrollLeft, + top: this.container.scrollTop, + // Get the current mouse position + x: e.clientX, + y: e.clientY, + } }); + + this.image.addEventListener('mousemove', this.mouseMoveHandler); + this.image.addEventListener('mouseup', this.mouseUpHandler); + } + + mouseMoveHandler = e => { + const dx = e.clientX - this.state.dragPosition.x; + const dy = e.clientY - this.state.dragPosition.y; + + this.container.scrollLeft = Math.max(this.state.dragPosition.left - dx, this.state.lockScroll.x); + this.container.scrollTop = Math.max(this.state.dragPosition.top - dy, this.state.lockScroll.y); + + this.setState({ dragged: true }); + } + + mouseUpHandler = () => { + this.container.style.cursor = 'grab'; + this.container.style.removeProperty('user-select'); + + this.image.removeEventListener('mousemove', this.mouseMoveHandler); + this.image.removeEventListener('mouseup', this.mouseUpHandler); + } + handleTouchStart = e => { if (e.touches.length !== 2) return; @@ -80,7 +268,8 @@ export default class ZoomableImage extends React.PureComponent { const distance = getDistance(...e.touches); const midpoint = getMidpoint(...e.touches); - const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance); + const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate); + const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance); this.zoom(scale, midpoint); @@ -89,7 +278,7 @@ export default class ZoomableImage extends React.PureComponent { } zoom(nextScale, midpoint) { - const { scale } = this.state; + const { scale, zoomMatrix } = this.state; const { scrollLeft, scrollTop } = this.container; // math memo: @@ -104,14 +293,105 @@ export default class ZoomableImage extends React.PureComponent { this.setState({ scale: nextScale }, () => { this.container.scrollLeft = nextScrollLeft; this.container.scrollTop = nextScrollTop; + // reset the translateX/Y constantly + if (nextScale < zoomMatrix.rate) { + this.setState({ + lockTranslate: { + x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)), + y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)), + }, + }); + } }); } handleClick = e => { // don't propagate event to MediaModal e.stopPropagation(); + const dragged = this.state.dragged; + this.setState({ dragged: false }); + if (dragged) return; const handler = this.props.onClick; if (handler) handler(); + this.setState({ navigationHidden: !this.state.navigationHidden }); + } + + handleMouseDown = e => { + e.preventDefault(); + } + + initZoomMatrix = () => { + const { width, height } = this.props; + const { clientWidth, clientHeight } = this.container; + const { offsetWidth, offsetHeight } = this.image; + const clientHeightFixed = clientHeight - NAV_BAR_HEIGHT; + + const type = width / height < clientWidth / clientHeightFixed ? 'width' : 'height'; + const fullScreen = type === 'width' ? width > clientWidth : height > clientHeightFixed; + const rate = type === 'width' ? Math.min(clientWidth, width) / offsetWidth : Math.min(clientHeightFixed, height) / offsetHeight; + const scrollTop = type === 'width' ? (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2; + const scrollLeft = (clientWidth - offsetWidth) / 2; + const translateX = type === 'width' ? (width - offsetWidth) / (2 * rate) : 0; + const translateY = type === 'height' ? (height - offsetHeight) / (2 * rate) : 0; + + this.setState({ + zoomMatrix: { + type: type, + fullScreen: fullScreen, + rate: rate, + clientWidth: clientWidth, + clientHeight: clientHeight, + offsetWidth: offsetWidth, + offsetHeight: offsetHeight, + clientHeightFixed: clientHeightFixed, + scrollTop: scrollTop, + scrollLeft: scrollLeft, + translateX: translateX, + translateY: translateY, + }, + }); + } + + handleZoomClick = e => { + e.preventDefault(); + e.stopPropagation(); + + const { scale, zoomMatrix } = this.state; + + if ( scale >= zoomMatrix.rate ) { + this.setState({ + scale: MIN_SCALE, + lockScroll: { + x: 0, + y: 0, + }, + lockTranslate: { + x: 0, + y: 0, + }, + }, () => { + this.container.scrollLeft = 0; + this.container.scrollTop = 0; + }); + } else { + this.setState({ + scale: zoomMatrix.rate, + lockScroll: { + x: zoomMatrix.scrollLeft, + y: zoomMatrix.scrollTop, + }, + lockTranslate: { + x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX, + y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY, + }, + }, () => { + this.container.scrollLeft = zoomMatrix.scrollLeft; + this.container.scrollTop = zoomMatrix.scrollTop; + }); + } + + this.container.style.cursor = 'grab'; + this.container.style.removeProperty('user-select'); } setContainerRef = c => { @@ -123,29 +403,47 @@ export default class ZoomableImage extends React.PureComponent { } render () { - const { alt, src } = this.props; - const { scale } = this.state; - const overflow = scale === 1 ? 'hidden' : 'scroll'; + const { alt, src, width, height, intl } = this.props; + const { scale, lockTranslate } = this.state; + const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll'; + const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : ''; + const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand); return ( - <div - className='zoomable-image' - ref={this.setContainerRef} - style={{ overflow }} - > - <img - role='presentation' - ref={this.setImageRef} - alt={alt} - title={alt} - src={src} + <React.Fragment> + <IconButton + className={`media-modal__zoom-button ${zoomButtonShouldHide}`} + title={zoomButtonTitle} + icon={this.state.zoomState} + onClick={this.handleZoomClick} + size={40} style={{ - transform: `scale(${scale})`, - transformOrigin: '0 0', + fontSize: '30px', /* Fontawesome's fa-compress fa-expand is larger than fa-close */ }} - onClick={this.handleClick} /> - </div> + <div + className='zoomable-image' + ref={this.setContainerRef} + style={{ overflow }} + > + <img + role='presentation' + ref={this.setImageRef} + alt={alt} + title={alt} + src={src} + width={width} + height={height} + style={{ + transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`, + transformOrigin: '0 0', + }} + draggable={false} + onClick={this.handleClick} + onMouseDown={this.handleMouseDown} + /> + </div> + </React.Fragment> ); } diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index 95bee1331..870812856 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -211,7 +211,7 @@ class Video extends React.PureComponent { handleTimeUpdate = () => { this.setState({ currentTime: this.video.currentTime, - duration: Math.floor(this.video.duration), + duration:this.video.duration, }); } @@ -281,9 +281,9 @@ class Video extends React.PureComponent { togglePlay = () => { if (this.state.paused) { - this.video.play(); + this.setState({ paused: false }, () => this.video.play()); } else { - this.video.pause(); + this.setState({ paused: true }, () => this.video.pause()); } } @@ -381,13 +381,16 @@ class Video extends React.PureComponent { } toggleMute = () => { - this.video.muted = !this.video.muted; - this.setState({ muted: this.video.muted }); + const muted = !this.video.muted; + + this.setState({ muted }, () => { + this.video.muted = muted; + }); } toggleReveal = () => { if (this.state.revealed) { - this.video.pause(); + this.setState({ paused: true }); } if (this.props.onToggleVisibility) { @@ -475,13 +478,6 @@ class Video extends React.PureComponent { return (<div className={computedClass} ref={this.setPlayerRef} tabindex={0}></div>); } - let warning; - if (sensitive) { - warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; - } else { - warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; - } - let preload; if (this.props.currentTime || fullscreen || dragging) { @@ -492,6 +488,14 @@ class Video extends React.PureComponent { preload = 'none'; } + let warning; + + if (sensitive) { + warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; + } else { + warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; + } + return ( <div className={computedClass} @@ -551,8 +555,8 @@ class Video extends React.PureComponent { <div className='video-player__buttons-bar'> <div className='video-player__buttons left'> - <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> - <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> + <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} /> @@ -568,7 +572,7 @@ class Video extends React.PureComponent { <span className='video-player__time'> <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span> <span className='video-player__time-sep'>/</span> - <span className='video-player__time-total'>{formatTime(duration)}</span> + <span className='video-player__time-total'>{formatTime(Math.floor(duration))}</span> </span> )} @@ -576,10 +580,10 @@ class Video extends React.PureComponent { </div> <div className='video-player__buttons right'> - {(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} - {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} - {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} - <button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> + {(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} + {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} className='player-button' onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} + {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} className='player-button' onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} + <button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} className='player-button' onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> </div> </div> </div> diff --git a/app/javascript/flavours/glitch/locales/ku.js b/app/javascript/flavours/glitch/locales/ku.js new file mode 100644 index 000000000..19e0e95aa --- /dev/null +++ b/app/javascript/flavours/glitch/locales/ku.js @@ -0,0 +1,7 @@ +import inherited from 'mastodon/locales/ku.json'; + +const messages = { + // No translations available. +}; + +export default Object.assign({}, inherited, messages); diff --git a/app/javascript/flavours/glitch/locales/sa.js b/app/javascript/flavours/glitch/locales/sa.js new file mode 100644 index 000000000..4cade0a07 --- /dev/null +++ b/app/javascript/flavours/glitch/locales/sa.js @@ -0,0 +1,7 @@ +import inherited from 'mastodon/locales/sa.json'; + +const messages = { + // No translations available. +}; + +export default Object.assign({}, inherited, messages); diff --git a/app/javascript/flavours/glitch/locales/sc.js b/app/javascript/flavours/glitch/locales/sc.js new file mode 100644 index 000000000..88a83aa53 --- /dev/null +++ b/app/javascript/flavours/glitch/locales/sc.js @@ -0,0 +1,7 @@ +import inherited from 'mastodon/locales/sc.json'; + +const messages = { + // No translations available. +}; + +export default Object.assign({}, inherited, messages); diff --git a/app/javascript/flavours/glitch/locales/zgh.js b/app/javascript/flavours/glitch/locales/zgh.js new file mode 100644 index 000000000..f2f15b1a4 --- /dev/null +++ b/app/javascript/flavours/glitch/locales/zgh.js @@ -0,0 +1,7 @@ +import inherited from 'mastodon/locales/zgh.json'; + +const messages = { + // No translations available. +}; + +export default Object.assign({}, inherited, messages); diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index b4c5ef71a..c630cc7e3 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -19,6 +19,7 @@ import { NOTIFICATIONS_MARK_AS_READ, NOTIFICATIONS_SET_BROWSER_SUPPORT, NOTIFICATIONS_SET_BROWSER_PERMISSION, + NOTIFICATIONS_DISMISS_BROWSER_PERMISSION, } from 'flavours/glitch/actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS, @@ -283,6 +284,8 @@ export default function notifications(state = initialState, action) { return state.set('browserSupport', action.value); case NOTIFICATIONS_SET_BROWSER_PERMISSION: return state.set('browserPermission', action.value); + case NOTIFICATIONS_DISMISS_BROWSER_PERMISSION: + return state.set('browserPermission', 'denied'); case NOTIFICATION_MARK_FOR_DELETE: return markForDelete(state, action.id, action.yes); diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index be32ae52e..2550f50f4 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -702,6 +702,13 @@ flex-direction: column; align-items: center; justify-content: center; + position: relative; + + &__close { + position: absolute; + top: 10px; + right: 10px; + } h2 { font-size: 16px; diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 0614278e2..3e67754c5 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -425,6 +425,20 @@ align-items: center; justify-content: center; flex-direction: column; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE 10+ */ + + * { + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE 10+ */ + } + + &::-webkit-scrollbar, + *::-webkit-scrollbar { + width: 0; + height: 0; + background: transparent; /* Chrome/Safari/Webkit */ + } .image-loader__preview-canvas { max-width: $media-modal-media-max-width; diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index 58bdb642f..f504fa91e 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -385,7 +385,6 @@ } .video-player__controls { - padding: 0 15px; padding-top: 10px; background: transparent; } @@ -512,7 +511,8 @@ &__buttons-bar { display: flex; justify-content: space-between; - padding-bottom: 10px; + padding-bottom: 8px; + margin: 0 -5px; .video-player__download__icon { color: inherit; @@ -536,22 +536,13 @@ overflow: hidden; text-overflow: ellipsis; - &.left { - button { - padding-left: 0; - } - } - - &.right { - button { - padding-right: 0; - } - } + .player-button { + display: inline-block; + outline: 0; - button { flex: 0 0 auto; background: transparent; - padding: 2px 10px; + padding: 5px; font-size: 16px; border: 0; color: rgba($white, 0.75); @@ -569,6 +560,7 @@ flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; + margin: 0 5px; } &__time-sep, @@ -688,7 +680,7 @@ display: block; position: absolute; height: 4px; - top: 10px; + top: 14px; } &__progress, @@ -697,7 +689,7 @@ position: absolute; height: 4px; border-radius: 4px; - top: 10px; + top: 14px; background: lighten($ui-highlight-color, 8%); } @@ -712,7 +704,7 @@ border-radius: 50%; width: 12px; height: 12px; - top: 6px; + top: 10px; margin-left: -6px; background: lighten($ui-highlight-color, 8%); box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2); @@ -736,7 +728,7 @@ &.detailed, &.fullscreen { .video-player__buttons { - button { + .player-button { padding-top: 10px; padding-bottom: 10px; } diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index 85f216887..bc0965864 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -40,6 +40,21 @@ z-index: 9999; } +.media-modal__zoom-button { + position: absolute; + right: 64px; + top: 8px; + z-index: 100; + pointer-events: auto; + transition: opacity 0.3s linear; + will-change: opacity; +} + +.media-modal__zoom-button--hidden { + pointer-events: none; + opacity: 0; +} + .onboarding-modal, .error-modal, .embed-modal { @@ -427,10 +442,8 @@ .status__visibility-icon { color: $dark-text-color; - float: right; font-size: 14px; - margin-left: 4px; - margin-right: 4px; + padding: 0 4px; } .status__display-name { diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 03acd72d8..7ca1ae423 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -55,6 +55,7 @@ font-weight: 400; overflow: visible; padding-top: 5px; + clear: both; &:focus { outline: 0; diff --git a/app/javascript/flavours/glitch/styles/statuses.scss b/app/javascript/flavours/glitch/styles/statuses.scss index 982a1ab9a..58f74f954 100644 --- a/app/javascript/flavours/glitch/styles/statuses.scss +++ b/app/javascript/flavours/glitch/styles/statuses.scss @@ -201,19 +201,18 @@ display: initial; } - .status__relative-time, - .status__visibility-icon { + .status__relative-time { color: $dark-text-color; float: right; font-size: 14px; width: auto; margin: initial; padding: initial; + padding-bottom: 1px; } .status__visibility-icon { - margin-left: 4px; - margin-right: 4px; + padding: 0 4px; } .status__info .status__display-name { diff --git a/app/javascript/flavours/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss index 1ed1a5778..6e242281b 100644 --- a/app/javascript/flavours/glitch/styles/variables.scss +++ b/app/javascript/flavours/glitch/styles/variables.scss @@ -36,6 +36,8 @@ $dark-text-color: $ui-base-lighter-color !default; $secondary-text-color: $ui-secondary-color !default; $highlight-text-color: $ui-highlight-color !default; $action-button-color: $ui-base-lighter-color !default; +$passive-text-color: $gold-star !default; +$active-passive-text-color: $success-green !default; // For texts on inverted backgrounds $inverted-text-color: $ui-base-color !default; $lighter-text-color: $ui-base-lighter-color !default; diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss index 531425573..b397844a2 100644 --- a/app/javascript/flavours/glitch/styles/widgets.scss +++ b/app/javascript/flavours/glitch/styles/widgets.scss @@ -437,6 +437,26 @@ vertical-align: initial !important; } + &__interrelationships { + width: 21px; + } + + .fa { + font-size: 16px; + + &.active { + color: $highlight-text-color; + } + + &.passive { + color: $passive-text-color; + } + + &.active.passive { + color: $active-passive-text-color; + } + } + @media screen and (max-width: $no-gap-breakpoint) { tbody td.optional { display: none; diff --git a/app/javascript/flavours/glitch/util/dom_helpers.js b/app/javascript/flavours/glitch/util/dom_helpers.js index 3e1f4a26d..d94aeb9d4 100644 --- a/app/javascript/flavours/glitch/util/dom_helpers.js +++ b/app/javascript/flavours/glitch/util/dom_helpers.js @@ -1,9 +1,9 @@ // Package imports. -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; // This will either be a passive lister options object (if passive // events are supported), or `false`. -export const withPassive = detectPassiveEvents.hasSupport ? { passive: true } : false; +export const withPassive = supportsPassiveEvents ? { passive: true } : false; // Focuses the root element. export function focusRoot () { diff --git a/app/javascript/flavours/glitch/util/is_mobile.js b/app/javascript/flavours/glitch/util/is_mobile.js index db3c8bd80..7e584e8fa 100644 --- a/app/javascript/flavours/glitch/util/is_mobile.js +++ b/app/javascript/flavours/glitch/util/is_mobile.js @@ -1,4 +1,4 @@ -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; import { forceSingleColumn } from 'flavours/glitch/util/initial_state'; const LAYOUT_BREAKPOINT = 630; @@ -17,7 +17,7 @@ export function isMobile(width, columns) { const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; let userTouching = false; -let listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; +let listenerOptions = supportsPassiveEvents ? { passive: true } : false; function touchListener() { userTouching = true; |