about summary refs log tree commit diff
diff options
context:
space:
mode:
authorThibG <thib@sitedethib.com>2019-10-07 12:02:20 +0200
committerGitHub <noreply@github.com>2019-10-07 12:02:20 +0200
commitd2f7b8685cfd0ec9b69af505b56c791d9b5f1c82 (patch)
tree79e8fa613494ae37fe2adfb41bf9ba38f0ffd80d
parent3921125e5578fb3871fdcae0e8e8a77179f1ad72 (diff)
parent0fbe36e3fb4644945eeb0c142045a003e2793b19 (diff)
Merge pull request #1229 from ThibG/glitch-soc/features/upstream-scroll-behavior
Merge upstream changes to scroll behavior in single column
-rw-r--r--app/javascript/flavours/glitch/components/column.js15
-rw-r--r--app/javascript/flavours/glitch/components/column_back_button.js15
-rw-r--r--app/javascript/flavours/glitch/components/column_header.js16
-rw-r--r--app/javascript/flavours/glitch/components/scrollable_list.js60
-rw-r--r--app/javascript/flavours/glitch/containers/mastodon.js6
-rw-r--r--app/javascript/flavours/glitch/containers/media_container.js15
-rw-r--r--app/javascript/flavours/glitch/features/account/components/profile_column_header.js4
-rw-r--r--app/javascript/flavours/glitch/features/account_gallery/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/blocks/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/bookmarked_statuses/index.js3
-rw-r--r--app/javascript/flavours/glitch/features/community_timeline/index.js3
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/domain_blocks/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/favourited_statuses/index.js3
-rw-r--r--app/javascript/flavours/glitch/features/favourites/index.js14
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/hashtag_timeline/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/index.js3
-rw-r--r--app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/list_timeline/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/lists/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/mutes/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/notifications/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/pinned_statuses/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/public_timeline/index.js3
-rw-r--r--app/javascript/flavours/glitch/features/reblogs/index.js14
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js8
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_loading.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js5
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/tabs_bar.js10
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js15
-rw-r--r--app/javascript/flavours/glitch/packs/public.js8
-rw-r--r--app/javascript/flavours/glitch/styles/basics.scss20
-rw-r--r--app/javascript/flavours/glitch/styles/components/columns.scss27
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss1
-rw-r--r--app/javascript/flavours/glitch/styles/components/single_column.scss13
-rw-r--r--app/javascript/flavours/glitch/util/scrollbar.js34
41 files changed, 303 insertions, 87 deletions
diff --git a/app/javascript/flavours/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.js
index dc87818a5..5819d5362 100644
--- a/app/javascript/flavours/glitch/components/column.js
+++ b/app/javascript/flavours/glitch/components/column.js
@@ -10,10 +10,11 @@ export default class Column extends React.PureComponent {
     extraClasses: PropTypes.string,
     name: PropTypes.string,
     label: PropTypes.string,
+    bindToDocument: PropTypes.bool,
   };
 
   scrollTop () {
-    const scrollable = this.node.querySelector('.scrollable');
+    const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
 
     if (!scrollable) {
       return;
@@ -35,11 +36,19 @@ export default class Column extends React.PureComponent {
   }
 
   componentDidMount () {
-    this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
+    if (this.props.bindToDocument) {
+      document.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
+    } else {
+      this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
+    }
   }
 
   componentWillUnmount () {
-    this.node.removeEventListener('wheel', this.handleWheel);
+    if (this.props.bindToDocument) {
+      document.removeEventListener('wheel', this.handleWheel);
+    } else {
+      this.node.removeEventListener('wheel', this.handleWheel);
+    }
   }
 
   render () {
diff --git a/app/javascript/flavours/glitch/components/column_back_button.js b/app/javascript/flavours/glitch/components/column_back_button.js
index a0260e5af..8326cbb79 100644
--- a/app/javascript/flavours/glitch/components/column_back_button.js
+++ b/app/javascript/flavours/glitch/components/column_back_button.js
@@ -2,6 +2,7 @@ import React from 'react';
 import { FormattedMessage } from 'react-intl';
 import PropTypes from 'prop-types';
 import Icon from 'flavours/glitch/components/icon';
+import { createPortal } from 'react-dom';
 
 export default class ColumnBackButton extends React.PureComponent {
 
@@ -9,6 +10,10 @@ export default class ColumnBackButton extends React.PureComponent {
     router: PropTypes.object,
   };
 
+  static propTypes = {
+    multiColumn: PropTypes.bool,
+  };
+
   handleClick = (event) => {
     // if history is exhausted, or we would leave mastodon, just go to root.
     if (window.history.state) {
@@ -24,12 +29,20 @@ export default class ColumnBackButton extends React.PureComponent {
   }
 
   render () {
-    return (
+    const { multiColumn } = this.props;
+
+    const component = (
       <button onClick={this.handleClick} className='column-back-button'>
         <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
         <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
       </button>
     );
+
+    if (multiColumn) {
+      return component;
+    } else {
+      return createPortal(component, document.getElementById('tabs-bar__portal'));
+    }
   }
 
 }
diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js
index 3a064e90a..43c9f1144 100644
--- a/app/javascript/flavours/glitch/components/column_header.js
+++ b/app/javascript/flavours/glitch/components/column_header.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import { createPortal } from 'react-dom';
 import classNames from 'classnames';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 import ImmutablePropTypes from 'react-immutable-proptypes';
@@ -36,6 +37,7 @@ class ColumnHeader extends React.PureComponent {
     onEnterCleaningMode: PropTypes.func,
     children: PropTypes.node,
     pinned: PropTypes.bool,
+    placeholder: PropTypes.bool,
     onPin: PropTypes.func,
     onMove: PropTypes.func,
     onClick: PropTypes.func,
@@ -104,7 +106,7 @@ class ColumnHeader extends React.PureComponent {
   }
 
   render () {
-    const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props;
+    const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive, placeholder } = this.props;
     const { collapsed, animating, animatingNCD } = this.state;
 
     let title = this.props.title;
@@ -157,7 +159,7 @@ class ColumnHeader extends React.PureComponent {
           <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
         </div>
       );
-    } else if (multiColumn) {
+    } else if (multiColumn && this.props.onPin) {
       pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
     }
 
@@ -179,13 +181,13 @@ class ColumnHeader extends React.PureComponent {
       collapsedContent.push(pinButton);
     }
 
-    if (children || multiColumn) {
+    if (children || (multiColumn && this.props.onPin)) {
       collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
     }
 
     const hasTitle = icon && title;
 
-    return (
+    const component = (
       <div className={wrapperClassName}>
         <h1 className={buttonClassName}>
           {hasTitle && (
@@ -229,6 +231,12 @@ class ColumnHeader extends React.PureComponent {
         </div>
       </div>
     );
+
+    if (multiColumn || placeholder) {
+      return component;
+    } else {
+      return createPortal(component, document.getElementById('tabs-bar__portal'));
+    }
   }
 
 }
diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js
index 7c0b6d082..c022290a4 100644
--- a/app/javascript/flavours/glitch/components/scrollable_list.js
+++ b/app/javascript/flavours/glitch/components/scrollable_list.js
@@ -35,6 +35,7 @@ export default class ScrollableList extends PureComponent {
     alwaysPrepend: PropTypes.bool,
     emptyMessage: PropTypes.node,
     children: PropTypes.node,
+    bindToDocument: PropTypes.bool,
   };
 
   static defaultProps = {
@@ -50,7 +51,9 @@ export default class ScrollableList extends PureComponent {
 
   handleScroll = throttle(() => {
     if (this.node) {
-      const { scrollTop, scrollHeight, clientHeight } = this.node;
+      const scrollTop = this.getScrollTop();
+      const scrollHeight = this.getScrollHeight();
+      const clientHeight = this.getClientHeight();
       const offset = scrollHeight - scrollTop - clientHeight;
 
       if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
@@ -80,9 +83,14 @@ export default class ScrollableList extends PureComponent {
   scrollToTopOnMouseIdle = false;
 
   setScrollTop = newScrollTop => {
-    if (this.node.scrollTop !== newScrollTop) {
+    if (this.getScrollTop() !== newScrollTop) {
       this.lastScrollWasSynthetic = true;
-      this.node.scrollTop = newScrollTop;
+
+      if (this.props.bindToDocument) {
+        document.scrollingElement.scrollTop = newScrollTop;
+      } else {
+        this.node.scrollTop = newScrollTop;
+      }
     }
   };
 
@@ -100,7 +108,7 @@ export default class ScrollableList extends PureComponent {
     this.mouseIdleTimer =
       setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
 
-    if (!this.mouseMovedRecently && this.node.scrollTop === 0) {
+    if (!this.mouseMovedRecently && this.getScrollTop() === 0) {
       // Only set if we just started moving and are scrolled to the top.
       this.scrollToTopOnMouseIdle = true;
     }
@@ -132,15 +140,27 @@ export default class ScrollableList extends PureComponent {
   }
 
   getScrollPosition = () => {
-    if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
-      return {height: this.node.scrollHeight, top: this.node.scrollTop};
+    if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
+      return { height: this.getScrollHeight(), top: this.getScrollTop() };
     } else {
       return null;
     }
   }
 
+  getScrollTop = () => {
+    return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop;
+  }
+
+  getScrollHeight = () => {
+    return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight;
+  }
+
+  getClientHeight = () => {
+    return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight;
+  }
+
   updateScrollBottom = (snapshot) => {
-    const newScrollTop = this.node.scrollHeight - snapshot;
+    const newScrollTop = this.getScrollHeight() - snapshot;
 
     this.setScrollTop(newScrollTop);
   }
@@ -155,8 +175,8 @@ export default class ScrollableList extends PureComponent {
       this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
     const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0);
 
-    if (pendingChanged || someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
-      return this.node.scrollHeight - this.node.scrollTop;
+    if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
+      return this.getScrollHeight() - this.getScrollTop();
     } else {
       return null;
     }
@@ -165,7 +185,9 @@ export default class ScrollableList extends PureComponent {
   componentDidUpdate (prevProps, prevState, snapshot) {
     // Reset the scroll position when a new child comes in in order not to
     // jerk the scrollbar around if you're already scrolled down the page.
-    if (snapshot !== null) this.updateScrollBottom(snapshot);
+    if (snapshot !== null) {
+      this.updateScrollBottom(snapshot);
+    }
   }
 
   componentWillUnmount () {
@@ -191,13 +213,23 @@ export default class ScrollableList extends PureComponent {
   }
 
   attachScrollListener () {
-    this.node.addEventListener('scroll', this.handleScroll);
-    this.node.addEventListener('wheel', this.handleWheel);
+    if (this.props.bindToDocument) {
+      document.addEventListener('scroll', this.handleScroll);
+      document.addEventListener('wheel', this.handleWheel);
+    } else {
+      this.node.addEventListener('scroll', this.handleScroll);
+      this.node.addEventListener('wheel', this.handleWheel);
+    }
   }
 
   detachScrollListener () {
-    this.node.removeEventListener('scroll', this.handleScroll);
-    this.node.removeEventListener('wheel', this.handleWheel);
+    if (this.props.bindToDocument) {
+      document.removeEventListener('scroll', this.handleScroll);
+      document.removeEventListener('wheel', this.handleWheel);
+    } else {
+      this.node.removeEventListener('scroll', this.handleScroll);
+      this.node.removeEventListener('wheel', this.handleWheel);
+    }
   }
 
   getFirstChildKey (props) {
diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js
index 59eef6636..8101be87e 100644
--- a/app/javascript/flavours/glitch/containers/mastodon.js
+++ b/app/javascript/flavours/glitch/containers/mastodon.js
@@ -49,6 +49,10 @@ export default class Mastodon extends React.PureComponent {
     }
   }
 
+  shouldUpdateScroll (_, { location }) {
+    return !(location.state && location.state.mastodonModalOpen);
+  }
+
   render () {
     const { locale } = this.props;
 
@@ -57,7 +61,7 @@ export default class Mastodon extends React.PureComponent {
         <Provider store={store}>
           <ErrorBoundary>
             <BrowserRouter basename='/web'>
-              <ScrollContext>
+              <ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
                 <Route path='/' component={UI} />
               </ScrollContext>
             </BrowserRouter>
diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js
index 41547412e..a483510b0 100644
--- a/app/javascript/flavours/glitch/containers/media_container.js
+++ b/app/javascript/flavours/glitch/containers/media_container.js
@@ -2,16 +2,17 @@ import React, { PureComponent, Fragment } from 'react';
 import ReactDOM from 'react-dom';
 import PropTypes from 'prop-types';
 import { IntlProvider, addLocaleData } from 'react-intl';
+import { List as ImmutableList, fromJS } from 'immutable';
 import { getLocale } from 'mastodon/locales';
+import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar';
 import MediaGallery from 'flavours/glitch/components/media_gallery';
-import Video from 'flavours/glitch/features/video';
-import Card from 'flavours/glitch/features/status/components/card';
 import Poll from 'flavours/glitch/components/poll';
 import Hashtag from 'flavours/glitch/components/hashtag';
-import Audio from 'flavours/glitch/features/audio';
 import ModalRoot from 'flavours/glitch/components/modal_root';
 import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
-import { List as ImmutableList, fromJS } from 'immutable';
+import Video from 'flavours/glitch/features/video';
+import Card from 'flavours/glitch/features/status/components/card';
+import Audio from 'flavours/glitch/features/audio';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
@@ -33,6 +34,8 @@ export default class MediaContainer extends PureComponent {
 
   handleOpenMedia = (media, index) => {
     document.body.classList.add('with-modals--active');
+    document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
+
     this.setState({ media, index });
   }
 
@@ -40,11 +43,15 @@ export default class MediaContainer extends PureComponent {
     const media = ImmutableList([video]);
 
     document.body.classList.add('with-modals--active');
+    document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
+
     this.setState({ media, time });
   }
 
   handleCloseMedia = () => {
     document.body.classList.remove('with-modals--active');
+    document.documentElement.style.marginRight = 0;
+
     this.setState({ media: null, index: null, time: null });
   }
 
diff --git a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js
index b6d373a2c..17c08e375 100644
--- a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js
+++ b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js
@@ -12,11 +12,12 @@ class ProfileColumnHeader extends React.PureComponent {
 
   static propTypes = {
     onClick: PropTypes.func,
+    multiColumn: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
   render() {
-    const { onClick, intl } = this.props;
+    const { onClick, intl, multiColumn } = this.props;
 
     return (
       <ColumnHeader
@@ -24,6 +25,7 @@ class ProfileColumnHeader extends React.PureComponent {
         title={intl.formatMessage(messages.profile)}
         onClick={onClick}
         showBackButton
+        multiColumn={multiColumn}
       />
     );
   }
diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js
index ff39764bb..f5fe6c930 100644
--- a/app/javascript/flavours/glitch/features/account_gallery/index.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/index.js
@@ -55,6 +55,7 @@ class AccountGallery extends ImmutablePureComponent {
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
     isAccount: PropTypes.bool,
+    multiColumn: PropTypes.bool,
   };
 
   state = {
@@ -130,7 +131,7 @@ class AccountGallery extends ImmutablePureComponent {
   }
 
   render () {
-    const { attachments, isLoading, hasMore, isAccount } = this.props;
+    const { attachments, isLoading, hasMore, isAccount, multiColumn } = this.props;
     const { width } = this.state;
 
     if (!isAccount) {
@@ -157,7 +158,7 @@ class AccountGallery extends ImmutablePureComponent {
 
     return (
       <Column ref={this.setColumnRef}>
-        <ProfileColumnHeader onClick={this.handleHeaderClick} />
+        <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
 
         <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={this.shouldUpdateScroll}>
           <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js
index 2f0859341..1f02c1be5 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -39,6 +39,7 @@ class AccountTimeline extends ImmutablePureComponent {
     hasMore: PropTypes.bool,
     withReplies: PropTypes.bool,
     isAccount: PropTypes.bool,
+    multiColumn: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -76,7 +77,7 @@ class AccountTimeline extends ImmutablePureComponent {
   }
 
   render () {
-    const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount } = this.props;
+    const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, multiColumn } = this.props;
 
     if (!isAccount) {
       return (
@@ -96,7 +97,7 @@ class AccountTimeline extends ImmutablePureComponent {
 
     return (
       <Column ref={this.setRef} name='account'>
-        <ProfileColumnHeader onClick={this.handleHeaderClick} />
+        <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
 
         <StatusList
           prepend={<HeaderContainer accountId={this.props.params.accountId} />}
@@ -108,6 +109,7 @@ class AccountTimeline extends ImmutablePureComponent {
           hasMore={hasMore}
           onLoadMore={this.handleLoadMore}
           emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />}
+          bindToDocument={!multiColumn}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/blocks/index.js b/app/javascript/flavours/glitch/features/blocks/index.js
index 8a84f5a55..9eb6fe02e 100644
--- a/app/javascript/flavours/glitch/features/blocks/index.js
+++ b/app/javascript/flavours/glitch/features/blocks/index.js
@@ -31,6 +31,7 @@ class Blocks extends ImmutablePureComponent {
     accountIds: ImmutablePropTypes.list,
     hasMore: PropTypes.bool,
     intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -42,7 +43,7 @@ class Blocks extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, accountIds, hasMore } = this.props;
+    const { intl, accountIds, hasMore, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -55,13 +56,14 @@ class Blocks extends ImmutablePureComponent {
     const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
 
     return (
-      <Column name='blocks' icon='ban' heading={intl.formatMessage(messages.heading)}>
+      <Column name='blocks' bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
         <ScrollableList
           scrollKey='blocks'
           onLoadMore={this.handleLoadMore}
           hasMore={hasMore}
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
             <AccountContainer key={id} id={id} />
diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js
index 7d550e362..58b9e6396 100644
--- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js
@@ -73,7 +73,7 @@ class Bookmarks extends ImmutablePureComponent {
     const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked toots yet. When you bookmark one, it will show up here." />;
 
     return (
-      <Column ref={this.setRef} name='bookmarks'>
+      <Column bindToDocument={!multiColumn} ref={this.setRef} name='bookmarks'>
         <ColumnHeader
           icon='bookmark'
           title={intl.formatMessage(messages.heading)}
@@ -93,6 +93,7 @@ class Bookmarks extends ImmutablePureComponent {
           isLoading={isLoading}
           onLoadMore={this.handleLoadMore}
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js
index 5585edc9c..c548824ce 100644
--- a/app/javascript/flavours/glitch/features/community_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/community_timeline/index.js
@@ -105,7 +105,7 @@ class CommunityTimeline extends React.PureComponent {
     const pinned = !!columnId;
 
     return (
-      <Column ref={this.setRef} name='local' label={intl.formatMessage(messages.title)}>
+      <Column ref={this.setRef} name='local' bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
         <ColumnHeader
           icon='users'
           active={hasUnread}
@@ -125,6 +125,7 @@ class CommunityTimeline extends React.PureComponent {
           timelineId={`community${onlyMedia ? ':media' : ''}`}
           onLoadMore={this.handleLoadMore}
           emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
+          bindToDocument={!multiColumn}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js
index e2c0a439a..7741c6922 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js
@@ -135,7 +135,7 @@ class DirectTimeline extends React.PureComponent {
     }
 
     return (
-      <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
         <ColumnHeader
           icon='envelope'
           active={hasUnread}
diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js
index 49e0368d7..cd105a49b 100644
--- a/app/javascript/flavours/glitch/features/domain_blocks/index.js
+++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js
@@ -32,6 +32,7 @@ class Blocks extends ImmutablePureComponent {
     hasMore: PropTypes.bool,
     domains: ImmutablePropTypes.list,
     intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -43,7 +44,7 @@ class Blocks extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, domains, hasMore } = this.props;
+    const { intl, domains, hasMore, multiColumn } = this.props;
 
     if (!domains) {
       return (
@@ -56,13 +57,14 @@ class Blocks extends ImmutablePureComponent {
     const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />;
 
     return (
-      <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
+      <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
         <ScrollableList
           scrollKey='domain_blocks'
           onLoadMore={this.handleLoadMore}
           hasMore={hasMore}
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         >
           {domains.map(domain =>
             <DomainContainer key={domain} domain={domain} />
diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
index 719a31d6e..c6470ba74 100644
--- a/app/javascript/flavours/glitch/features/favourited_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
@@ -73,7 +73,7 @@ class Favourites extends ImmutablePureComponent {
     const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite toots yet. When you favourite one, it will show up here." />;
 
     return (
-      <Column ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}>
+      <Column bindToDocument={!multiColumn} ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}>
         <ColumnHeader
           icon='star'
           title={intl.formatMessage(messages.heading)}
@@ -93,6 +93,7 @@ class Favourites extends ImmutablePureComponent {
           isLoading={isLoading}
           onLoadMore={this.handleLoadMore}
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js
index 7afadf12e..953bf171f 100644
--- a/app/javascript/flavours/glitch/features/favourites/index.js
+++ b/app/javascript/flavours/glitch/features/favourites/index.js
@@ -6,6 +6,7 @@ import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import { fetchFavourites } from 'flavours/glitch/actions/interactions';
 import AccountContainer from 'flavours/glitch/containers/account_container';
 import Column from 'flavours/glitch/features/ui/components/column';
+import Icon from 'flavours/glitch/components/icon';
 import ColumnHeader from 'flavours/glitch/components/column_header';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -13,6 +14,7 @@ import ScrollableList from '../../components/scrollable_list';
 
 const messages = defineMessages({
   heading: { id: 'column.favourited_by', defaultMessage: 'Favourited by' },
+  refresh: { id: 'refresh', defaultMessage: 'Refresh' },
 });
 
 const mapStateToProps = (state, props) => ({
@@ -27,6 +29,7 @@ class Favourites extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     accountIds: ImmutablePropTypes.list,
+    multiColumn: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
@@ -50,8 +53,12 @@ class Favourites extends ImmutablePureComponent {
     this.column = c;
   }
 
+  handleRefresh = () => {
+    this.props.dispatch(fetchFavourites(this.props.params.statusId));
+  }
+
   render () {
-    const { intl, accountIds } = this.props;
+    const { intl, accountIds, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -70,10 +77,15 @@ class Favourites extends ImmutablePureComponent {
           title={intl.formatMessage(messages.heading)}
           onClick={this.handleHeaderClick}
           showBackButton
+          multiColumn={multiColumn}
+          extraButton={(
+            <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
+          )}
         />
         <ScrollableList
           scrollKey='favourites'
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
             <AccountContainer key={id} id={id} withNote={false} />
diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js
index a7e8f4b61..36770aace 100644
--- a/app/javascript/flavours/glitch/features/follow_requests/index.js
+++ b/app/javascript/flavours/glitch/features/follow_requests/index.js
@@ -31,6 +31,7 @@ class FollowRequests extends ImmutablePureComponent {
     hasMore: PropTypes.bool,
     accountIds: ImmutablePropTypes.list,
     intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -42,7 +43,7 @@ class FollowRequests extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, accountIds, hasMore } = this.props;
+    const { intl, accountIds, hasMore, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -55,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 name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}>
+      <Column bindToDocument={!multiColumn} name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
 
         <ScrollableList
@@ -63,6 +64,7 @@ class FollowRequests extends ImmutablePureComponent {
           onLoadMore={this.handleLoadMore}
           hasMore={hasMore}
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
             <AccountAuthorizeContainer key={id} id={id} />
diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js
index 2bd0e6e2f..c78dcc8e4 100644
--- a/app/javascript/flavours/glitch/features/followers/index.js
+++ b/app/javascript/flavours/glitch/features/followers/index.js
@@ -33,6 +33,7 @@ class Followers extends ImmutablePureComponent {
     accountIds: ImmutablePropTypes.list,
     hasMore: PropTypes.bool,
     isAccount: PropTypes.bool,
+    multiColumn: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -70,7 +71,7 @@ class Followers extends ImmutablePureComponent {
   }
 
   render () {
-    const { accountIds, hasMore, isAccount } = this.props;
+    const { accountIds, hasMore, isAccount, multiColumn } = this.props;
 
     if (!isAccount) {
       return (
@@ -92,7 +93,7 @@ class Followers extends ImmutablePureComponent {
 
     return (
       <Column ref={this.setRef}>
-        <ProfileColumnHeader onClick={this.handleHeaderClick} />
+        <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
 
         <ScrollableList
           scrollKey='followers'
@@ -101,6 +102,7 @@ class Followers extends ImmutablePureComponent {
           prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
           alwaysPrepend
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
             <AccountContainer key={id} id={id} withNote={false} />
diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js
index f03da0c94..df7c19c22 100644
--- a/app/javascript/flavours/glitch/features/following/index.js
+++ b/app/javascript/flavours/glitch/features/following/index.js
@@ -33,6 +33,7 @@ class Following extends ImmutablePureComponent {
     accountIds: ImmutablePropTypes.list,
     hasMore: PropTypes.bool,
     isAccount: PropTypes.bool,
+    multiColumn: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -70,7 +71,7 @@ class Following extends ImmutablePureComponent {
   }
 
   render () {
-    const { accountIds, hasMore, isAccount } = this.props;
+    const { accountIds, hasMore, isAccount, multiColumn } = this.props;
 
     if (!isAccount) {
       return (
@@ -92,7 +93,7 @@ class Following extends ImmutablePureComponent {
 
     return (
       <Column ref={this.setRef}>
-        <ProfileColumnHeader onClick={this.handleHeaderClick} />
+        <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
 
         <ScrollableList
           scrollKey='following'
@@ -101,6 +102,7 @@ class Following extends ImmutablePureComponent {
           prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
           alwaysPrepend
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
             <AccountContainer key={id} id={id} withNote={false} />
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js
index a5095fbd9..d8a51c689 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started/index.js
@@ -166,7 +166,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
     ]);
 
     return (
-      <Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
+      <Column bindToDocument={!multiColumn} name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
         <div className='scrollable optionally-scrollable'>
           <div className='getting-started__wrapper'>
             {!multiColumn && <NavigationBar account={myAccount} />}
diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
index d39505f46..16dd80c4f 100644
--- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
@@ -145,6 +145,7 @@ class HashtagTimeline extends React.PureComponent {
           pinned={pinned}
           multiColumn={multiColumn}
           showBackButton
+          bindToDocument={!multiColumn}
         >
           {columnId && <ColumnSettingsContainer columnId={columnId} />}
         </ColumnHeader>
@@ -155,6 +156,7 @@ class HashtagTimeline extends React.PureComponent {
           timelineId={`hashtag:${id}`}
           onLoadMore={this.handleLoadMore}
           emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
+          bindToDocument={!multiColumn}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js
index defb1dcc1..9b71a4404 100644
--- a/app/javascript/flavours/glitch/features/home_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/index.js
@@ -97,7 +97,7 @@ class HomeTimeline extends React.PureComponent {
     const pinned = !!columnId;
 
     return (
-      <Column ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}>
+      <Column bindToDocument={!multiColumn} ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}>
         <ColumnHeader
           icon='home'
           active={hasUnread}
@@ -117,6 +117,7 @@ class HomeTimeline extends React.PureComponent {
           onLoadMore={this.handleLoadMore}
           timelineId='home'
           emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />}
+          bindToDocument={!multiColumn}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js
index 95daa6907..e40dbf44e 100644
--- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js
+++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js
@@ -25,10 +25,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
   };
 
   render () {
-    const { intl, collapseEnabled } = this.props;
+    const { intl, collapseEnabled, multiColumn } = this.props;
 
     return (
-      <Column icon='question' heading={intl.formatMessage(messages.heading)}>
+      <Column bindToDocument={!multiColumn} icon='question' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
         <div className='keyboard-shortcuts scrollable optionally-scrollable'>
           <table>
diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js
index 8c3d0af51..908a65597 100644
--- a/app/javascript/flavours/glitch/features/list_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/list_timeline/index.js
@@ -174,6 +174,7 @@ class ListTimeline extends React.PureComponent {
           onClick={this.handleHeaderClick}
           pinned={pinned}
           multiColumn={multiColumn}
+          bindToDocument={!multiColumn}
         >
           <div className='column-header__links'>
             <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
@@ -212,6 +213,7 @@ class ListTimeline extends React.PureComponent {
           timelineId={`list:${id}`}
           onLoadMore={this.handleLoadMore}
           emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />}
+          bindToDocument={!multiColumn}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/lists/index.js b/app/javascript/flavours/glitch/features/lists/index.js
index 79bf2e601..adde3dd5c 100644
--- a/app/javascript/flavours/glitch/features/lists/index.js
+++ b/app/javascript/flavours/glitch/features/lists/index.js
@@ -40,6 +40,7 @@ class Lists extends ImmutablePureComponent {
     dispatch: PropTypes.func.isRequired,
     lists: ImmutablePropTypes.list,
     intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -47,7 +48,7 @@ class Lists extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, lists } = this.props;
+    const { intl, lists, multiColumn } = this.props;
 
     if (!lists) {
       return (
@@ -60,7 +61,7 @@ class Lists extends ImmutablePureComponent {
     const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />;
 
     return (
-      <Column icon='bars' heading={intl.formatMessage(messages.heading)}>
+      <Column bindToDocument={!multiColumn} icon='bars' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
 
         <NewListForm />
@@ -69,6 +70,7 @@ class Lists extends ImmutablePureComponent {
         <ScrollableList
           scrollKey='lists'
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         >
           {lists.map(list =>
             <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js
index 7c20ca9b9..c27a530d5 100644
--- a/app/javascript/flavours/glitch/features/mutes/index.js
+++ b/app/javascript/flavours/glitch/features/mutes/index.js
@@ -31,6 +31,7 @@ class Mutes extends ImmutablePureComponent {
     hasMore: PropTypes.bool,
     accountIds: ImmutablePropTypes.list,
     intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -42,7 +43,7 @@ class Mutes extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, accountIds, hasMore } = this.props;
+    const { intl, accountIds, hasMore, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -55,13 +56,14 @@ class Mutes extends ImmutablePureComponent {
     const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />;
 
     return (
-      <Column name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}>
+      <Column bindToDocument={!multiColumn} name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}>
         <ColumnBackButtonSlim />
         <ScrollableList
           scrollKey='mutes'
           onLoadMore={this.handleLoadMore}
           hasMore={hasMore}
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
             <AccountContainer key={id} id={id} />
diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js
index 99b2391c7..7f06d70c5 100644
--- a/app/javascript/flavours/glitch/features/notifications/index.js
+++ b/app/javascript/flavours/glitch/features/notifications/index.js
@@ -226,6 +226,7 @@ class Notifications extends React.PureComponent {
         onScrollToTop={this.handleScrollToTop}
         onScroll={this.handleScroll}
         shouldUpdateScroll={shouldUpdateScroll}
+        bindToDocument={!multiColumn}
       >
         {scrollableContent}
       </ScrollableList>
@@ -233,6 +234,7 @@ class Notifications extends React.PureComponent {
 
     return (
       <Column
+        bindToDocument={!multiColumn}
         ref={this.setColumnRef}
         name='notifications'
         extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null}
diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js
index 8d406ddf4..34d8e465f 100644
--- a/app/javascript/flavours/glitch/features/pinned_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.js
@@ -27,6 +27,7 @@ class PinnedStatuses extends ImmutablePureComponent {
     statusIds: ImmutablePropTypes.list.isRequired,
     intl: PropTypes.object.isRequired,
     hasMore: PropTypes.bool.isRequired,
+    multiColumn: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -42,15 +43,16 @@ class PinnedStatuses extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, statusIds, hasMore } = this.props;
+    const { intl, statusIds, hasMore, multiColumn } = this.props;
 
     return (
-      <Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
+      <Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
         <ColumnBackButtonSlim />
         <StatusList
           statusIds={statusIds}
           scrollKey='pinned_statuses'
           hasMore={hasMore}
+          bindToDocument={!multiColumn}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js
index 4bcf3da9d..49a5011c0 100644
--- a/app/javascript/flavours/glitch/features/public_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/public_timeline/index.js
@@ -104,7 +104,7 @@ class PublicTimeline extends React.PureComponent {
     const pinned = !!columnId;
 
     return (
-      <Column ref={this.setRef} name='federated' label={intl.formatMessage(messages.title)}>
+      <Column bindToDocument={!multiColumn} ref={this.setRef} name='federated' label={intl.formatMessage(messages.title)}>
         <ColumnHeader
           icon='globe'
           active={hasUnread}
@@ -124,6 +124,7 @@ class PublicTimeline extends React.PureComponent {
           trackScroll={!pinned}
           scrollKey={`public_timeline-${columnId}`}
           emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
+          bindToDocument={!multiColumn}
         />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js
index a8e9db7f5..258070358 100644
--- a/app/javascript/flavours/glitch/features/reblogs/index.js
+++ b/app/javascript/flavours/glitch/features/reblogs/index.js
@@ -6,6 +6,7 @@ import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import { fetchReblogs } from 'flavours/glitch/actions/interactions';
 import AccountContainer from 'flavours/glitch/containers/account_container';
 import Column from 'flavours/glitch/features/ui/components/column';
+import Icon from 'flavours/glitch/components/icon';
 import ColumnHeader from 'flavours/glitch/components/column_header';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -13,6 +14,7 @@ import ScrollableList from 'flavours/glitch/components/scrollable_list';
 
 const messages = defineMessages({
   heading: { id: 'column.reblogged_by', defaultMessage: 'Boosted by' },
+  refresh: { id: 'refresh', defaultMessage: 'Refresh' },
 });
 
 const mapStateToProps = (state, props) => ({
@@ -27,6 +29,7 @@ class Reblogs extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     accountIds: ImmutablePropTypes.list,
+    multiColumn: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
@@ -50,8 +53,12 @@ class Reblogs extends ImmutablePureComponent {
     this.column = c;
   }
 
+  handleRefresh = () => {
+    this.props.dispatch(fetchReblogs(this.props.params.statusId));
+  }
+
   render () {
-    const { intl, accountIds } = this.props;
+    const { intl, accountIds, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -70,11 +77,16 @@ class Reblogs extends ImmutablePureComponent {
           title={intl.formatMessage(messages.heading)}
           onClick={this.handleHeaderClick}
           showBackButton
+          multiColumn={multiColumn}
+          extraButton={(
+            <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
+          )}
         />
 
         <ScrollableList
           scrollKey='reblogs'
           emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
             <AccountContainer key={id} id={id} withNote={false} />
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index dd17823ad..508510c4e 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -155,6 +155,7 @@ class Status extends ImmutablePureComponent {
     descendantsIds: ImmutablePropTypes.list,
     intl: PropTypes.object.isRequired,
     askReplyConfirmation: PropTypes.bool,
+    multiColumn: PropTypes.bool,
     domain: PropTypes.string.isRequired,
   };
 
@@ -497,13 +498,13 @@ class Status extends ImmutablePureComponent {
   render () {
     let ancestors, descendants;
     const { setExpansion } = this;
-    const { status, settings, ancestorsIds, descendantsIds, intl, domain } = this.props;
+    const { status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props;
     const { fullscreen, isExpanded } = this.state;
 
     if (status === null) {
       return (
         <Column>
-          <ColumnBackButton />
+          <ColumnBackButton multiColumn={multiColumn} />
           <MissingIndicator />
         </Column>
       );
@@ -531,12 +532,13 @@ class Status extends ImmutablePureComponent {
     };
 
     return (
-      <Column ref={this.setColumnRef} label={intl.formatMessage(messages.detailedStatus)}>
+      <Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.detailedStatus)}>
         <ColumnHeader
           icon='comment'
           title={intl.formatMessage(messages.tootHeading)}
           onClick={this.handleHeaderClick}
           showBackButton
+          multiColumn={multiColumn}
           extraButton={(
             <button className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={!isExpanded ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button>
           )}
diff --git a/app/javascript/flavours/glitch/features/ui/components/column_loading.js b/app/javascript/flavours/glitch/features/ui/components/column_loading.js
index ba2d0824e..22c00c915 100644
--- a/app/javascript/flavours/glitch/features/ui/components/column_loading.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_loading.js
@@ -21,7 +21,7 @@ export default class ColumnLoading extends ImmutablePureComponent {
     let { title, icon } = this.props;
     return (
       <Column>
-        <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} />
+        <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} placeholder />
         <div className='scrollable' />
       </Column>
     );
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index 117ce4c55..488daf0cc 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Base from '../../../components/modal_root';
+import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar';
+import Base from 'flavours/glitch/components/modal_root';
 import BundleContainer from '../containers/bundle_container';
 import BundleModalError from './bundle_modal_error';
 import ModalLoading from './modal_loading';
@@ -61,8 +62,10 @@ export default class ModalRoot extends React.PureComponent {
   componentDidUpdate (prevProps, prevState, { visible }) {
     if (visible) {
       document.body.classList.add('with-modals--active');
+      document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
     } else {
       document.body.classList.remove('with-modals--active');
+      document.documentElement.style.marginRight = 0;
     }
   }
 
diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
index 90e645a82..a67405215 100644
--- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
+++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
@@ -73,9 +73,13 @@ class TabsBar extends React.PureComponent {
     const { intl: { formatMessage } } = this.props;
 
     return (
-      <nav className='tabs-bar' ref={this.setRef}>
-        {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
-      </nav>
+      <div className='tabs-bar__wrapper'>
+        <nav className='tabs-bar' ref={this.setRef}>
+          {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
+        </nav>
+
+        <div id='tabs-bar__portal' />
+      </div>
     );
   }
 
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index c201cd93d..e5925a484 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -129,12 +129,25 @@ class SwitchingColumnsArea extends React.PureComponent {
 
   componentWillMount () {
     window.addEventListener('resize', this.handleResize, { passive: true });
+
+    if (this.state.mobile) {
+      document.body.classList.toggle('layout-single-column', true);
+      document.body.classList.toggle('layout-multiple-columns', false);
+    } else {
+      document.body.classList.toggle('layout-single-column', false);
+      document.body.classList.toggle('layout-multiple-columns', true);
+    }
   }
 
-  componentDidUpdate (prevProps) {
+  componentDidUpdate (prevProps, prevState) {
     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 () {
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index 5a15830df..767fb023c 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -94,14 +94,6 @@ function main() {
       new Rellax('.parallax', { speed: -1 });
     }
 
-    if (document.body.classList.contains('with-modals')) {
-      const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
-      const scrollbarWidthStyle = document.createElement('style');
-      scrollbarWidthStyle.id = 'scrollbar-width';
-      document.head.appendChild(scrollbarWidthStyle);
-      scrollbarWidthStyle.sheet.insertRule(`body.with-modals--active { margin-right: ${scrollbarWidth}px; }`, 0);
-    }
-
     delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
     delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
 
diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss
index 64e543b78..77631097a 100644
--- a/app/javascript/flavours/glitch/styles/basics.scss
+++ b/app/javascript/flavours/glitch/styles/basics.scss
@@ -7,7 +7,7 @@
 
 body {
   font-family: $font-sans-serif, sans-serif;
-  background: darken($ui-base-color, 8%);
+  background: darken($ui-base-color, 7%);
   font-size: 13px;
   line-height: 18px;
   font-weight: 400;
@@ -34,11 +34,19 @@ body {
   }
 
   &.app-body {
-    position: absolute;
-    width: 100%;
-    height: 100%;
     padding: 0;
-    background: $ui-base-color;
+
+    &.layout-single-column {
+      height: auto;
+      min-height: 100vh;
+      overflow-y: scroll;
+    }
+
+    &.layout-multiple-columns {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+    }
 
     &.with-modals--active {
       overflow-y: hidden;
@@ -55,7 +63,6 @@ body {
 
     &--active {
       overflow-y: hidden;
-      margin-right: 13px;
     }
   }
 
@@ -124,7 +131,6 @@ button {
   & > div {
     display: flex;
     width: 100%;
-    height: 100%;
     align-items: center;
     justify-content: center;
     outline: 0 !important;
diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index 7f3c21163..6ba9698c5 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -17,6 +17,7 @@
     justify-content: center;
     width: 100%;
     height: 100%;
+    min-height: 100vh;
 
     &__pane {
       height: 100%;
@@ -24,12 +25,14 @@
       pointer-events: none;
       display: flex;
       justify-content: flex-end;
+      min-width: 285px;
 
       &--start {
         justify-content: flex-start;
       }
 
       &__inner {
+        position: fixed;
         width: 285px;
         pointer-events: auto;
         height: 100%;
@@ -40,6 +43,7 @@
       box-sizing: border-box;
       width: 100%;
       max-width: 600px;
+      flex: 0 0 auto;
       display: flex;
       flex-direction: column;
 
@@ -50,6 +54,26 @@
   }
 }
 
+.tabs-bar__wrapper {
+  background: darken($ui-base-color, 8%);
+  position: sticky;
+  top: 0;
+  z-index: 2;
+  padding-top: 0;
+
+  @media screen and (min-width: $no-gap-breakpoint) {
+    padding-top: 10px;
+  }
+
+  .tabs-bar {
+    margin-bottom: 0;
+
+    @media screen and (min-width: $no-gap-breakpoint) {
+      margin-bottom: 10px;
+    }
+  }
+}
+
 .react-swipeable-view-container {
   &,
   .columns-area,
@@ -83,7 +107,6 @@
   flex-direction: column;
   width: 100%;
   height: 100%;
-  background: darken($ui-base-color, 7%);
 }
 
 .column {
@@ -91,6 +114,8 @@
 }
 
 .column-back-button {
+  box-sizing: border-box;
+  width: 100%;
   background: lighten($ui-base-color, 4%);
   color: $highlight-text-color;
   cursor: pointer;
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 6f0d4c0be..055a494e7 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -1415,6 +1415,7 @@
   position: absolute;
   top: 0;
   left: 0;
+  z-index: 9999;
 }
 
 .icon-badge-wrapper {
diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss
index 1d8055fe5..edf705b5f 100644
--- a/app/javascript/flavours/glitch/styles/components/single_column.scss
+++ b/app/javascript/flavours/glitch/styles/components/single_column.scss
@@ -127,8 +127,17 @@
     top: 15px;
   }
 
+  .scrollable {
+    overflow: visible;
+
+    @supports(display: grid) {
+      contain: content;
+    }
+  }
+
   @media screen and (min-width: $no-gap-breakpoint) {
     padding: 10px 0;
+    padding-top: 0;
   }
 
   @media screen and (min-width: 630px) {
@@ -217,13 +226,11 @@
 
 @media screen and (min-width: $no-gap-breakpoint) {
   .tabs-bar {
-    margin: 10px auto;
-    margin-bottom: 0;
     width: 100%;
   }
 
   .react-swipeable-view-container .columns-area--mobile {
-    height: calc(100% - 20px) !important;
+    height: calc(100% - 10px) !important;
   }
 
   .getting-started__wrapper,
diff --git a/app/javascript/flavours/glitch/util/scrollbar.js b/app/javascript/flavours/glitch/util/scrollbar.js
new file mode 100644
index 000000000..929b036d6
--- /dev/null
+++ b/app/javascript/flavours/glitch/util/scrollbar.js
@@ -0,0 +1,34 @@
+/** @type {number | null} */
+let cachedScrollbarWidth = null;
+
+/**
+ * @return {number}
+ */
+const getActualScrollbarWidth = () => {
+  const outer = document.createElement('div');
+  outer.style.visibility = 'hidden';
+  outer.style.overflow = 'scroll';
+  document.body.appendChild(outer);
+
+  const inner = document.createElement('div');
+  outer.appendChild(inner);
+
+  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
+  outer.parentNode.removeChild(outer);
+
+  return scrollbarWidth;
+};
+
+/**
+ * @return {number}
+ */
+export const getScrollbarWidth = () => {
+  if (cachedScrollbarWidth !== null) {
+    return cachedScrollbarWidth;
+  }
+
+  const scrollbarWidth = getActualScrollbarWidth();
+  cachedScrollbarWidth = scrollbarWidth;
+
+  return scrollbarWidth;
+};