about summary refs log tree commit diff
path: root/app/javascript/mastodon/containers/mastodon.js
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2017-05-03 02:04:16 +0200
committerGitHub <noreply@github.com>2017-05-03 02:04:16 +0200
commitf5bf5ebb82e3af420dcd23d602b1be6cc86838e1 (patch)
tree92eef08642a038cf44ccbc6d16a884293e7a0814 /app/javascript/mastodon/containers/mastodon.js
parent26bc5915727e0a0173c03cb49f5193dd612fb888 (diff)
Replace sprockets/browserify with Webpack (#2617)
* Replace browserify with webpack

* Add react-intl-translations-manager

* Do not minify in development, add offline-plugin for ServiceWorker background cache updates

* Adjust tests and dependencies

* Fix production deployments

* Fix tests

* More optimizations

* Improve travis cache for npm stuff

* Re-run travis

* Add back support for custom.scss as before

* Remove offline-plugin and babili

* Fix issue with Immutable.List().unshift(...values) not working as expected

* Make travis load schema instead of running all migrations in sequence

* Fix missing React import in WarningContainer. Optimize rendering performance by using ImmutablePureComponent instead of
React.PureComponent. ImmutablePureComponent uses Immutable.is() to compare props. Replace dynamic callback bindings in
<UI />

* Add react definitions to places that use JSX

* Add Procfile.dev for running rails, webpack and streaming API at the same time
Diffstat (limited to 'app/javascript/mastodon/containers/mastodon.js')
-rw-r--r--app/javascript/mastodon/containers/mastodon.js314
1 files changed, 314 insertions, 0 deletions
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
new file mode 100644
index 000000000..637199686
--- /dev/null
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -0,0 +1,314 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import PropTypes from 'prop-types';
+import configureStore from '../store/configureStore';
+import {
+  refreshTimelineSuccess,
+  updateTimeline,
+  deleteFromTimelines,
+  refreshTimeline,
+  connectTimeline,
+  disconnectTimeline
+} from '../actions/timelines';
+import { showOnboardingOnce } from '../actions/onboarding';
+import { updateNotifications, refreshNotifications } from '../actions/notifications';
+import createBrowserHistory from 'history/lib/createBrowserHistory';
+import {
+  applyRouterMiddleware,
+  useRouterHistory,
+  Router,
+  Route,
+  IndexRedirect,
+  IndexRoute
+} from 'react-router';
+import { useScroll } from 'react-router-scroll';
+import UI from '../features/ui';
+import Status from '../features/status';
+import GettingStarted from '../features/getting_started';
+import PublicTimeline from '../features/public_timeline';
+import CommunityTimeline from '../features/community_timeline';
+import AccountTimeline from '../features/account_timeline';
+import HomeTimeline from '../features/home_timeline';
+import Compose from '../features/compose';
+import Followers from '../features/followers';
+import Following from '../features/following';
+import Reblogs from '../features/reblogs';
+import Favourites from '../features/favourites';
+import HashtagTimeline from '../features/hashtag_timeline';
+import Notifications from '../features/notifications';
+import FollowRequests from '../features/follow_requests';
+import GenericNotFound from '../features/generic_not_found';
+import FavouritedStatuses from '../features/favourited_statuses';
+import Blocks from '../features/blocks';
+import Mutes from '../features/mutes';
+import Report from '../features/report';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import ar from 'react-intl/locale-data/ar';
+import en from 'react-intl/locale-data/en';
+import de from 'react-intl/locale-data/de';
+import eo from 'react-intl/locale-data/eo';
+import es from 'react-intl/locale-data/es';
+import fa from 'react-intl/locale-data/fa';
+import fi from 'react-intl/locale-data/fi';
+import fr from 'react-intl/locale-data/fr';
+import he from 'react-intl/locale-data/he';
+import hu from 'react-intl/locale-data/hu';
+import it from 'react-intl/locale-data/it';
+import ja from 'react-intl/locale-data/ja';
+import pt from 'react-intl/locale-data/pt';
+import nl from 'react-intl/locale-data/nl';
+import no from 'react-intl/locale-data/no';
+import ru from 'react-intl/locale-data/ru';
+import uk from 'react-intl/locale-data/uk';
+import zh from 'react-intl/locale-data/zh';
+import bg from 'react-intl/locale-data/bg';
+import id from 'react-intl/locale-data/id';
+import getMessagesForLocale from '../locales';
+import { hydrateStore } from '../actions/store';
+import createStream from '../stream';
+
+const store = configureStore();
+const initialState = JSON.parse(document.getElementById("initial-state").textContent);
+store.dispatch(hydrateStore(initialState));
+
+const browserHistory = useRouterHistory(createBrowserHistory)({
+  basename: '/web'
+});
+
+addLocaleData([
+  ...en,
+  ...ar,
+  ...de,
+  ...eo,
+  ...es,
+  ...fa,
+  ...fi,
+  ...fr,
+  ...he,
+  ...hu,
+  ...it,
+  ...ja,
+  ...pt,
+  ...nl,
+  ...no,
+  ...ru,
+  ...uk,
+  ...zh,
+  ...bg,
+  ...id,
+]);
+
+const getTopWhenReplacing = (previous, { location }) => location && location.action === 'REPLACE' && [0, 0];
+
+const hiddenColumnContainerStyle = {
+  position: 'absolute',
+  left: '0',
+  top:  '0',
+  visibility: 'hidden'
+};
+
+class Container extends React.PureComponent {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      renderedPersistents: [],
+      unrenderedPersistents: [],
+    };
+  }
+
+  componentWillMount () {
+    this.unlistenHistory = null;
+
+    this.setState(() => {
+      return {
+        mountImpersistent: false,
+        renderedPersistents: [],
+        unrenderedPersistents: [
+          {pathname: '/timelines/home', component: HomeTimeline},
+          {pathname: '/timelines/public', component: PublicTimeline},
+          {pathname: '/timelines/public/local', component: CommunityTimeline},
+
+          {pathname: '/notifications', component: Notifications},
+          {pathname: '/favourites', component: FavouritedStatuses}
+        ],
+      };
+    }, () => {
+      if (this.unlistenHistory) {
+        return;
+      }
+
+      this.unlistenHistory = browserHistory.listen(location => {
+        const pathname = location.pathname.replace(/\/$/, '').toLowerCase();
+
+        this.setState(oldState => {
+          let persistentMatched = false;
+
+          const newState = {
+            renderedPersistents: oldState.renderedPersistents.map(persistent => {
+              const givenMatched = persistent.pathname === pathname;
+
+              if (givenMatched) {
+                persistentMatched = true;
+              }
+
+              return {
+                hidden: !givenMatched,
+                pathname: persistent.pathname,
+                component: persistent.component
+              };
+            }),
+          };
+
+          if (!persistentMatched) {
+            newState.unrenderedPersistents = [];
+
+            oldState.unrenderedPersistents.forEach(persistent => {
+              if (persistent.pathname === pathname) {
+                persistentMatched = true;
+
+                newState.renderedPersistents.push({
+                  hidden: false,
+                  pathname: persistent.pathname,
+                  component: persistent.component
+                });
+              } else {
+                newState.unrenderedPersistents.push(persistent);
+              }
+            });
+          }
+
+          newState.mountImpersistent = !persistentMatched;
+
+          return newState;
+        });
+      });
+    });
+  }
+
+  componentWillUnmount () {
+    if (this.unlistenHistory) {
+      this.unlistenHistory();
+    }
+
+    this.unlistenHistory = "done";
+  }
+
+  render () {
+    // Hide some components rather than unmounting them to allow to show again
+    // quickly and keep the view state such as the scrolled offset.
+    const persistentsView = this.state.renderedPersistents.map((persistent) =>
+      <div aria-hidden={persistent.hidden} key={persistent.pathname} className='mastodon-column-container' style={persistent.hidden ? hiddenColumnContainerStyle : null}>
+        <persistent.component shouldUpdateScroll={persistent.hidden ? Function.prototype : getTopWhenReplacing} />
+      </div>
+    );
+
+    return (
+      <UI>
+        {this.state.mountImpersistent && this.props.children}
+        {persistentsView}
+      </UI>
+    );
+  }
+}
+
+Container.propTypes = {
+  children: PropTypes.node,
+};
+
+class Mastodon extends React.Component {
+
+  componentDidMount() {
+    const { locale }  = this.props;
+    const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
+    const accessToken = store.getState().getIn(['meta', 'access_token']);
+
+    this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
+
+      connected () {
+        store.dispatch(connectTimeline('home'));
+      },
+
+      disconnected () {
+        store.dispatch(disconnectTimeline('home'));
+      },
+
+      received (data) {
+        switch(data.event) {
+        case 'update':
+          store.dispatch(updateTimeline('home', JSON.parse(data.payload)));
+          break;
+        case 'delete':
+          store.dispatch(deleteFromTimelines(data.payload));
+          break;
+        case 'notification':
+          store.dispatch(updateNotifications(JSON.parse(data.payload), getMessagesForLocale(locale), locale));
+          break;
+        }
+      },
+
+      reconnected () {
+        store.dispatch(connectTimeline('home'));
+        store.dispatch(refreshTimeline('home'));
+        store.dispatch(refreshNotifications());
+      }
+
+    });
+
+    // Desktop notifications
+    if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
+      Notification.requestPermission();
+    }
+
+    store.dispatch(showOnboardingOnce());
+  }
+
+  componentWillUnmount () {
+    if (typeof this.subscription !== 'undefined') {
+      this.subscription.close();
+      this.subscription = null;
+    }
+  }
+
+  render () {
+    const { locale } = this.props;
+
+    return (
+      <IntlProvider locale={locale} messages={getMessagesForLocale(locale)}>
+        <Provider store={store}>
+          <Router history={browserHistory} render={applyRouterMiddleware(useScroll())}>
+            <Route path='/' component={Container}>
+              <IndexRedirect to='/getting-started' />
+              <Route path='getting-started' component={GettingStarted} />
+              <Route path='timelines/tag/:id' component={HashtagTimeline} />
+
+              <Route path='statuses/new' component={Compose} />
+              <Route path='statuses/:statusId' component={Status} />
+              <Route path='statuses/:statusId/reblogs' component={Reblogs} />
+              <Route path='statuses/:statusId/favourites' component={Favourites} />
+
+              <Route path='accounts/:accountId' component={AccountTimeline} />
+              <Route path='accounts/:accountId/followers' component={Followers} />
+              <Route path='accounts/:accountId/following' component={Following} />
+
+              <Route path='follow_requests' component={FollowRequests} />
+              <Route path='blocks' component={Blocks} />
+              <Route path='mutes' component={Mutes} />
+              <Route path='report' component={Report} />
+
+              <Route path='*' component={GenericNotFound} />
+            </Route>
+          </Router>
+        </Provider>
+      </IntlProvider>
+    );
+  }
+
+}
+
+Mastodon.propTypes = {
+  locale: PropTypes.string.isRequired
+};
+
+export default Mastodon;