diff --git a/app/javascript/flavours/glitch/actions/app.js b/app/javascript/flavours/glitch/actions/app.js new file mode 100644 index 000000000..de2d93e29 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/app.js @@ -0,0 +1,6 @@ +export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE'; + +export const changeLayout = layout => ({ + type: APP_LAYOUT_CHANGE, + layout, +}); diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index e238456c5..d3b6bd7e9 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -99,8 +99,11 @@ class Status extends ImmutablePureComponent { onClick: PropTypes.func, scrollKey: PropTypes.string, deployPictureInPicture: PropTypes.func, - usingPiP: PropTypes.bool, settings: ImmutablePropTypes.map.isRequired, + pictureInPicture: PropTypes.shape({ + inUse: PropTypes.bool, + available: PropTypes.bool, + }), }; state = { @@ -126,7 +129,7 @@ class Status extends ImmutablePureComponent { 'hidden', 'expanded', 'unread', - 'usingPiP', + 'pictureInPicture', ] updateOnStates = [ @@ -503,7 +506,7 @@ class Status extends ImmutablePureComponent { hidden, unread, featured, - usingPiP, + pictureInPicture, ...other } = this.props; const { isCollapsed, forceFilter } = this.state; @@ -595,7 +598,7 @@ class Status extends ImmutablePureComponent { attachments = status.get('media_attachments'); - if (usingPiP) { + if (pictureInPicture.inUse) { media.push(); mediaIcons.push('video-camera'); } else if (attachments.size > 0) { @@ -623,7 +626,7 @@ class Status extends ImmutablePureComponent { width={this.props.cachedMediaWidth} height={110} cacheWidth={this.props.cacheMediaWidth} - deployPictureInPicture={this.handleDeployPictureInPicture} + deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} sensitive={status.get('sensitive')} blurhash={attachment.get('blurhash')} visible={this.state.showMedia} @@ -652,7 +655,7 @@ class Status extends ImmutablePureComponent { onOpenVideo={this.handleOpenVideo} width={this.props.cachedMediaWidth} cacheWidth={this.props.cacheMediaWidth} - deployPictureInPicture={this.handleDeployPictureInPicture} + deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} />)} diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 0ba2e712c..370308043 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -76,12 +76,16 @@ const makeMapStateToProps = () => { } return { - containerId : props.containerId || props.id, // Should match reblogStatus's id for reblogs - status : status, - account : account || props.account, - settings : state.get('local_settings'), - prepend : prepend || props.prepend, - usingPiP : state.get('picture_in_picture').statusId === props.id, + containerId: props.containerId || props.id, // Should match reblogStatus's id for reblogs + status: status, + account: account || props.account, + settings: state.get('local_settings'), + prepend: prepend || props.prepend, + + pictureInPicture: { + inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id, + available: state.getIn(['meta', 'layout']) !== 'mobile', + }, }; }; diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 2be6d9478..c861f5568 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -5,13 +5,14 @@ import LoadingBarContainer from './containers/loading_bar_container'; import ModalContainer from './containers/modal_container'; import { connect } from 'react-redux'; import { Redirect, withRouter } from 'react-router-dom'; -import { isMobile } from 'flavours/glitch/util/is_mobile'; +import { layoutFromWindow } from 'flavours/glitch/util/is_mobile'; import { debounce } from 'lodash'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose'; import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications'; import { fetchRules } from 'flavours/glitch/actions/rules'; import { clearHeight } from 'flavours/glitch/actions/height_cache'; +import { changeLayout } from 'flavours/glitch/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers'; import UploadArea from './components/upload_area'; @@ -66,10 +67,12 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ + layout: state.getIn(['meta', 'layout']), hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4, - layout: state.getIn(['local_settings', 'layout']), + layout: state.getIn(['meta', 'layout']), + layout_local_setting: state.getIn(['local_settings', 'layout']), isWide: state.getIn(['local_settings', 'stretch']), navbarUnder: state.getIn(['local_settings', 'navbar_under']), dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, @@ -120,26 +123,13 @@ class SwitchingColumnsArea extends React.PureComponent { static propTypes = { children: PropTypes.node, - layout: PropTypes.string, location: PropTypes.object, navbarUnder: PropTypes.bool, - onLayoutChange: PropTypes.func.isRequired, + mobile: PropTypes.bool, }; - state = { - mobile: isMobile(window.innerWidth, this.props.layout), - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.layout !== this.props.layout) { - this.setState({ mobile: isMobile(window.innerWidth, nextProps.layout) }); - } - } - componentWillMount () { - window.addEventListener('resize', this.handleResize, { passive: true }); - - if (this.state.mobile) { + if (this.props.mobile) { document.body.classList.toggle('layout-single-column', true); document.body.classList.toggle('layout-multiple-columns', false); } else { @@ -148,37 +138,14 @@ class SwitchingColumnsArea extends React.PureComponent { } } - componentDidUpdate (prevProps, prevState) { + componentDidUpdate (prevProps) { if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { this.node.handleChildrenContentChange(); } - if (prevState.mobile !== this.state.mobile) { - document.body.classList.toggle('layout-single-column', this.state.mobile); - document.body.classList.toggle('layout-multiple-columns', !this.state.mobile); - } - } - - componentWillUnmount () { - window.removeEventListener('resize', this.handleResize); - } - - handleLayoutChange = debounce(() => { - // The cached heights are no longer accurate, invalidate - this.props.onLayoutChange(); - }, 500, { - trailing: true, - }) - - handleResize = () => { - const mobile = isMobile(window.innerWidth, this.props.layout); - - if (mobile !== this.state.mobile) { - this.handleLayoutChange.cancel(); - this.props.onLayoutChange(); - this.setState({ mobile }); - } else { - this.handleLayoutChange(); + if (prevProps.mobile !== this.props.mobile) { + document.body.classList.toggle('layout-single-column', this.props.mobile); + document.body.classList.toggle('layout-multiple-columns', !this.props.mobile); } } @@ -189,12 +156,11 @@ class SwitchingColumnsArea extends React.PureComponent { } render () { - const { children, navbarUnder } = this.props; - const singleColumn = this.state.mobile; - const redirect = singleColumn ? : ; + const { children, mobile, navbarUnder } = this.props; + const redirect = mobile ? : ; return ( - + {redirect} @@ -256,7 +222,7 @@ class UI extends React.Component { static propTypes = { dispatch: PropTypes.func.isRequired, children: PropTypes.node, - layout: PropTypes.string, + layout_local_setting: PropTypes.string, isWide: PropTypes.bool, systemFontUi: PropTypes.bool, navbarUnder: PropTypes.bool, @@ -272,6 +238,7 @@ class UI extends React.Component { unreadNotifications: PropTypes.number, showFaviconBadge: PropTypes.bool, moved: PropTypes.map, + layout: PropTypes.string.isRequired, firstLaunch: PropTypes.bool, username: PropTypes.string, }; @@ -293,11 +260,6 @@ class UI extends React.Component { } } - handleLayoutChange = () => { - // The cached heights are no longer accurate, invalidate - this.props.dispatch(clearHeight()); - } - handleDragEnter = (e) => { e.preventDefault(); @@ -378,8 +340,27 @@ class UI extends React.Component { } } - componentWillMount () { + handleLayoutChange = debounce(() => { + this.props.dispatch(clearHeight()); // The cached heights are no longer accurate, invalidate + }, 500, { + trailing: true, + }); + + handleResize = () => { + const layout = layoutFromWindow(this.props.layout_local_setting); + + if (layout !== this.props.layout) { + this.handleLayoutChange.cancel(); + this.props.dispatch(changeLayout(layout)); + } else { + this.handleLayoutChange(); + } + } + + componentDidMount () { window.addEventListener('beforeunload', this.handleBeforeUnload, false); + window.addEventListener('resize', this.handleResize, { passive: true }); + document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('drop', this.handleDrop, false); @@ -403,9 +384,7 @@ class UI extends React.Component { this.props.dispatch(expandNotifications()); setTimeout(() => this.props.dispatch(fetchRules()), 3000); - } - componentDidMount () { this.hotkeys.__mousetrap__.stopCallback = (e, element) => { return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); }; @@ -427,6 +406,19 @@ class UI extends React.Component { } } + componentWillReceiveProps (nextProps) { + if (nextProps.layout_local_setting !== this.props.layout_local_setting) { + const layout = layoutFromWindow(nextProps.layout_local_setting); + + if (layout !== this.props.layout) { + this.handleLayoutChange.cancel(); + this.props.dispatch(changeLayout(layout)); + } else { + this.handleLayoutChange(); + } + } + } + componentDidUpdate (prevProps) { if (this.props.unreadNotifications != prevProps.unreadNotifications || this.props.showFaviconBadge != prevProps.showFaviconBadge) { @@ -446,6 +438,8 @@ class UI extends React.Component { } window.removeEventListener('beforeunload', this.handleBeforeUnload); + window.removeEventListener('resize', this.handleResize); + document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver); document.removeEventListener('drop', this.handleDrop); @@ -576,7 +570,7 @@ class UI extends React.Component { render () { const { draggingOver } = this.state; - const { children, layout, isWide, navbarUnder, location, dropdownMenuIsOpen, moved } = this.props; + const { children, isWide, navbarUnder, location, dropdownMenuIsOpen, layout, moved } = this.props; const columnsClass = layout => { switch (layout) { @@ -632,11 +626,11 @@ class UI extends React.Component { )}} /> )} - + {children} - + {layout !== 'mobile' && } diff --git a/app/javascript/flavours/glitch/reducers/meta.js b/app/javascript/flavours/glitch/reducers/meta.js index 0f3ab3b84..0364ec289 100644 --- a/app/javascript/flavours/glitch/reducers/meta.js +++ b/app/javascript/flavours/glitch/reducers/meta.js @@ -1,16 +1,25 @@ import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; +import { APP_LAYOUT_CHANGE } from 'flavours/glitch/actions/app'; import { Map as ImmutableMap } from 'immutable'; +import { layoutFromWindow } from 'flavours/glitch/util/is_mobile'; const initialState = ImmutableMap({ streaming_api_base_url: null, access_token: null, + layout: layoutFromWindow(), permissions: '0', }); export default function meta(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: - return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions'])); + return state.merge( + action.state.get('meta')) + .set('permissions', action.state.getIn(['role', 'permissions'])) + .set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout'])) + ); + case APP_LAYOUT_CHANGE: + return state.set('layout', action.layout); default: return state; } diff --git a/app/javascript/flavours/glitch/util/is_mobile.js b/app/javascript/flavours/glitch/util/is_mobile.js index 7e584e8fa..c8517f592 100644 --- a/app/javascript/flavours/glitch/util/is_mobile.js +++ b/app/javascript/flavours/glitch/util/is_mobile.js @@ -3,14 +3,26 @@ import { forceSingleColumn } from 'flavours/glitch/util/initial_state'; const LAYOUT_BREAKPOINT = 630; -export function isMobile(width, columns) { - switch (columns) { +export const isMobile = width => width <= LAYOUT_BREAKPOINT; + +export const layoutFromWindow = (layout_local_setting) => { + switch (layout_local_setting) { case 'multiple': - return false; + return 'multi-column'; case 'single': - return true; + if (isMobile(window.innerWidth)) { + return 'mobile'; + } else { + return 'single-column'; + } default: - return forceSingleColumn || width <= LAYOUT_BREAKPOINT; + if (isMobile(window.innerWidth)) { + return 'mobile'; + } else if (forceSingleColumn) { + return 'single-column'; + } else { + return 'multi-column'; + } } }; @@ -19,17 +31,13 @@ const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; let userTouching = false; let listenerOptions = supportsPassiveEvents ? { passive: true } : false; -function touchListener() { +const touchListener = () => { userTouching = true; window.removeEventListener('touchstart', touchListener, listenerOptions); -} +}; window.addEventListener('touchstart', touchListener, listenerOptions); -export function isUserTouching() { - return userTouching; -} +export const isUserTouching = () => userTouching; -export function isIOS() { - return iOS; -}; +export const isIOS = () => iOS;