diff --git a/app/javascript/flavours/glitch/actions/app.js b/app/javascript/flavours/glitch/actions/app.js
new file mode 100644
index 0000000000..de2d93e292
--- /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 e238456c5b..d3b6bd7e98 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 0ba2e712c2..3703080435 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 2be6d94780..c861f55688 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 0f3ab3b848..0364ec2890 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 7e584e8fa5..c8517f592a 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;