about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2019-07-19 09:25:22 +0200
committerThibaut Girka <thib@sitedethib.com>2019-10-05 22:53:20 +0200
commit9e2e623ebe25b8e58a6c8f4bf947015481f10c66 (patch)
tree456537a09210291271436ee013c8335cb063d312 /app/javascript/flavours/glitch
parent3921125e5578fb3871fdcae0e8e8a77179f1ad72 (diff)
[Glitch] Change single-column mode to scroll the whole page
Port aa22b38fdbc1842549b6cbc0e0d948f85a71b92a to glitch-soc

Signed-off-by: Thibaut Girka <thib@sitedethib.com>
Diffstat (limited to 'app/javascript/flavours/glitch')
-rw-r--r--app/javascript/flavours/glitch/components/scrollable_list.js60
-rw-r--r--app/javascript/flavours/glitch/containers/media_container.js7
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/blocks/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/community_timeline/index.js1
-rw-r--r--app/javascript/flavours/glitch/features/domain_blocks/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/favourited_statuses/index.js1
-rw-r--r--app/javascript/flavours/glitch/features/favourites/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/hashtag_timeline/index.js1
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/index.js1
-rw-r--r--app/javascript/flavours/glitch/features/list_timeline/index.js1
-rw-r--r--app/javascript/flavours/glitch/features/lists/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/mutes/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/notifications/index.js1
-rw-r--r--app/javascript/flavours/glitch/features/pinned_statuses/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/public_timeline/index.js1
-rw-r--r--app/javascript/flavours/glitch/features/reblogs/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js24
-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.scss3
-rw-r--r--app/javascript/flavours/glitch/styles/components/single_column.scss4
26 files changed, 150 insertions, 42 deletions
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/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js
index 41547412e..0afe50740 100644
--- a/app/javascript/flavours/glitch/containers/media_container.js
+++ b/app/javascript/flavours/glitch/containers/media_container.js
@@ -10,6 +10,7 @@ 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 { getScrollbarWidth } from 'flavours/glitch/features/ui/components/modal_root';
 import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
 import { List as ImmutableList, fromJS } from 'immutable';
 
@@ -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_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js
index 2f0859341..a6ed4564c 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 (
@@ -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..ae0cdf2fe 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 (
@@ -62,6 +63,7 @@ class Blocks extends ImmutablePureComponent {
           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/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js
index 5585edc9c..ca437d2b0 100644
--- a/app/javascript/flavours/glitch/features/community_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/community_timeline/index.js
@@ -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/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js
index 49e0368d7..b92ce349b 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 (
@@ -63,6 +64,7 @@ class Blocks extends ImmutablePureComponent {
           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..99b532294 100644
--- a/app/javascript/flavours/glitch/features/favourited_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
@@ -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..3c0c2a905 100644
--- a/app/javascript/flavours/glitch/features/favourites/index.js
+++ b/app/javascript/flavours/glitch/features/favourites/index.js
@@ -27,6 +27,7 @@ class Favourites extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     accountIds: ImmutablePropTypes.list,
+    multiColumn: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
@@ -51,7 +52,7 @@ class Favourites extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, accountIds } = this.props;
+    const { intl, accountIds, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -74,6 +75,7 @@ class Favourites extends ImmutablePureComponent {
         <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..04c1f3635 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 (
@@ -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..39fdffae6 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 (
@@ -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..493f86a66 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 (
@@ -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/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
index d39505f46..b64b4bf13 100644
--- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
@@ -155,6 +155,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..b01c8cced 100644
--- a/app/javascript/flavours/glitch/features/home_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/index.js
@@ -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/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js
index 8c3d0af51..f4b926e3c 100644
--- a/app/javascript/flavours/glitch/features/list_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/list_timeline/index.js
@@ -212,6 +212,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..8d1b9d3ff 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 (
@@ -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..e5a8010b4 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 (
@@ -62,6 +63,7 @@ class Mutes extends ImmutablePureComponent {
           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..785a7dc51 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>
diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js
index 8d406ddf4..b0db90c2c 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,7 +43,7 @@ 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}>
@@ -51,6 +52,7 @@ class PinnedStatuses extends ImmutablePureComponent {
           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..b64c634ac 100644
--- a/app/javascript/flavours/glitch/features/public_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/public_timeline/index.js
@@ -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..808b25b9e 100644
--- a/app/javascript/flavours/glitch/features/reblogs/index.js
+++ b/app/javascript/flavours/glitch/features/reblogs/index.js
@@ -27,6 +27,7 @@ class Reblogs extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     accountIds: ImmutablePropTypes.list,
+    multiColumn: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
@@ -51,7 +52,7 @@ class Reblogs extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, accountIds } = this.props;
+    const { intl, accountIds, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -75,6 +76,7 @@ class Reblogs extends ImmutablePureComponent {
         <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/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index 117ce4c55..53334835d 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -46,6 +46,28 @@ const MODAL_COMPONENTS = {
   'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
 };
 
+let cachedScrollbarWidth = null;
+
+export const getScrollbarWidth = () => {
+  if (cachedScrollbarWidth !== null) {
+    return cachedScrollbarWidth;
+  }
+
+  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;
+  cachedScrollbarWidth = scrollbarWidth;
+  outer.parentNode.removeChild(outer);
+
+  return scrollbarWidth;
+};
+
 export default class ModalRoot extends React.PureComponent {
 
   static propTypes = {
@@ -61,8 +83,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/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..ed02118f0 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: 100%;
+      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..32f587ca0 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%;
@@ -30,6 +31,7 @@
       }
 
       &__inner {
+        position: fixed;
         width: 285px;
         pointer-events: auto;
         height: 100%;
@@ -83,7 +85,6 @@
   flex-direction: column;
   width: 100%;
   height: 100%;
-  background: darken($ui-base-color, 7%);
 }
 
 .column {
diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss
index 1d8055fe5..fdee67ed1 100644
--- a/app/javascript/flavours/glitch/styles/components/single_column.scss
+++ b/app/javascript/flavours/glitch/styles/components/single_column.scss
@@ -127,6 +127,10 @@
     top: 15px;
   }
 
+  .scrollable {
+    overflow: visible;
+  }
+
   @media screen and (min-width: $no-gap-breakpoint) {
     padding: 10px 0;
   }