about summary refs log tree commit diff
path: root/app/javascript/mastodon/features/ui/util
diff options
context:
space:
mode:
authorSorin Davidoi <sorin.davidoi@gmail.com>2017-07-08 00:06:02 +0200
committerEugen Rochko <eugen@zeonfederated.com>2017-07-08 00:06:02 +0200
commit348d6f5e7551e632e7dea41e61c40f79aac59be9 (patch)
tree54cc599e3509457c25603653d5490bd96efe39c6 /app/javascript/mastodon/features/ui/util
parent00df69bc89f1b5ffdf290bde8359b3854e2b1395 (diff)
Lazy load components (#3879)
* feat: Lazy-load routes

* feat: Lazy-load modals

* feat: Lazy-load columns

* refactor: Simplify Bundle API

* feat: Optimize bundles

* feat: Prevent flashing the waiting state

* feat: Preload commonly used bundles

* feat: Lazy load Compose reducers

* feat: Lazy load Notifications reducer

* refactor: Move all dynamic imports into one file

* fix: Minor bugs

* fix: Manually hydrate the lazy-loaded reducers

* refactor: Move all dynamic imports to async-components

* fix: Loading modal style

* refactor: Avoid converting the raw state for each lazy hydration

* refactor: Remove unused component

* refactor: Maintain modal name

* fix: Add as=script to preload link

* chore: Fix lint error

* fix(components/bundle): Check if timestamp is set when computing elapsed

* fix: Load compose reducers for the onboarding modal
Diffstat (limited to 'app/javascript/mastodon/features/ui/util')
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js143
-rw-r--r--app/javascript/mastodon/features/ui/util/react_router_helpers.js65
2 files changed, 208 insertions, 0 deletions
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
new file mode 100644
index 000000000..c9f81136d
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -0,0 +1,143 @@
+import { store } from '../../../containers/mastodon';
+import { injectAsyncReducer } from '../../../store/configureStore';
+
+// NOTE: When lazy-loading reducers, make sure to add them
+// to application.html.haml (if the component is preloaded there)
+
+export function EmojiPicker () {
+  return import(/* webpackChunkName: "emojione_picker" */'emojione-picker');
+}
+
+export function Compose () {
+  return Promise.all([
+    import(/* webpackChunkName: "features/compose" */'../../compose'),
+    import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'),
+    import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'),
+    import(/* webpackChunkName: "reducers/search" */'../../../reducers/search'),
+  ]).then(([component, composeReducer, mediaAttachmentsReducer, searchReducer]) => {
+    injectAsyncReducer(store, 'compose', composeReducer.default);
+    injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default);
+    injectAsyncReducer(store, 'search', searchReducer.default);
+
+    return component;
+  });
+}
+
+export function Notifications () {
+  return Promise.all([
+    import(/* webpackChunkName: "features/notifications" */'../../notifications'),
+    import(/* webpackChunkName: "reducers/notifications" */'../../../reducers/notifications'),
+  ]).then(([component, notificationsReducer]) => {
+    injectAsyncReducer(store, 'notifications', notificationsReducer.default);
+
+    return component;
+  });
+}
+
+export function HomeTimeline () {
+  return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
+}
+
+export function PublicTimeline () {
+  return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
+}
+
+export function CommunityTimeline () {
+  return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
+}
+
+export function HashtagTimeline () {
+  return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
+}
+
+export function Status () {
+  return import(/* webpackChunkName: "features/status" */'../../status');
+}
+
+export function GettingStarted () {
+  return import(/* webpackChunkName: "features/getting_started" */'../../getting_started');
+}
+
+export function AccountTimeline () {
+  return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline');
+}
+
+export function AccountGallery () {
+  return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery');
+}
+
+export function Followers () {
+  return import(/* webpackChunkName: "features/followers" */'../../followers');
+}
+
+export function Following () {
+  return import(/* webpackChunkName: "features/following" */'../../following');
+}
+
+export function Reblogs () {
+  return import(/* webpackChunkName: "features/reblogs" */'../../reblogs');
+}
+
+export function Favourites () {
+  return import(/* webpackChunkName: "features/favourites" */'../../favourites');
+}
+
+export function FollowRequests () {
+  return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
+}
+
+export function GenericNotFound () {
+  return import(/* webpackChunkName: "features/generic_not_found" */'../../generic_not_found');
+}
+
+export function FavouritedStatuses () {
+  return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses');
+}
+
+export function Blocks () {
+  return import(/* webpackChunkName: "features/blocks" */'../../blocks');
+}
+
+export function Mutes () {
+  return import(/* webpackChunkName: "features/mutes" */'../../mutes');
+}
+
+export function MediaModal () {
+  return import(/* webpackChunkName: "modals/media_modal" */'../components/media_modal');
+}
+
+export function OnboardingModal () {
+  return Promise.all([
+    import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'),
+    import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'),
+    import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'),
+  ]).then(([component, composeReducer, mediaAttachmentsReducer]) => {
+    injectAsyncReducer(store, 'compose', composeReducer.default);
+    injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default);
+    return component;
+  });
+}
+
+export function VideoModal () {
+  return import(/* webpackChunkName: "modals/video_modal" */'../components/video_modal');
+}
+
+export function BoostModal () {
+  return import(/* webpackChunkName: "modals/boost_modal" */'../components/boost_modal');
+}
+
+export function ConfirmationModal () {
+  return import(/* webpackChunkName: "modals/confirmation_modal" */'../components/confirmation_modal');
+}
+
+export function ReportModal () {
+  return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
+}
+
+export function MediaGallery () {
+  return import(/* webpackChunkName: "status/MediaGallery" */'../../../components/media_gallery');
+}
+
+export function VideoPlayer () {
+  return import(/* webpackChunkName: "status/VideoPlayer" */'../../../components/video_player');
+}
diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
new file mode 100644
index 000000000..e33a6df6f
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Switch from 'react-router-dom/Switch';
+import Route from 'react-router-dom/Route';
+
+import ColumnLoading from '../components/column_loading';
+import BundleColumnError from '../components/bundle_column_error';
+import BundleContainer from '../containers/bundle_container';
+
+// Small wrapper to pass multiColumn to the route components
+export const WrappedSwitch = ({ multiColumn, children }) => (
+  <Switch>
+    {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
+  </Switch>
+);
+
+WrappedSwitch.propTypes = {
+  multiColumn: PropTypes.bool,
+  children: PropTypes.node,
+};
+
+// Small Wraper to extract the params from the route and pass
+// them to the rendered component, together with the content to
+// be rendered inside (the children)
+export class WrappedRoute extends React.Component {
+
+  static propTypes = {
+    component: PropTypes.func.isRequired,
+    content: PropTypes.node,
+    multiColumn: PropTypes.bool,
+  }
+
+  renderComponent = ({ match }) => {
+    this.match = match; // Needed for this.renderBundle
+
+    const { component } = this.props;
+
+    return (
+      <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
+        {this.renderBundle}
+      </BundleContainer>
+    );
+  }
+
+  renderLoading = () => {
+    return <ColumnLoading />;
+  }
+
+  renderError = (props) => {
+    return <BundleColumnError {...props} />;
+  }
+
+  renderBundle = (Component) => {
+    const { match: { params }, props: { content, multiColumn } } = this;
+
+    return <Component params={params} multiColumn={multiColumn}>{content}</Component>;
+  }
+
+  render () {
+    const { component: Component, content, ...rest } = this.props;
+
+    return <Route {...rest} render={this.renderComponent} />;
+  }
+
+}