about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch
diff options
context:
space:
mode:
authorThibG <thib@sitedethib.com>2019-12-08 21:27:36 +0100
committerGitHub <noreply@github.com>2019-12-08 21:27:36 +0100
commit246addd5b33a172600342af3fb6fb5e4c80ad95e (patch)
tree98b87efd3f5f3d41d75482707924fde708c074ec /app/javascript/flavours/glitch
parent7d59e25fbd7a0209c11cbe6aac4400bbca4de82b (diff)
parent49027ee948bc76cc89214f8f340d353763796f1d (diff)
Merge pull request #1251 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app/javascript/flavours/glitch')
-rw-r--r--app/javascript/flavours/glitch/actions/notifications.js2
-rw-r--r--app/javascript/flavours/glitch/components/poll.js28
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js11
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/column_settings.js11
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/follow_request.js130
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/notification.js13
-rw-r--r--app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js16
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js13
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/video_modal.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/notifications.js11
-rw-r--r--app/javascript/flavours/glitch/reducers/push_notifications.js1
-rw-r--r--app/javascript/flavours/glitch/reducers/settings.js3
-rw-r--r--app/javascript/flavours/glitch/reducers/user_lists.js11
-rw-r--r--app/javascript/flavours/glitch/styles/components/status.scss7
-rw-r--r--app/javascript/flavours/glitch/styles/polls.scss21
-rw-r--r--app/javascript/flavours/glitch/util/stream.js2
16 files changed, 261 insertions, 21 deletions
diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js
index 7effb07d1..940f3c3d4 100644
--- a/app/javascript/flavours/glitch/actions/notifications.js
+++ b/app/javascript/flavours/glitch/actions/notifications.js
@@ -121,7 +121,7 @@ const excludeTypesFromSettings = state => state.getIn(['settings', 'notification
 
 
 const excludeTypesFromFilter = filter => {
-  const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']);
+  const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
   return allTypes.filterNot(item => item === filter).toJS();
 };
 
diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js
index 2d2a7cbe0..62965df94 100644
--- a/app/javascript/flavours/glitch/components/poll.js
+++ b/app/javascript/flavours/glitch/components/poll.js
@@ -67,9 +67,7 @@ class Poll extends ImmutablePureComponent {
     }
   }
 
-  handleOptionChange = e => {
-    const { target: { value } } = e;
-
+  _toggleOption = value => {
     if (this.props.poll.get('multiple')) {
       const tmp = { ...this.state.selected };
       if (tmp[value]) {
@@ -83,8 +81,20 @@ class Poll extends ImmutablePureComponent {
       tmp[value] = true;
       this.setState({ selected: tmp });
     }
+  }
+
+  handleOptionChange = ({ target: { value } }) => {
+    this._toggleOption(value);
   };
 
+  handleOptionKeyPress = (e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      this._toggleOption(e.target.getAttribute('data-index'));
+      e.stopPropagation();
+      e.preventDefault();
+    }
+  }
+
   handleVote = () => {
     if (this.props.disabled) {
       return;
@@ -135,7 +145,17 @@ class Poll extends ImmutablePureComponent {
             disabled={disabled}
           />
 
-          {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
+          {!showResults && (
+            <span
+              className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
+              tabIndex='0'
+              role={poll.get('multiple') ? 'checkbox' : 'radio'}
+              onKeyPress={this.handleOptionKeyPress}
+              aria-checked={active}
+              aria-label={option.get('title')}
+              data-index={optionIndex}
+            />
+          )}
           {showResults && <span className='poll__number'>
             {!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
             {Math.round(percent)}%
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index e65a68b4d..6b4aff616 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -232,9 +232,18 @@ class Header extends ImmutablePureComponent {
     const content          = { __html: account.get('note_emojified') };
     const displayNameHtml = { __html: account.get('display_name_html') };
     const fields          = account.get('fields');
-    const badge           = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
     const acct            = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
 
+    let badge;
+
+    if (account.get('bot')) {
+      badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>);
+    } else if (account.get('group')) {
+      badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
+    } else {
+      badge = null;
+    }
+
     return (
       <div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
         <div className='account__header__image'>
diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
index e29bd61f5..e4d5d0eda 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
@@ -58,6 +58,17 @@ export default class ColumnSettings extends React.PureComponent {
           </div>
         </div>
 
+        <div role='group' aria-labelledby='notifications-follow-request'>
+          <span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
+
+          <div className='column-settings__row'>
+            <SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />}
+            <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
+            <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
         <div role='group' aria-labelledby='notifications-favourite'>
           <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
 
diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow_request.js b/app/javascript/flavours/glitch/features/notifications/components/follow_request.js
new file mode 100644
index 000000000..d73dac434
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/notifications/components/follow_request.js
@@ -0,0 +1,130 @@
+import React, { Fragment } from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import Avatar from 'flavours/glitch/components/avatar';
+import DisplayName from 'flavours/glitch/components/display_name';
+import Permalink from 'flavours/glitch/components/permalink';
+import IconButton from 'flavours/glitch/components/icon_button';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import NotificationOverlayContainer from '../containers/overlay_container';
+import { HotKeys } from 'react-hotkeys';
+import Icon from 'flavours/glitch/components/icon';
+
+const messages = defineMessages({
+  authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
+  reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
+});
+
+export default @injectIntl
+class FollowRequest extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.map.isRequired,
+    onAuthorize: PropTypes.func.isRequired,
+    onReject: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    notification: ImmutablePropTypes.map.isRequired,
+  };
+
+  handleMoveUp = () => {
+    const { notification, onMoveUp } = this.props;
+    onMoveUp(notification.get('id'));
+  }
+
+  handleMoveDown = () => {
+    const { notification, onMoveDown } = this.props;
+    onMoveDown(notification.get('id'));
+  }
+
+  handleOpen = () => {
+    this.handleOpenProfile();
+  }
+
+  handleOpenProfile = () => {
+    const { notification } = this.props;
+    this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`);
+  }
+
+  handleMention = e => {
+    e.preventDefault();
+
+    const { notification, onMention } = this.props;
+    onMention(notification.get('account'), this.context.router.history);
+  }
+
+  getHandlers () {
+    return {
+      moveUp: this.handleMoveUp,
+      moveDown: this.handleMoveDown,
+      open: this.handleOpen,
+      openProfile: this.handleOpenProfile,
+      mention: this.handleMention,
+      reply: this.handleMention,
+    };
+  }
+
+  render () {
+    const { intl, hidden, account, onAuthorize, onReject, notification } = this.props;
+
+    if (!account) {
+      return <div />;
+    }
+
+    if (hidden) {
+      return (
+        <Fragment>
+          {account.get('display_name')}
+          {account.get('username')}
+        </Fragment>
+      );
+    }
+
+    //  Links to the display name.
+    const displayName = account.get('display_name_html') || account.get('username');
+    const link = (
+      <bdi><Permalink
+        className='notification__display-name'
+        href={account.get('url')}
+        title={account.get('acct')}
+        to={`/accounts/${account.get('id')}`}
+        dangerouslySetInnerHTML={{ __html: displayName }}
+      /></bdi>
+    );
+
+    return (
+      <HotKeys handlers={this.getHandlers()}>
+        <div className='notification notification-follow-request focusable' tabIndex='0'>
+          <div className='notification__message'>
+            <div className='notification__favourite-icon-wrapper'>
+              <Icon id='user' fixedWidth />
+            </div>
+
+            <FormattedMessage
+              id='notification.follow_request'
+              defaultMessage='{name} has requested to follow you'
+              values={{ name: link }}
+            />
+          </div>
+
+          <div className='account'>
+            <div className='account__wrapper'>
+              <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
+                <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
+                <DisplayName account={account} />
+              </Permalink>
+
+              <div className='account__relationship'>
+                <IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
+                <IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} />
+              </div>
+            </div>
+          </div>
+
+          <NotificationOverlayContainer notification={notification} />
+        </div>
+      </HotKeys>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js
index 5c5bbf604..62fc28386 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/notification.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js
@@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 //  Our imports,
 import StatusContainer from 'flavours/glitch/containers/status_container';
 import NotificationFollow from './follow';
+import NotificationFollowRequestContainer from '../containers/follow_request_container';
 
 export default class Notification extends ImmutablePureComponent {
 
@@ -47,6 +48,18 @@ export default class Notification extends ImmutablePureComponent {
           onMention={onMention}
         />
       );
+    case 'follow_request':
+      return (
+        <NotificationFollowRequestContainer
+          hidden={hidden}
+          id={notification.get('id')}
+          account={notification.get('account')}
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+        />
+      );
     case 'mention':
       return (
         <StatusContainer
diff --git a/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js b/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js
new file mode 100644
index 000000000..82357adfb
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux';
+import { makeGetAccount } from 'flavours/glitch/selectors';
+import FollowRequest from '../components/follow_request';
+import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts';
+
+const mapDispatchToProps = (dispatch, { account }) => ({
+  onAuthorize () {
+    dispatch(authorizeFollowRequest(account.get('id')));
+  },
+
+  onReject () {
+    dispatch(rejectFollowRequest(account.get('id')));
+  },
+});
+
+export default connect(null, mapDispatchToProps)(FollowRequest);
diff --git a/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js b/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js
index 189f403bd..c30427896 100644
--- a/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js
+++ b/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js
@@ -4,12 +4,10 @@ import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
 import { connect } from 'react-redux';
 import { NavLink, withRouter } from 'react-router-dom';
 import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
-import { me } from 'flavours/glitch/util/initial_state';
 import { List as ImmutableList } from 'immutable';
 import { FormattedMessage } from 'react-intl';
 
 const mapStateToProps = state => ({
-  locked: state.getIn(['accounts', me, 'locked']),
   count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
 });
 
@@ -19,22 +17,19 @@ class FollowRequestsNavLink extends React.Component {
 
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
-    locked: PropTypes.bool,
     count: PropTypes.number.isRequired,
   };
 
   componentDidMount () {
-    const { dispatch, locked } = this.props;
+    const { dispatch } = this.props;
 
-    if (locked) {
-      dispatch(fetchFollowRequests());
-    }
+    dispatch(fetchFollowRequests());
   }
 
   render () {
-    const { locked, count } = this.props;
+    const { count } = this.props;
 
-    if (!locked || count === 0) {
+    if (count === 0) {
       return null;
     }
 
diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
index ef69f60f4..e7309021e 100644
--- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
@@ -5,7 +5,7 @@ import Video from 'flavours/glitch/features/video';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { FormattedMessage } from 'react-intl';
 import classNames from 'classnames';
-import Icon from 'mastodon/components/icon';
+import Icon from 'flavours/glitch/components/icon';
 
 export default class VideoModal extends ImmutablePureComponent {
 
diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js
index 8d5c6785c..3623e90da 100644
--- a/app/javascript/flavours/glitch/reducers/notifications.js
+++ b/app/javascript/flavours/glitch/reducers/notifications.js
@@ -20,6 +20,8 @@ import {
 import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
+  FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
+  FOLLOW_REQUEST_REJECT_SUCCESS,
 } from 'flavours/glitch/actions/accounts';
 import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks';
 import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
@@ -113,8 +115,8 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
   });
 };
 
-const filterNotifications = (state, accountIds) => {
-  const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')));
+const filterNotifications = (state, accountIds, type) => {
+  const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
   return state.update('items', helper).update('pendingItems', helper);
 };
 
@@ -227,6 +229,11 @@ export default function notifications(state = initialState, action) {
     return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
   case DOMAIN_BLOCK_SUCCESS:
     return filterNotifications(state, action.accounts);
+  case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
+  case FOLLOW_REQUEST_REJECT_SUCCESS:
+    return filterNotifications(state, [action.id], 'follow_request');
+  case ACCOUNT_MUTE_SUCCESS:
+    return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
   case NOTIFICATIONS_CLEAR:
     return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
   case TIMELINE_DELETE:
diff --git a/app/javascript/flavours/glitch/reducers/push_notifications.js b/app/javascript/flavours/glitch/reducers/push_notifications.js
index e87e8fc1a..117fb5167 100644
--- a/app/javascript/flavours/glitch/reducers/push_notifications.js
+++ b/app/javascript/flavours/glitch/reducers/push_notifications.js
@@ -6,6 +6,7 @@ const initialState = Immutable.Map({
   subscription: null,
   alerts: new Immutable.Map({
     follow: false,
+    follow_request: false,
     favourite: false,
     reblog: false,
     mention: false,
diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js
index 9be27a02f..ef99ad552 100644
--- a/app/javascript/flavours/glitch/reducers/settings.js
+++ b/app/javascript/flavours/glitch/reducers/settings.js
@@ -34,6 +34,7 @@ const initialState = ImmutableMap({
   notifications: ImmutableMap({
     alerts: ImmutableMap({
       follow: true,
+      follow_request: false,
       favourite: true,
       reblog: true,
       mention: true,
@@ -48,6 +49,7 @@ const initialState = ImmutableMap({
 
     shows: ImmutableMap({
       follow: true,
+      follow_request: false,
       favourite: true,
       reblog: true,
       mention: true,
@@ -56,6 +58,7 @@ const initialState = ImmutableMap({
 
     sounds: ImmutableMap({
       follow: true,
+      follow_request: false,
       favourite: true,
       reblog: true,
       mention: true,
diff --git a/app/javascript/flavours/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js
index b4e1d1eae..c8a6f524e 100644
--- a/app/javascript/flavours/glitch/reducers/user_lists.js
+++ b/app/javascript/flavours/glitch/reducers/user_lists.js
@@ -1,4 +1,7 @@
 import {
+  NOTIFICATIONS_UPDATE,
+} from '../actions/notifications';
+import {
   FOLLOWERS_FETCH_SUCCESS,
   FOLLOWERS_EXPAND_SUCCESS,
   FOLLOWING_FETCH_SUCCESS,
@@ -53,6 +56,12 @@ const appendToList = (state, type, id, accounts, next) => {
   });
 };
 
+const normalizeFollowRequest = (state, notification) => {
+  return state.updateIn(['follow_requests', 'items'], list => {
+    return list.filterNot(item => item === notification.account.id).unshift(notification.account.id);
+  });
+};
+
 export default function userLists(state = initialState, action) {
   switch(action.type) {
   case FOLLOWERS_FETCH_SUCCESS:
@@ -67,6 +76,8 @@ export default function userLists(state = initialState, action) {
     return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
   case FAVOURITES_FETCH_SUCCESS:
     return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+  case NOTIFICATIONS_UPDATE:
+    return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
   case FOLLOW_REQUESTS_FETCH_SUCCESS:
     return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
   case FOLLOW_REQUESTS_EXPAND_SUCCESS:
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index 77d67576b..00f947cdc 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -232,7 +232,9 @@
 }
 
 .notif-cleaning {
-  .status, .notification-follow {
+  .status,
+  .notification-follow,
+  .notification-follow-request {
     padding-right: ($dismiss-overlay-width + 0.5rem);
   }
 }
@@ -256,7 +258,8 @@
   position: absolute;
 }
 
-.notification-follow {
+.notification-follow,
+.notification-follow-request {
   position: relative;
 
   // same like Status
diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss
index 95d8e510c..49d0e7f71 100644
--- a/app/javascript/flavours/glitch/styles/polls.scss
+++ b/app/javascript/flavours/glitch/styles/polls.scss
@@ -98,6 +98,23 @@
       border-color: $valid-value-color;
       background: $valid-value-color;
     }
+
+    &:active,
+    &:focus,
+    &:hover {
+      border-width: 4px;
+      background: none;
+    }
+
+    &::-moz-focus-inner {
+      outline: 0 !important;
+      border: 0;
+    }
+
+    &:focus,
+    &:active {
+      outline: 0 !important;
+    }
   }
 
   &__number {
@@ -168,6 +185,10 @@
     select {
       width: 100%;
       flex: 1 1 50%;
+
+      &:focus {
+        border-color: $highlight-text-color;
+      }
     }
   }
 
diff --git a/app/javascript/flavours/glitch/util/stream.js b/app/javascript/flavours/glitch/util/stream.js
index c4642344f..50f90d44c 100644
--- a/app/javascript/flavours/glitch/util/stream.js
+++ b/app/javascript/flavours/glitch/util/stream.js
@@ -1,4 +1,4 @@
-import WebSocketClient from 'websocket.js';
+import WebSocketClient from '@gamestdio/websocket';
 
 const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));