From b75bf336bdb874eb9bbe3cec1a95ee59c2b3b54c Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 11 Oct 2022 11:39:52 +0200 Subject: Move more modules from flavours/glitch/utils to flavours/glitch --- app/javascript/flavours/glitch/actions/compose.js | 2 +- app/javascript/flavours/glitch/actions/markers.js | 2 +- .../flavours/glitch/actions/notifications.js | 2 +- .../actions/push_notifications/registerer.js | 2 +- .../flavours/glitch/actions/streaming.js | 2 +- .../flavours/glitch/actions/timelines.js | 2 +- app/javascript/flavours/glitch/base_polyfills.js | 47 ++++ app/javascript/flavours/glitch/compare_id.js | 11 + .../flavours/glitch/components/column.js | 2 +- .../flavours/glitch/components/media_gallery.js | 2 +- .../glitch/containers/dropdown_menu_container.js | 2 +- app/javascript/flavours/glitch/extra_polyfills.js | 5 + .../account_gallery/components/media_item.js | 2 +- .../features/compose/components/compose_form.js | 2 +- .../glitch/features/compose/components/dropdown.js | 2 +- .../glitch/features/compose/components/upload.js | 2 +- .../glitch/features/notifications/index.js | 2 +- .../glitch/features/ui/components/column.js | 4 +- .../glitch/features/ui/components/columns_area.js | 2 +- .../glitch/features/ui/components/tabs_bar.js | 2 +- .../flavours/glitch/features/ui/index.js | 2 +- app/javascript/flavours/glitch/is_mobile.js | 43 ++++ .../flavours/glitch/load_keyboard_extensions.js | 16 ++ app/javascript/flavours/glitch/load_polyfills.js | 42 ++++ app/javascript/flavours/glitch/main.js | 57 +++++ app/javascript/flavours/glitch/packs/about.js | 2 +- app/javascript/flavours/glitch/packs/home.js | 4 +- app/javascript/flavours/glitch/packs/public.js | 4 +- app/javascript/flavours/glitch/packs/settings.js | 4 +- app/javascript/flavours/glitch/packs/share.js | 2 +- app/javascript/flavours/glitch/performance.js | 31 +++ app/javascript/flavours/glitch/reducers/compose.js | 2 +- .../flavours/glitch/reducers/contexts.js | 2 +- .../flavours/glitch/reducers/conversations.js | 2 +- app/javascript/flavours/glitch/reducers/meta.js | 2 +- .../flavours/glitch/reducers/notifications.js | 2 +- .../flavours/glitch/reducers/settings.js | 2 +- .../flavours/glitch/reducers/timelines.js | 2 +- app/javascript/flavours/glitch/scroll.js | 32 +++ app/javascript/flavours/glitch/settings.js | 47 ++++ app/javascript/flavours/glitch/stream.js | 265 +++++++++++++++++++++ .../flavours/glitch/utils/base_polyfills.js | 47 ---- app/javascript/flavours/glitch/utils/compare_id.js | 11 - .../flavours/glitch/utils/extra_polyfills.js | 5 - app/javascript/flavours/glitch/utils/is_mobile.js | 43 ---- .../glitch/utils/load_keyboard_extensions.js | 16 -- .../flavours/glitch/utils/load_polyfills.js | 42 ---- app/javascript/flavours/glitch/utils/main.js | 57 ----- .../flavours/glitch/utils/performance.js | 31 --- app/javascript/flavours/glitch/utils/scroll.js | 32 --- app/javascript/flavours/glitch/utils/settings.js | 47 ---- app/javascript/flavours/glitch/utils/stream.js | 265 --------------------- app/javascript/flavours/glitch/utils/uuid.js | 3 - app/javascript/flavours/glitch/uuid.js | 3 + 54 files changed, 633 insertions(+), 633 deletions(-) create mode 100644 app/javascript/flavours/glitch/base_polyfills.js create mode 100644 app/javascript/flavours/glitch/compare_id.js create mode 100644 app/javascript/flavours/glitch/extra_polyfills.js create mode 100644 app/javascript/flavours/glitch/is_mobile.js create mode 100644 app/javascript/flavours/glitch/load_keyboard_extensions.js create mode 100644 app/javascript/flavours/glitch/load_polyfills.js create mode 100644 app/javascript/flavours/glitch/main.js create mode 100644 app/javascript/flavours/glitch/performance.js create mode 100644 app/javascript/flavours/glitch/scroll.js create mode 100644 app/javascript/flavours/glitch/settings.js create mode 100644 app/javascript/flavours/glitch/stream.js delete mode 100644 app/javascript/flavours/glitch/utils/base_polyfills.js delete mode 100644 app/javascript/flavours/glitch/utils/compare_id.js delete mode 100644 app/javascript/flavours/glitch/utils/extra_polyfills.js delete mode 100644 app/javascript/flavours/glitch/utils/is_mobile.js delete mode 100644 app/javascript/flavours/glitch/utils/load_keyboard_extensions.js delete mode 100644 app/javascript/flavours/glitch/utils/load_polyfills.js delete mode 100644 app/javascript/flavours/glitch/utils/main.js delete mode 100644 app/javascript/flavours/glitch/utils/performance.js delete mode 100644 app/javascript/flavours/glitch/utils/scroll.js delete mode 100644 app/javascript/flavours/glitch/utils/settings.js delete mode 100644 app/javascript/flavours/glitch/utils/stream.js delete mode 100644 app/javascript/flavours/glitch/utils/uuid.js create mode 100644 app/javascript/flavours/glitch/uuid.js (limited to 'app') diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 2f2fea590..0ef505363 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -3,7 +3,7 @@ import { CancelToken, isCancel } from 'axios'; import { throttle } from 'lodash'; import { search as emojiSearch } from 'flavours/glitch/utils/emoji/emoji_mart_search_light'; import { useEmoji } from './emojis'; -import { tagHistory } from 'flavours/glitch/utils/settings'; +import { tagHistory } from '../settings'; import { recoverHashtags } from 'flavours/glitch/utils/hashtag'; import resizeImage from 'flavours/glitch/utils/resize_image'; import { importFetchedAccounts } from './importer'; diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index 66ae19316..3b6a76bc4 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,6 +1,6 @@ import api from '../api'; import { debounce } from 'lodash'; -import compareId from 'flavours/glitch/utils/compare_id'; +import compareId from '../compare_id'; import { List as ImmutableList } from 'immutable'; export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 343d75908..158a5b7e4 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -13,7 +13,7 @@ import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from 'flavours/glitch/utils/html'; import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; -import compareId from 'flavours/glitch/utils/compare_id'; +import compareId from 'flavours/glitch/compare_id'; import { requestNotificationPermission } from 'flavours/glitch/utils/notifications'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; diff --git a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js index d55e91fd4..762fe260c 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js @@ -1,5 +1,5 @@ import api from '../../api'; -import { pushNotificationsSetting } from 'flavours/glitch/utils/settings'; +import { pushNotificationsSetting } from '../../settings'; import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; // Taken from https://www.npmjs.com/package/web-push diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index e41f5cf46..ffac1b258 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -1,6 +1,6 @@ // @ts-check -import { connectStream } from 'flavours/glitch/utils/stream'; +import { connectStream } from '../stream'; import { updateTimeline, deleteFromTimelines, diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index f812babbd..ef1e4dbbb 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -2,7 +2,7 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; import api, { getLinks } from 'flavours/glitch/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from 'flavours/glitch/utils/compare_id'; +import compareId from 'flavours/glitch/compare_id'; import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; import { toServerSideType } from 'flavours/glitch/utils/filters'; diff --git a/app/javascript/flavours/glitch/base_polyfills.js b/app/javascript/flavours/glitch/base_polyfills.js new file mode 100644 index 000000000..12096d902 --- /dev/null +++ b/app/javascript/flavours/glitch/base_polyfills.js @@ -0,0 +1,47 @@ +import 'intl'; +import 'intl/locale-data/jsonp/en'; +import 'es6-symbol/implement'; +import includes from 'array-includes'; +import assign from 'object-assign'; +import values from 'object.values'; +import isNaN from 'is-nan'; +import { decode as decodeBase64 } from './utils/base64'; +import promiseFinally from 'promise.prototype.finally'; + +if (!Array.prototype.includes) { + includes.shim(); +} + +if (!Object.assign) { + Object.assign = assign; +} + +if (!Object.values) { + values.shim(); +} + +if (!Number.isNaN) { + Number.isNaN = isNaN; +} + +promiseFinally.shim(); + +if (!HTMLCanvasElement.prototype.toBlob) { + const BASE64_MARKER = ';base64,'; + + Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { + value(callback, type = 'image/png', quality) { + const dataURL = this.toDataURL(type, quality); + let data; + + if (dataURL.indexOf(BASE64_MARKER) >= 0) { + const [, base64] = dataURL.split(BASE64_MARKER); + data = decodeBase64(base64); + } else { + [, data] = dataURL.split(','); + } + + callback(new Blob([data], { type })); + }, + }); +} diff --git a/app/javascript/flavours/glitch/compare_id.js b/app/javascript/flavours/glitch/compare_id.js new file mode 100644 index 000000000..66cf51c4b --- /dev/null +++ b/app/javascript/flavours/glitch/compare_id.js @@ -0,0 +1,11 @@ +export default function compareId (id1, id2) { + if (id1 === id2) { + return 0; + } + + if (id1.length === id2.length) { + return id1 > id2 ? 1 : -1; + } else { + return id1.length > id2.length ? 1 : -1; + } +}; diff --git a/app/javascript/flavours/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.js index 052939e8a..cf0e6d5e4 100644 --- a/app/javascript/flavours/glitch/components/column.js +++ b/app/javascript/flavours/glitch/components/column.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { supportsPassiveEvents } from 'detect-passive-events'; -import { scrollTop } from 'flavours/glitch/utils/scroll'; +import { scrollTop } from '../scroll'; export default class Column extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 4bcc1ff10..5414b4858 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { is } from 'immutable'; import IconButton from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { isIOS } from 'flavours/glitch/utils/is_mobile'; +import { isIOS } from '../is_mobile'; import classNames from 'classnames'; import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; import { debounce } from 'lodash'; diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index 6b02a56ad..b2dff63db 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -2,7 +2,7 @@ import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dro import { openModal, closeModal } from 'flavours/glitch/actions/modal'; import { connect } from 'react-redux'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; -import { isUserTouching } from 'flavours/glitch/utils/is_mobile'; +import { isUserTouching } from '../is_mobile'; const mapStateToProps = state => ({ dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), diff --git a/app/javascript/flavours/glitch/extra_polyfills.js b/app/javascript/flavours/glitch/extra_polyfills.js new file mode 100644 index 000000000..3acc55abd --- /dev/null +++ b/app/javascript/flavours/glitch/extra_polyfills.js @@ -0,0 +1,5 @@ +import 'intersection-observer'; +import 'requestidlecallback'; +import objectFitImages from 'object-fit-images'; + +objectFitImages(); diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index 7e75d7dfe..a16ee4806 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -2,7 +2,7 @@ import Blurhash from 'flavours/glitch/components/blurhash'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; -import { isIOS } from 'flavours/glitch/utils/is_mobile'; +import { isIOS } from 'flavours/glitch/is_mobile'; import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index 3a9fa01e1..44dd2693a 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -9,7 +9,7 @@ import EmojiPicker from 'flavours/glitch/features/emoji_picker'; import PollFormContainer from '../containers/poll_form_container'; import UploadFormContainer from '../containers/upload_form_container'; import WarningContainer from '../containers/warning_container'; -import { isMobile } from 'flavours/glitch/utils/is_mobile'; +import { isMobile } from 'flavours/glitch/is_mobile'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { countableText } from '../util/counter'; import OptionsContainer from '../containers/options_container'; diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index b072317b7..829f6d00f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -9,7 +9,7 @@ import IconButton from 'flavours/glitch/components/icon_button'; import DropdownMenu from './dropdown_menu'; // Utils. -import { isUserTouching } from 'flavours/glitch/utils/is_mobile'; +import { isUserTouching } from 'flavours/glitch/is_mobile'; import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; // The component. diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js index b926c455d..ade6f0437 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload.js @@ -6,7 +6,7 @@ import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; import Icon from 'flavours/glitch/components/icon'; -import { isUserTouching } from 'flavours/glitch/utils/is_mobile'; +import { isUserTouching } from 'flavours/glitch/is_mobile'; export default class Upload extends ImmutablePureComponent { diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 3c221732e..26eeba168 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -26,7 +26,7 @@ import { debounce } from 'lodash'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import LoadGap from 'flavours/glitch/components/load_gap'; import Icon from 'flavours/glitch/components/icon'; -import compareId from 'flavours/glitch/utils/compare_id'; +import compareId from 'flavours/glitch/compare_id'; import NotificationsPermissionBanner from './components/notifications_permission_banner'; import NotSignedInIndicator from 'flavours/glitch/components/not_signed_in_indicator'; import { Helmet } from 'react-helmet'; diff --git a/app/javascript/flavours/glitch/features/ui/components/column.js b/app/javascript/flavours/glitch/features/ui/components/column.js index c97f98420..e9c1e2f87 100644 --- a/app/javascript/flavours/glitch/features/ui/components/column.js +++ b/app/javascript/flavours/glitch/features/ui/components/column.js @@ -2,8 +2,8 @@ import React from 'react'; import ColumnHeader from './column_header'; import PropTypes from 'prop-types'; import { debounce } from 'lodash'; -import { scrollTop } from 'flavours/glitch/utils/scroll'; -import { isMobile } from 'flavours/glitch/utils/is_mobile'; +import { scrollTop } from 'flavours/glitch/scroll'; +import { isMobile } from 'flavours/glitch/is_mobile'; export default class Column extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index b38fe4b17..718b4a27f 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -32,7 +32,7 @@ import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; import { supportsPassiveEvents } from 'detect-passive-events'; -import { scrollRight } from 'flavours/glitch/utils/scroll'; +import { scrollRight } from 'flavours/glitch/scroll'; const componentMap = { 'COMPOSE': Compose, 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 cc4b70293..9c82fc91d 100644 --- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js +++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage, injectIntl } from 'react-intl'; import { debounce } from 'lodash'; -import { isUserTouching } from 'flavours/glitch/utils/is_mobile'; +import { isUserTouching } from 'flavours/glitch/is_mobile'; import Icon from 'flavours/glitch/components/icon'; import NotificationsCounterIcon from './notifications_counter_icon'; diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 3c679d394..c8cc905e7 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -5,7 +5,7 @@ import LoadingBarContainer from './containers/loading_bar_container'; import ModalContainer from './containers/modal_container'; import { connect } from 'react-redux'; import { Redirect, withRouter } from 'react-router-dom'; -import { layoutFromWindow } from 'flavours/glitch/utils/is_mobile'; +import { layoutFromWindow } from 'flavours/glitch/is_mobile'; import { debounce } from 'lodash'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose'; import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; diff --git a/app/javascript/flavours/glitch/is_mobile.js b/app/javascript/flavours/glitch/is_mobile.js new file mode 100644 index 000000000..0d5663098 --- /dev/null +++ b/app/javascript/flavours/glitch/is_mobile.js @@ -0,0 +1,43 @@ +import { supportsPassiveEvents } from 'detect-passive-events'; +import { forceSingleColumn } from 'flavours/glitch/initial_state'; + +const LAYOUT_BREAKPOINT = 630; + +export const isMobile = width => width <= LAYOUT_BREAKPOINT; + +export const layoutFromWindow = (layout_local_setting) => { + switch (layout_local_setting) { + case 'multiple': + return 'multi-column'; + case 'single': + if (isMobile(window.innerWidth)) { + return 'mobile'; + } else { + return 'single-column'; + } + default: + if (isMobile(window.innerWidth)) { + return 'mobile'; + } else if (forceSingleColumn) { + return 'single-column'; + } else { + return 'multi-column'; + } + } +}; + +const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; + +let userTouching = false; +let listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +const touchListener = () => { + userTouching = true; + window.removeEventListener('touchstart', touchListener, listenerOptions); +}; + +window.addEventListener('touchstart', touchListener, listenerOptions); + +export const isUserTouching = () => userTouching; + +export const isIOS = () => iOS; diff --git a/app/javascript/flavours/glitch/load_keyboard_extensions.js b/app/javascript/flavours/glitch/load_keyboard_extensions.js new file mode 100644 index 000000000..2dd0e45fa --- /dev/null +++ b/app/javascript/flavours/glitch/load_keyboard_extensions.js @@ -0,0 +1,16 @@ +// On KaiOS, we may not be able to use a mouse cursor or navigate using Tab-based focus, so we install +// special left/right focus navigation keyboard listeners, at least on public pages (i.e. so folks +// can at least log in using KaiOS devices). + +function importArrowKeyNavigation() { + return import(/* webpackChunkName: "arrow-key-navigation" */ 'arrow-key-navigation'); +} + +export default function loadKeyboardExtensions() { + if (/KAIOS/.test(navigator.userAgent)) { + return importArrowKeyNavigation().then(arrowKeyNav => { + arrowKeyNav.register(); + }); + } + return Promise.resolve(); +} diff --git a/app/javascript/flavours/glitch/load_polyfills.js b/app/javascript/flavours/glitch/load_polyfills.js new file mode 100644 index 000000000..73eedc9dc --- /dev/null +++ b/app/javascript/flavours/glitch/load_polyfills.js @@ -0,0 +1,42 @@ +// Convenience function to load polyfills and return a promise when it's done. +// If there are no polyfills, then this is just Promise.resolve() which means +// it will execute in the same tick of the event loop (i.e. near-instant). + +function importBasePolyfills() { + return import(/* webpackChunkName: "base_polyfills" */ './base_polyfills'); +} + +function importExtraPolyfills() { + return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills'); +} + +function loadPolyfills() { + const needsBasePolyfills = !( + Array.prototype.includes && + HTMLCanvasElement.prototype.toBlob && + window.Intl && + Number.isNaN && + Object.assign && + Object.values && + window.Symbol && + Promise.prototype.finally + ); + + // Latest version of Firefox and Safari do not have IntersectionObserver. + // Edge does not have requestIdleCallback and object-fit CSS property. + // This avoids shipping them all the polyfills. + const needsExtraPolyfills = !( + window.IntersectionObserver && + window.IntersectionObserverEntry && + 'isIntersecting' in IntersectionObserverEntry.prototype && + window.requestIdleCallback && + 'object-fit' in (new Image()).style + ); + + return Promise.all([ + needsBasePolyfills && importBasePolyfills(), + needsExtraPolyfills && importExtraPolyfills(), + ]); +} + +export default loadPolyfills; diff --git a/app/javascript/flavours/glitch/main.js b/app/javascript/flavours/glitch/main.js new file mode 100644 index 000000000..04efcd43f --- /dev/null +++ b/app/javascript/flavours/glitch/main.js @@ -0,0 +1,57 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications'; +import Mastodon, { store } from 'flavours/glitch/containers/mastodon'; +import ready from 'flavours/glitch/ready'; + +const perf = require('flavours/glitch/performance'); + +/** + * @returns {Promise} + */ +function main() { + perf.start('main()'); + + if (window.history && history.replaceState) { + const { pathname, search, hash } = window.location; + const path = pathname + search + hash; + if (!(/^\/web($|\/)/).test(path)) { + history.replaceState(null, document.title, `/web${path}`); + } + } + + return ready(async () => { + const mountNode = document.getElementById('mastodon'); + const props = JSON.parse(mountNode.getAttribute('data-props')); + + ReactDOM.render(, mountNode); + store.dispatch(setupBrowserNotifications()); + + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + const [{ Workbox }, { me }] = await Promise.all([ + import('workbox-window'), + import('mastodon/initial_state'), + ]); + + const wb = new Workbox('/sw.js'); + + try { + await wb.register(); + } catch (err) { + console.error(err); + + return; + } + + if (me) { + const registerPushNotifications = await import('flavours/glitch/actions/push_notifications'); + + store.dispatch(registerPushNotifications.register()); + } + } + + perf.stop('main()'); + }); +} + +export default main; diff --git a/app/javascript/flavours/glitch/packs/about.js b/app/javascript/flavours/glitch/packs/about.js index 50e789946..ef17fdea4 100644 --- a/app/javascript/flavours/glitch/packs/about.js +++ b/app/javascript/flavours/glitch/packs/about.js @@ -1,5 +1,5 @@ import 'packs/public-path'; -import loadPolyfills from 'flavours/glitch/utils/load_polyfills'; +import loadPolyfills from 'flavours/glitch/load_polyfills'; function loaded() { const TimelineContainer = require('flavours/glitch/containers/timeline_container').default; diff --git a/app/javascript/flavours/glitch/packs/home.js b/app/javascript/flavours/glitch/packs/home.js index 0311df9b2..ace9dc3c4 100644 --- a/app/javascript/flavours/glitch/packs/home.js +++ b/app/javascript/flavours/glitch/packs/home.js @@ -1,8 +1,8 @@ import 'packs/public-path'; -import loadPolyfills from 'flavours/glitch/utils/load_polyfills'; +import loadPolyfills from 'flavours/glitch/load_polyfills'; loadPolyfills().then(async () => { - const { default: main } = await import('flavours/glitch/utils/main'); + const { default: main } = await import('flavours/glitch/main'); return main(); }).catch(e => { diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index d920acc00..4b5b7343e 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -1,7 +1,7 @@ import 'packs/public-path'; -import loadPolyfills from 'flavours/glitch/utils/load_polyfills'; +import loadPolyfills from 'flavours/glitch/load_polyfills'; import ready from 'flavours/glitch/ready'; -import loadKeyboardExtensions from 'flavours/glitch/utils/load_keyboard_extensions'; +import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions'; function main() { const IntlMessageFormat = require('intl-messageformat').default; diff --git a/app/javascript/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js index 80f53bc78..4c85f6556 100644 --- a/app/javascript/flavours/glitch/packs/settings.js +++ b/app/javascript/flavours/glitch/packs/settings.js @@ -1,7 +1,7 @@ import 'packs/public-path'; -import loadPolyfills from 'flavours/glitch/utils/load_polyfills'; +import loadPolyfills from 'flavours/glitch/load_polyfills'; import ready from 'flavours/glitch/ready'; -import loadKeyboardExtensions from 'flavours/glitch/utils/load_keyboard_extensions'; +import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions'; import 'cocoon-js-vanilla'; function main() { diff --git a/app/javascript/flavours/glitch/packs/share.js b/app/javascript/flavours/glitch/packs/share.js index 49c8545e7..e5a79849a 100644 --- a/app/javascript/flavours/glitch/packs/share.js +++ b/app/javascript/flavours/glitch/packs/share.js @@ -1,5 +1,5 @@ import 'packs/public-path'; -import loadPolyfills from 'flavours/glitch/utils/load_polyfills'; +import loadPolyfills from 'flavours/glitch/load_polyfills'; function loaded() { const ComposeContainer = require('flavours/glitch/containers/compose_container').default; diff --git a/app/javascript/flavours/glitch/performance.js b/app/javascript/flavours/glitch/performance.js new file mode 100644 index 000000000..450a90626 --- /dev/null +++ b/app/javascript/flavours/glitch/performance.js @@ -0,0 +1,31 @@ +// +// Tools for performance debugging, only enabled in development mode. +// Open up Chrome Dev Tools, then Timeline, then User Timing to see output. +// Also see config/webpack/loaders/mark.js for the webpack loader marks. +// + +let marky; + +if (process.env.NODE_ENV === 'development') { + if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) { + // Increase Firefox's performance entry limit; otherwise it's capped to 150. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135 + performance.setResourceTimingBufferSize(Infinity); + } + marky = require('marky'); + // allows us to easily do e.g. ReactPerf.printWasted() while debugging + //window.ReactPerf = require('react-addons-perf'); + //window.ReactPerf.start(); +} + +export function start(name) { + if (process.env.NODE_ENV === 'development') { + marky.mark(name); + } +} + +export function stop(name) { + if (process.env.NODE_ENV === 'development') { + marky.stop(name); + } +} diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index d0bc46002..035e9f564 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -53,7 +53,7 @@ import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines'; import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; import { REDRAFT } from 'flavours/glitch/actions/statuses'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; -import uuid from 'flavours/glitch/utils/uuid'; +import uuid from '../uuid'; import { privacyPreference } from 'flavours/glitch/utils/privacy_preference'; import { me, defaultContentType } from 'flavours/glitch/initial_state'; import { overwrite } from 'flavours/glitch/utils/js_helpers'; diff --git a/app/javascript/flavours/glitch/reducers/contexts.js b/app/javascript/flavours/glitch/reducers/contexts.js index 5dec090da..a0fcc4158 100644 --- a/app/javascript/flavours/glitch/reducers/contexts.js +++ b/app/javascript/flavours/glitch/reducers/contexts.js @@ -5,7 +5,7 @@ import { import { CONTEXT_FETCH_SUCCESS } from 'flavours/glitch/actions/statuses'; import { TIMELINE_DELETE, TIMELINE_UPDATE } from 'flavours/glitch/actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from 'flavours/glitch/utils/compare_id'; +import compareId from '../compare_id'; const initialState = ImmutableMap({ inReplyTos: ImmutableMap(), diff --git a/app/javascript/flavours/glitch/reducers/conversations.js b/app/javascript/flavours/glitch/reducers/conversations.js index 4e8db82a9..4407dcf04 100644 --- a/app/javascript/flavours/glitch/reducers/conversations.js +++ b/app/javascript/flavours/glitch/reducers/conversations.js @@ -11,7 +11,7 @@ import { } from '../actions/conversations'; import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'flavours/glitch/actions/accounts'; import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks'; -import compareId from 'flavours/glitch/utils/compare_id'; +import compareId from '../compare_id'; const initialState = ImmutableMap({ items: ImmutableList(), diff --git a/app/javascript/flavours/glitch/reducers/meta.js b/app/javascript/flavours/glitch/reducers/meta.js index 7af7e9514..b1482777a 100644 --- a/app/javascript/flavours/glitch/reducers/meta.js +++ b/app/javascript/flavours/glitch/reducers/meta.js @@ -1,7 +1,7 @@ import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; import { APP_LAYOUT_CHANGE } from 'flavours/glitch/actions/app'; import { Map as ImmutableMap } from 'immutable'; -import { layoutFromWindow } from 'flavours/glitch/utils/is_mobile'; +import { layoutFromWindow } from 'flavours/glitch/is_mobile'; const initialState = ImmutableMap({ streaming_api_base_url: null, diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index d2eda9ab8..1b593b128 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -32,7 +32,7 @@ import { import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines'; import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from 'flavours/glitch/utils/compare_id'; +import compareId from '../compare_id'; const initialState = ImmutableMap({ pendingItems: ImmutableList(), diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index 90e50479f..82927f7cd 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -6,7 +6,7 @@ import { EMOJI_USE } from 'flavours/glitch/actions/emojis'; import { LANGUAGE_USE } from 'flavours/glitch/actions/languages'; import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists'; import { Map as ImmutableMap, fromJS } from 'immutable'; -import uuid from 'flavours/glitch/utils/uuid'; +import uuid from '../uuid'; const initialState = ImmutableMap({ saved: true, diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index 2586c837b..407293c62 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -17,7 +17,7 @@ import { ACCOUNT_UNFOLLOW_SUCCESS, } from 'flavours/glitch/actions/accounts'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; -import compareId from 'flavours/glitch/utils/compare_id'; +import compareId from '../compare_id'; const initialState = ImmutableMap(); diff --git a/app/javascript/flavours/glitch/scroll.js b/app/javascript/flavours/glitch/scroll.js new file mode 100644 index 000000000..84fe58269 --- /dev/null +++ b/app/javascript/flavours/glitch/scroll.js @@ -0,0 +1,32 @@ +const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; + +const scroll = (node, key, target) => { + const startTime = Date.now(); + const offset = node[key]; + const gap = target - offset; + const duration = 1000; + let interrupt = false; + + const step = () => { + const elapsed = Date.now() - startTime; + const percentage = elapsed / duration; + + if (percentage > 1 || interrupt) { + return; + } + + node[key] = easingOutQuint(0, elapsed, offset, gap, duration); + requestAnimationFrame(step); + }; + + step(); + + return () => { + interrupt = true; + }; +}; + +const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; + +export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); +export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); diff --git a/app/javascript/flavours/glitch/settings.js b/app/javascript/flavours/glitch/settings.js new file mode 100644 index 000000000..7643a508e --- /dev/null +++ b/app/javascript/flavours/glitch/settings.js @@ -0,0 +1,47 @@ +export default class Settings { + + constructor(keyBase = null) { + this.keyBase = keyBase; + } + + generateKey(id) { + return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id; + } + + set(id, data) { + const key = this.generateKey(id); + try { + const encodedData = JSON.stringify(data); + localStorage.setItem(key, encodedData); + return data; + } catch (e) { + return null; + } + } + + get(id) { + const key = this.generateKey(id); + try { + const rawData = localStorage.getItem(key); + return JSON.parse(rawData); + } catch (e) { + return null; + } + } + + remove(id) { + const data = this.get(id); + if (data) { + const key = this.generateKey(id); + try { + localStorage.removeItem(key); + } catch (e) { + } + } + return data; + } + +} + +export const pushNotificationsSetting = new Settings('mastodon_push_notification_data'); +export const tagHistory = new Settings('mastodon_tag_history'); diff --git a/app/javascript/flavours/glitch/stream.js b/app/javascript/flavours/glitch/stream.js new file mode 100644 index 000000000..c6d12cd6f --- /dev/null +++ b/app/javascript/flavours/glitch/stream.js @@ -0,0 +1,265 @@ +// @ts-check + +import WebSocketClient from '@gamestdio/websocket'; + +/** + * @type {WebSocketClient | undefined} + */ +let sharedConnection; + +/** + * @typedef Subscription + * @property {string} channelName + * @property {Object.} params + * @property {function(): void} onConnect + * @property {function(StreamEvent): void} onReceive + * @property {function(): void} onDisconnect + */ + +/** + * @typedef StreamEvent + * @property {string} event + * @property {object} payload + */ + +/** + * @type {Array.} + */ +const subscriptions = []; + +/** + * @type {Object.} + */ +const subscriptionCounters = {}; + +/** + * @param {Subscription} subscription + */ +const addSubscription = subscription => { + subscriptions.push(subscription); +}; + +/** + * @param {Subscription} subscription + */ +const removeSubscription = subscription => { + const index = subscriptions.indexOf(subscription); + + if (index !== -1) { + subscriptions.splice(index, 1); + } +}; + +/** + * @param {Subscription} subscription + */ +const subscribe = ({ channelName, params, onConnect }) => { + const key = channelNameWithInlineParams(channelName, params); + + subscriptionCounters[key] = subscriptionCounters[key] || 0; + + if (subscriptionCounters[key] === 0) { + sharedConnection.send(JSON.stringify({ type: 'subscribe', stream: channelName, ...params })); + } + + subscriptionCounters[key] += 1; + onConnect(); +}; + +/** + * @param {Subscription} subscription + */ +const unsubscribe = ({ channelName, params, onDisconnect }) => { + const key = channelNameWithInlineParams(channelName, params); + + subscriptionCounters[key] = subscriptionCounters[key] || 1; + + if (subscriptionCounters[key] === 1 && sharedConnection.readyState === WebSocketClient.OPEN) { + sharedConnection.send(JSON.stringify({ type: 'unsubscribe', stream: channelName, ...params })); + } + + subscriptionCounters[key] -= 1; + onDisconnect(); +}; + +const sharedCallbacks = { + connected () { + subscriptions.forEach(subscription => subscribe(subscription)); + }, + + received (data) { + const { stream } = data; + + subscriptions.filter(({ channelName, params }) => { + const streamChannelName = stream[0]; + + if (stream.length === 1) { + return channelName === streamChannelName; + } + + const streamIdentifier = stream[1]; + + if (['hashtag', 'hashtag:local'].includes(channelName)) { + return channelName === streamChannelName && params.tag === streamIdentifier; + } else if (channelName === 'list') { + return channelName === streamChannelName && params.list === streamIdentifier; + } + + return false; + }).forEach(subscription => { + subscription.onReceive(data); + }); + }, + + disconnected () { + subscriptions.forEach(subscription => unsubscribe(subscription)); + }, + + reconnected () { + }, +}; + +/** + * @param {string} channelName + * @param {Object.} params + * @return {string} + */ +const channelNameWithInlineParams = (channelName, params) => { + if (Object.keys(params).length === 0) { + return channelName; + } + + return `${channelName}&${Object.keys(params).map(key => `${key}=${params[key]}`).join('&')}`; +}; + +/** + * @param {string} channelName + * @param {Object.} params + * @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks + * @return {function(): void} + */ +export const connectStream = (channelName, params, callbacks) => (dispatch, getState) => { + const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); + const accessToken = getState().getIn(['meta', 'access_token']); + const { onConnect, onReceive, onDisconnect } = callbacks(dispatch, getState); + + // If we cannot use a websockets connection, we must fall back + // to using individual connections for each channel + if (!streamingAPIBaseURL.startsWith('ws')) { + const connection = createConnection(streamingAPIBaseURL, accessToken, channelNameWithInlineParams(channelName, params), { + connected () { + onConnect(); + }, + + received (data) { + onReceive(data); + }, + + disconnected () { + onDisconnect(); + }, + + reconnected () { + onConnect(); + }, + }); + + return () => { + connection.close(); + }; + } + + const subscription = { + channelName, + params, + onConnect, + onReceive, + onDisconnect, + }; + + addSubscription(subscription); + + // If a connection is open, we can execute the subscription right now. Otherwise, + // because we have already registered it, it will be executed on connect + + if (!sharedConnection) { + sharedConnection = /** @type {WebSocketClient} */ (createConnection(streamingAPIBaseURL, accessToken, '', sharedCallbacks)); + } else if (sharedConnection.readyState === WebSocketClient.OPEN) { + subscribe(subscription); + } + + return () => { + removeSubscription(subscription); + unsubscribe(subscription); + }; +}; + +const KNOWN_EVENT_TYPES = [ + 'update', + 'delete', + 'notification', + 'conversation', + 'filters_changed', + 'encrypted_message', + 'announcement', + 'announcement.delete', + 'announcement.reaction', +]; + +/** + * @param {MessageEvent} e + * @param {function(StreamEvent): void} received + */ +const handleEventSourceMessage = (e, received) => { + received({ + event: e.type, + payload: e.data, + }); +}; + +/** + * @param {string} streamingAPIBaseURL + * @param {string} accessToken + * @param {string} channelName + * @param {{ connected: Function, received: function(StreamEvent): void, disconnected: Function, reconnected: Function }} callbacks + * @return {WebSocketClient | EventSource} + */ +const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => { + const params = channelName.split('&'); + + channelName = params.shift(); + + if (streamingAPIBaseURL.startsWith('ws')) { + const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken); + + ws.onopen = connected; + ws.onmessage = e => received(JSON.parse(e.data)); + ws.onclose = disconnected; + ws.onreconnect = reconnected; + + return ws; + } + + channelName = channelName.replace(/:/g, '/'); + + if (channelName.endsWith(':media')) { + channelName = channelName.replace('/media', ''); + params.push('only_media=true'); + } + + params.push(`access_token=${accessToken}`); + + const es = new EventSource(`${streamingAPIBaseURL}/api/v1/streaming/${channelName}?${params.join('&')}`); + + es.onopen = () => { + connected(); + }; + + KNOWN_EVENT_TYPES.forEach(type => { + es.addEventListener(type, e => handleEventSourceMessage(/** @type {MessageEvent} */ (e), received)); + }); + + es.onerror = /** @type {function(): void} */ (disconnected); + + return es; +}; diff --git a/app/javascript/flavours/glitch/utils/base_polyfills.js b/app/javascript/flavours/glitch/utils/base_polyfills.js deleted file mode 100644 index 4b8123dba..000000000 --- a/app/javascript/flavours/glitch/utils/base_polyfills.js +++ /dev/null @@ -1,47 +0,0 @@ -import 'intl'; -import 'intl/locale-data/jsonp/en'; -import 'es6-symbol/implement'; -import includes from 'array-includes'; -import assign from 'object-assign'; -import values from 'object.values'; -import isNaN from 'is-nan'; -import { decode as decodeBase64 } from './base64'; -import promiseFinally from 'promise.prototype.finally'; - -if (!Array.prototype.includes) { - includes.shim(); -} - -if (!Object.assign) { - Object.assign = assign; -} - -if (!Object.values) { - values.shim(); -} - -if (!Number.isNaN) { - Number.isNaN = isNaN; -} - -promiseFinally.shim(); - -if (!HTMLCanvasElement.prototype.toBlob) { - const BASE64_MARKER = ';base64,'; - - Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { - value(callback, type = 'image/png', quality) { - const dataURL = this.toDataURL(type, quality); - let data; - - if (dataURL.indexOf(BASE64_MARKER) >= 0) { - const [, base64] = dataURL.split(BASE64_MARKER); - data = decodeBase64(base64); - } else { - [, data] = dataURL.split(','); - } - - callback(new Blob([data], { type })); - }, - }); -} diff --git a/app/javascript/flavours/glitch/utils/compare_id.js b/app/javascript/flavours/glitch/utils/compare_id.js deleted file mode 100644 index 66cf51c4b..000000000 --- a/app/javascript/flavours/glitch/utils/compare_id.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function compareId (id1, id2) { - if (id1 === id2) { - return 0; - } - - if (id1.length === id2.length) { - return id1 > id2 ? 1 : -1; - } else { - return id1.length > id2.length ? 1 : -1; - } -}; diff --git a/app/javascript/flavours/glitch/utils/extra_polyfills.js b/app/javascript/flavours/glitch/utils/extra_polyfills.js deleted file mode 100644 index 3acc55abd..000000000 --- a/app/javascript/flavours/glitch/utils/extra_polyfills.js +++ /dev/null @@ -1,5 +0,0 @@ -import 'intersection-observer'; -import 'requestidlecallback'; -import objectFitImages from 'object-fit-images'; - -objectFitImages(); diff --git a/app/javascript/flavours/glitch/utils/is_mobile.js b/app/javascript/flavours/glitch/utils/is_mobile.js deleted file mode 100644 index 0d5663098..000000000 --- a/app/javascript/flavours/glitch/utils/is_mobile.js +++ /dev/null @@ -1,43 +0,0 @@ -import { supportsPassiveEvents } from 'detect-passive-events'; -import { forceSingleColumn } from 'flavours/glitch/initial_state'; - -const LAYOUT_BREAKPOINT = 630; - -export const isMobile = width => width <= LAYOUT_BREAKPOINT; - -export const layoutFromWindow = (layout_local_setting) => { - switch (layout_local_setting) { - case 'multiple': - return 'multi-column'; - case 'single': - if (isMobile(window.innerWidth)) { - return 'mobile'; - } else { - return 'single-column'; - } - default: - if (isMobile(window.innerWidth)) { - return 'mobile'; - } else if (forceSingleColumn) { - return 'single-column'; - } else { - return 'multi-column'; - } - } -}; - -const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; - -let userTouching = false; -let listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -const touchListener = () => { - userTouching = true; - window.removeEventListener('touchstart', touchListener, listenerOptions); -}; - -window.addEventListener('touchstart', touchListener, listenerOptions); - -export const isUserTouching = () => userTouching; - -export const isIOS = () => iOS; diff --git a/app/javascript/flavours/glitch/utils/load_keyboard_extensions.js b/app/javascript/flavours/glitch/utils/load_keyboard_extensions.js deleted file mode 100644 index 2dd0e45fa..000000000 --- a/app/javascript/flavours/glitch/utils/load_keyboard_extensions.js +++ /dev/null @@ -1,16 +0,0 @@ -// On KaiOS, we may not be able to use a mouse cursor or navigate using Tab-based focus, so we install -// special left/right focus navigation keyboard listeners, at least on public pages (i.e. so folks -// can at least log in using KaiOS devices). - -function importArrowKeyNavigation() { - return import(/* webpackChunkName: "arrow-key-navigation" */ 'arrow-key-navigation'); -} - -export default function loadKeyboardExtensions() { - if (/KAIOS/.test(navigator.userAgent)) { - return importArrowKeyNavigation().then(arrowKeyNav => { - arrowKeyNav.register(); - }); - } - return Promise.resolve(); -} diff --git a/app/javascript/flavours/glitch/utils/load_polyfills.js b/app/javascript/flavours/glitch/utils/load_polyfills.js deleted file mode 100644 index 73eedc9dc..000000000 --- a/app/javascript/flavours/glitch/utils/load_polyfills.js +++ /dev/null @@ -1,42 +0,0 @@ -// Convenience function to load polyfills and return a promise when it's done. -// If there are no polyfills, then this is just Promise.resolve() which means -// it will execute in the same tick of the event loop (i.e. near-instant). - -function importBasePolyfills() { - return import(/* webpackChunkName: "base_polyfills" */ './base_polyfills'); -} - -function importExtraPolyfills() { - return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills'); -} - -function loadPolyfills() { - const needsBasePolyfills = !( - Array.prototype.includes && - HTMLCanvasElement.prototype.toBlob && - window.Intl && - Number.isNaN && - Object.assign && - Object.values && - window.Symbol && - Promise.prototype.finally - ); - - // Latest version of Firefox and Safari do not have IntersectionObserver. - // Edge does not have requestIdleCallback and object-fit CSS property. - // This avoids shipping them all the polyfills. - const needsExtraPolyfills = !( - window.IntersectionObserver && - window.IntersectionObserverEntry && - 'isIntersecting' in IntersectionObserverEntry.prototype && - window.requestIdleCallback && - 'object-fit' in (new Image()).style - ); - - return Promise.all([ - needsBasePolyfills && importBasePolyfills(), - needsExtraPolyfills && importExtraPolyfills(), - ]); -} - -export default loadPolyfills; diff --git a/app/javascript/flavours/glitch/utils/main.js b/app/javascript/flavours/glitch/utils/main.js deleted file mode 100644 index 39a46db00..000000000 --- a/app/javascript/flavours/glitch/utils/main.js +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications'; -import Mastodon, { store } from 'flavours/glitch/containers/mastodon'; -import ready from 'flavours/glitch/ready'; - -const perf = require('flavours/glitch/utils/performance'); - -/** - * @returns {Promise} - */ -function main() { - perf.start('main()'); - - if (window.history && history.replaceState) { - const { pathname, search, hash } = window.location; - const path = pathname + search + hash; - if (!(/^\/web($|\/)/).test(path)) { - history.replaceState(null, document.title, `/web${path}`); - } - } - - return ready(async () => { - const mountNode = document.getElementById('mastodon'); - const props = JSON.parse(mountNode.getAttribute('data-props')); - - ReactDOM.render(, mountNode); - store.dispatch(setupBrowserNotifications()); - - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - const [{ Workbox }, { me }] = await Promise.all([ - import('workbox-window'), - import('mastodon/initial_state'), - ]); - - const wb = new Workbox('/sw.js'); - - try { - await wb.register(); - } catch (err) { - console.error(err); - - return; - } - - if (me) { - const registerPushNotifications = await import('flavours/glitch/actions/push_notifications'); - - store.dispatch(registerPushNotifications.register()); - } - } - - perf.stop('main()'); - }); -} - -export default main; diff --git a/app/javascript/flavours/glitch/utils/performance.js b/app/javascript/flavours/glitch/utils/performance.js deleted file mode 100644 index 450a90626..000000000 --- a/app/javascript/flavours/glitch/utils/performance.js +++ /dev/null @@ -1,31 +0,0 @@ -// -// Tools for performance debugging, only enabled in development mode. -// Open up Chrome Dev Tools, then Timeline, then User Timing to see output. -// Also see config/webpack/loaders/mark.js for the webpack loader marks. -// - -let marky; - -if (process.env.NODE_ENV === 'development') { - if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) { - // Increase Firefox's performance entry limit; otherwise it's capped to 150. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135 - performance.setResourceTimingBufferSize(Infinity); - } - marky = require('marky'); - // allows us to easily do e.g. ReactPerf.printWasted() while debugging - //window.ReactPerf = require('react-addons-perf'); - //window.ReactPerf.start(); -} - -export function start(name) { - if (process.env.NODE_ENV === 'development') { - marky.mark(name); - } -} - -export function stop(name) { - if (process.env.NODE_ENV === 'development') { - marky.stop(name); - } -} diff --git a/app/javascript/flavours/glitch/utils/scroll.js b/app/javascript/flavours/glitch/utils/scroll.js deleted file mode 100644 index 84fe58269..000000000 --- a/app/javascript/flavours/glitch/utils/scroll.js +++ /dev/null @@ -1,32 +0,0 @@ -const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; - -const scroll = (node, key, target) => { - const startTime = Date.now(); - const offset = node[key]; - const gap = target - offset; - const duration = 1000; - let interrupt = false; - - const step = () => { - const elapsed = Date.now() - startTime; - const percentage = elapsed / duration; - - if (percentage > 1 || interrupt) { - return; - } - - node[key] = easingOutQuint(0, elapsed, offset, gap, duration); - requestAnimationFrame(step); - }; - - step(); - - return () => { - interrupt = true; - }; -}; - -const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; - -export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); -export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); diff --git a/app/javascript/flavours/glitch/utils/settings.js b/app/javascript/flavours/glitch/utils/settings.js deleted file mode 100644 index 7643a508e..000000000 --- a/app/javascript/flavours/glitch/utils/settings.js +++ /dev/null @@ -1,47 +0,0 @@ -export default class Settings { - - constructor(keyBase = null) { - this.keyBase = keyBase; - } - - generateKey(id) { - return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id; - } - - set(id, data) { - const key = this.generateKey(id); - try { - const encodedData = JSON.stringify(data); - localStorage.setItem(key, encodedData); - return data; - } catch (e) { - return null; - } - } - - get(id) { - const key = this.generateKey(id); - try { - const rawData = localStorage.getItem(key); - return JSON.parse(rawData); - } catch (e) { - return null; - } - } - - remove(id) { - const data = this.get(id); - if (data) { - const key = this.generateKey(id); - try { - localStorage.removeItem(key); - } catch (e) { - } - } - return data; - } - -} - -export const pushNotificationsSetting = new Settings('mastodon_push_notification_data'); -export const tagHistory = new Settings('mastodon_tag_history'); diff --git a/app/javascript/flavours/glitch/utils/stream.js b/app/javascript/flavours/glitch/utils/stream.js deleted file mode 100644 index c6d12cd6f..000000000 --- a/app/javascript/flavours/glitch/utils/stream.js +++ /dev/null @@ -1,265 +0,0 @@ -// @ts-check - -import WebSocketClient from '@gamestdio/websocket'; - -/** - * @type {WebSocketClient | undefined} - */ -let sharedConnection; - -/** - * @typedef Subscription - * @property {string} channelName - * @property {Object.} params - * @property {function(): void} onConnect - * @property {function(StreamEvent): void} onReceive - * @property {function(): void} onDisconnect - */ - -/** - * @typedef StreamEvent - * @property {string} event - * @property {object} payload - */ - -/** - * @type {Array.} - */ -const subscriptions = []; - -/** - * @type {Object.} - */ -const subscriptionCounters = {}; - -/** - * @param {Subscription} subscription - */ -const addSubscription = subscription => { - subscriptions.push(subscription); -}; - -/** - * @param {Subscription} subscription - */ -const removeSubscription = subscription => { - const index = subscriptions.indexOf(subscription); - - if (index !== -1) { - subscriptions.splice(index, 1); - } -}; - -/** - * @param {Subscription} subscription - */ -const subscribe = ({ channelName, params, onConnect }) => { - const key = channelNameWithInlineParams(channelName, params); - - subscriptionCounters[key] = subscriptionCounters[key] || 0; - - if (subscriptionCounters[key] === 0) { - sharedConnection.send(JSON.stringify({ type: 'subscribe', stream: channelName, ...params })); - } - - subscriptionCounters[key] += 1; - onConnect(); -}; - -/** - * @param {Subscription} subscription - */ -const unsubscribe = ({ channelName, params, onDisconnect }) => { - const key = channelNameWithInlineParams(channelName, params); - - subscriptionCounters[key] = subscriptionCounters[key] || 1; - - if (subscriptionCounters[key] === 1 && sharedConnection.readyState === WebSocketClient.OPEN) { - sharedConnection.send(JSON.stringify({ type: 'unsubscribe', stream: channelName, ...params })); - } - - subscriptionCounters[key] -= 1; - onDisconnect(); -}; - -const sharedCallbacks = { - connected () { - subscriptions.forEach(subscription => subscribe(subscription)); - }, - - received (data) { - const { stream } = data; - - subscriptions.filter(({ channelName, params }) => { - const streamChannelName = stream[0]; - - if (stream.length === 1) { - return channelName === streamChannelName; - } - - const streamIdentifier = stream[1]; - - if (['hashtag', 'hashtag:local'].includes(channelName)) { - return channelName === streamChannelName && params.tag === streamIdentifier; - } else if (channelName === 'list') { - return channelName === streamChannelName && params.list === streamIdentifier; - } - - return false; - }).forEach(subscription => { - subscription.onReceive(data); - }); - }, - - disconnected () { - subscriptions.forEach(subscription => unsubscribe(subscription)); - }, - - reconnected () { - }, -}; - -/** - * @param {string} channelName - * @param {Object.} params - * @return {string} - */ -const channelNameWithInlineParams = (channelName, params) => { - if (Object.keys(params).length === 0) { - return channelName; - } - - return `${channelName}&${Object.keys(params).map(key => `${key}=${params[key]}`).join('&')}`; -}; - -/** - * @param {string} channelName - * @param {Object.} params - * @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks - * @return {function(): void} - */ -export const connectStream = (channelName, params, callbacks) => (dispatch, getState) => { - const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); - const accessToken = getState().getIn(['meta', 'access_token']); - const { onConnect, onReceive, onDisconnect } = callbacks(dispatch, getState); - - // If we cannot use a websockets connection, we must fall back - // to using individual connections for each channel - if (!streamingAPIBaseURL.startsWith('ws')) { - const connection = createConnection(streamingAPIBaseURL, accessToken, channelNameWithInlineParams(channelName, params), { - connected () { - onConnect(); - }, - - received (data) { - onReceive(data); - }, - - disconnected () { - onDisconnect(); - }, - - reconnected () { - onConnect(); - }, - }); - - return () => { - connection.close(); - }; - } - - const subscription = { - channelName, - params, - onConnect, - onReceive, - onDisconnect, - }; - - addSubscription(subscription); - - // If a connection is open, we can execute the subscription right now. Otherwise, - // because we have already registered it, it will be executed on connect - - if (!sharedConnection) { - sharedConnection = /** @type {WebSocketClient} */ (createConnection(streamingAPIBaseURL, accessToken, '', sharedCallbacks)); - } else if (sharedConnection.readyState === WebSocketClient.OPEN) { - subscribe(subscription); - } - - return () => { - removeSubscription(subscription); - unsubscribe(subscription); - }; -}; - -const KNOWN_EVENT_TYPES = [ - 'update', - 'delete', - 'notification', - 'conversation', - 'filters_changed', - 'encrypted_message', - 'announcement', - 'announcement.delete', - 'announcement.reaction', -]; - -/** - * @param {MessageEvent} e - * @param {function(StreamEvent): void} received - */ -const handleEventSourceMessage = (e, received) => { - received({ - event: e.type, - payload: e.data, - }); -}; - -/** - * @param {string} streamingAPIBaseURL - * @param {string} accessToken - * @param {string} channelName - * @param {{ connected: Function, received: function(StreamEvent): void, disconnected: Function, reconnected: Function }} callbacks - * @return {WebSocketClient | EventSource} - */ -const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => { - const params = channelName.split('&'); - - channelName = params.shift(); - - if (streamingAPIBaseURL.startsWith('ws')) { - const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken); - - ws.onopen = connected; - ws.onmessage = e => received(JSON.parse(e.data)); - ws.onclose = disconnected; - ws.onreconnect = reconnected; - - return ws; - } - - channelName = channelName.replace(/:/g, '/'); - - if (channelName.endsWith(':media')) { - channelName = channelName.replace('/media', ''); - params.push('only_media=true'); - } - - params.push(`access_token=${accessToken}`); - - const es = new EventSource(`${streamingAPIBaseURL}/api/v1/streaming/${channelName}?${params.join('&')}`); - - es.onopen = () => { - connected(); - }; - - KNOWN_EVENT_TYPES.forEach(type => { - es.addEventListener(type, e => handleEventSourceMessage(/** @type {MessageEvent} */ (e), received)); - }); - - es.onerror = /** @type {function(): void} */ (disconnected); - - return es; -}; diff --git a/app/javascript/flavours/glitch/utils/uuid.js b/app/javascript/flavours/glitch/utils/uuid.js deleted file mode 100644 index be1899305..000000000 --- a/app/javascript/flavours/glitch/utils/uuid.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function uuid(a) { - return a ? (a^Math.random() * 16 >> a / 4).toString(16) : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid); -}; diff --git a/app/javascript/flavours/glitch/uuid.js b/app/javascript/flavours/glitch/uuid.js new file mode 100644 index 000000000..be1899305 --- /dev/null +++ b/app/javascript/flavours/glitch/uuid.js @@ -0,0 +1,3 @@ +export default function uuid(a) { + return a ? (a^Math.random() * 16 >> a / 4).toString(16) : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid); +}; -- cgit