From d3879c07b1b2140bd19433ae06855894228bd90f Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 11 Oct 2022 10:41:15 +0200 Subject: Rename flavours/glitch/util into flavours/glitch/utils --- app/javascript/flavours/glitch/features/compose/components/upload.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/javascript/flavours/glitch/features/compose/components/upload.js') diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js index 963b95c87..b344dece1 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload.js @@ -1,12 +1,12 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import Motion from 'flavours/glitch/util/optional_motion'; +import Motion from 'flavours/glitch/utils/optional_motion'; 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/util/is_mobile'; +import { isUserTouching } from 'flavours/glitch/utils/is_mobile'; export default class Upload extends ImmutablePureComponent { -- cgit From 015e798394a98bb826446ceb55bfadacc5888215 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 11 Oct 2022 10:51:33 +0200 Subject: Move some modules from flavours/glitch/utils/ back to flavours/glitch/features/ui/util/ --- .../flavours/glitch/components/dropdown_menu.js | 2 +- .../flavours/glitch/components/icon_button.js | 2 +- .../components/intersection_observer_article.js | 4 +- app/javascript/flavours/glitch/components/poll.js | 2 +- .../flavours/glitch/components/scrollable_list.js | 4 +- .../features/compose/components/dropdown_menu.js | 2 +- .../compose/components/language_dropdown.js | 2 +- .../glitch/features/compose/components/options.js | 2 +- .../glitch/features/compose/components/search.js | 2 +- .../glitch/features/compose/components/upload.js | 2 +- .../features/compose/components/upload_progress.js | 2 +- .../glitch/features/compose/components/warning.js | 2 +- .../flavours/glitch/features/compose/index.js | 2 +- .../flavours/glitch/features/list_editor/index.js | 2 +- .../features/pinned_accounts_editor/index.js | 2 +- .../features/status/components/detailed_status.js | 2 +- .../flavours/glitch/features/status/index.js | 2 +- .../glitch/features/ui/components/upload_area.js | 2 +- .../flavours/glitch/features/ui/index.js | 2 +- .../flavours/glitch/features/ui/util/fullscreen.js | 46 +++++++++++++++ .../glitch/features/ui/util/get_rect_from_entry.js | 21 +++++++ .../ui/util/intersection_observer_wrapper.js | 57 ++++++++++++++++++ .../glitch/features/ui/util/optional_motion.js | 5 ++ .../features/ui/util/react_router_helpers.js | 69 ++++++++++++++++++++++ .../glitch/features/ui/util/reduced_motion.js | 44 ++++++++++++++ .../glitch/features/ui/util/schedule_idle_task.js | 29 +++++++++ .../flavours/glitch/features/video/index.js | 2 +- app/javascript/flavours/glitch/utils/fullscreen.js | 46 --------------- .../flavours/glitch/utils/get_rect_from_entry.js | 21 ------- .../glitch/utils/intersection_observer_wrapper.js | 57 ------------------ .../flavours/glitch/utils/optional_motion.js | 5 -- .../flavours/glitch/utils/react_router_helpers.js | 69 ---------------------- .../flavours/glitch/utils/reduced_motion.js | 44 -------------- .../flavours/glitch/utils/schedule_idle_task.js | 29 --------- 34 files changed, 293 insertions(+), 293 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/ui/util/fullscreen.js create mode 100644 app/javascript/flavours/glitch/features/ui/util/get_rect_from_entry.js create mode 100644 app/javascript/flavours/glitch/features/ui/util/intersection_observer_wrapper.js create mode 100644 app/javascript/flavours/glitch/features/ui/util/optional_motion.js create mode 100644 app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js create mode 100644 app/javascript/flavours/glitch/features/ui/util/reduced_motion.js create mode 100644 app/javascript/flavours/glitch/features/ui/util/schedule_idle_task.js delete mode 100644 app/javascript/flavours/glitch/utils/fullscreen.js delete mode 100644 app/javascript/flavours/glitch/utils/get_rect_from_entry.js delete mode 100644 app/javascript/flavours/glitch/utils/intersection_observer_wrapper.js delete mode 100644 app/javascript/flavours/glitch/utils/optional_motion.js delete mode 100644 app/javascript/flavours/glitch/utils/react_router_helpers.js delete mode 100644 app/javascript/flavours/glitch/utils/reduced_motion.js delete mode 100644 app/javascript/flavours/glitch/utils/schedule_idle_task.js (limited to 'app/javascript/flavours/glitch/features/compose/components/upload.js') diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 33385bc5f..036e0b909 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from './icon_button'; import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; diff --git a/app/javascript/flavours/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js index 0ff5a60bd..42f5d4bc3 100644 --- a/app/javascript/flavours/glitch/components/icon_button.js +++ b/app/javascript/flavours/glitch/components/icon_button.js @@ -1,5 +1,5 @@ import React from 'react'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import PropTypes from 'prop-types'; import classNames from 'classnames'; diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js index aaf8170d9..90667d9f5 100644 --- a/app/javascript/flavours/glitch/components/intersection_observer_article.js +++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import scheduleIdleTask from 'flavours/glitch/utils/schedule_idle_task'; -import getRectFromEntry from 'flavours/glitch/utils/get_rect_from_entry'; +import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; +import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; // Diff these props in the "unrendered" state const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight']; diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index 593bf1356..6dfa73f49 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from 'flavours/glitch/features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import escapeTextContentForBrowser from 'escape-html'; import emojify from 'flavours/glitch/utils/emoji'; diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index ddf83ef7f..8eb2b66d4 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -4,11 +4,11 @@ import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; import LoadMore from './load_more'; import LoadPending from './load_pending'; -import IntersectionObserverWrapper from 'flavours/glitch/utils/intersection_observer_wrapper'; +import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; import classNames from 'classnames'; -import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/utils/fullscreen'; +import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; import LoadingIndicator from './loading_indicator'; import { connect } from 'react-redux'; diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index 0cab9a8a8..21835e628 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -10,7 +10,7 @@ import Icon from 'flavours/glitch/components/icon'; // Utils. import { withPassive } from 'flavours/glitch/utils/dom_helpers'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../../ui/util/optional_motion'; import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; // The spring to use with our motion. diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js index 563a9d687..31f1d4e73 100644 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; import TextIconButton from './text_icon_button'; import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from 'flavours/glitch/features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index b885ae5b0..32a464011 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -16,7 +16,7 @@ import LanguageDropdown from '../containers/language_dropdown_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; // Utils. -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../../ui/util/optional_motion'; import { pollLimits } from 'flavours/glitch/initial_state'; // Messages. diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index 148e43260..a59418e46 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -17,7 +17,7 @@ import Icon from 'flavours/glitch/components/icon'; // Utils. import { focusRoot } from 'flavours/glitch/utils/dom_helpers'; import { searchEnabled } from 'flavours/glitch/initial_state'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../../ui/util/optional_motion'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js index b344dece1..b926c455d 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload.js @@ -1,7 +1,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js index b5126bd4e..8896bbffd 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import Icon from 'flavours/glitch/components/icon'; diff --git a/app/javascript/flavours/glitch/features/compose/components/warning.js b/app/javascript/flavours/glitch/features/compose/components/warning.js index 3bcf9a7f7..4009be8c6 100644 --- a/app/javascript/flavours/glitch/features/compose/components/warning.js +++ b/app/javascript/flavours/glitch/features/compose/components/warning.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; export default class Warning extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/features/compose/index.js b/app/javascript/flavours/glitch/features/compose/index.js index 7c723b048..567bb3711 100644 --- a/app/javascript/flavours/glitch/features/compose/index.js +++ b/app/javascript/flavours/glitch/features/compose/index.js @@ -8,7 +8,7 @@ import { mountCompose, unmountCompose } from 'flavours/glitch/actions/compose'; import { injectIntl, defineMessages } from 'react-intl'; import classNames from 'classnames'; import SearchContainer from './containers/search_container'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import SearchResultsContainer from './containers/search_results_container'; import { me, mascot } from 'flavours/glitch/initial_state'; diff --git a/app/javascript/flavours/glitch/features/list_editor/index.js b/app/javascript/flavours/glitch/features/list_editor/index.js index 505b07cfc..c2ca07053 100644 --- a/app/javascript/flavours/glitch/features/list_editor/index.js +++ b/app/javascript/flavours/glitch/features/list_editor/index.js @@ -8,7 +8,7 @@ import { setupListEditor, clearListSuggestions, resetListEditor } from 'flavours import AccountContainer from './containers/account_container'; import SearchContainer from './containers/search_container'; import EditListForm from './components/edit_list_form'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; const mapStateToProps = state => ({ diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js index 30fc0b6f9..43ae0ec2f 100644 --- a/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js +++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js @@ -7,7 +7,7 @@ import { injectIntl, FormattedMessage } from 'react-intl'; import { fetchPinnedAccounts, clearPinnedAccountsSuggestions, resetPinnedAccountsEditor } from 'flavours/glitch/actions/accounts'; import AccountContainer from './containers/account_container'; import SearchContainer from './containers/search_container'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from 'flavours/glitch/features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; const mapStateToProps = state => ({ diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index ec03e6ba0..46770930f 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -13,7 +13,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from 'flavours/glitch/features/video'; import Audio from 'flavours/glitch/features/audio'; import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon'; -import scheduleIdleTask from 'flavours/glitch/utils/schedule_idle_task'; +import scheduleIdleTask from '../../ui/util/schedule_idle_task'; import classNames from 'classnames'; import PollContainer from 'flavours/glitch/containers/poll_container'; import Icon from 'flavours/glitch/components/icon'; diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 5ce2aab02..c967ef34d 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -48,7 +48,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; import { boostModal, favouriteModal, deleteModal, title } from 'flavours/glitch/initial_state'; -import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/utils/fullscreen'; +import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status'; import Icon from 'flavours/glitch/components/icon'; diff --git a/app/javascript/flavours/glitch/features/ui/components/upload_area.js b/app/javascript/flavours/glitch/features/ui/components/upload_area.js index 0f8f4535c..6958ba9df 100644 --- a/app/javascript/flavours/glitch/features/ui/components/upload_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/upload_area.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Motion from 'flavours/glitch/utils/optional_motion'; +import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { FormattedMessage } from 'react-intl'; diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index f8e868790..f77e89591 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -14,7 +14,7 @@ import { fetchServer } from 'flavours/glitch/actions/server'; import { clearHeight } from 'flavours/glitch/actions/height_cache'; import { changeLayout } from 'flavours/glitch/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; -import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/utils/react_router_helpers'; +import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import PermaLink from 'flavours/glitch/components/permalink'; import ColumnsAreaContainer from './containers/columns_area_container'; diff --git a/app/javascript/flavours/glitch/features/ui/util/fullscreen.js b/app/javascript/flavours/glitch/features/ui/util/fullscreen.js new file mode 100644 index 000000000..cf5d0cf98 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/util/fullscreen.js @@ -0,0 +1,46 @@ +// APIs for normalizing fullscreen operations. Note that Edge uses +// the WebKit-prefixed APIs currently (as of Edge 16). + +export const isFullscreen = () => document.fullscreenElement || + document.webkitFullscreenElement || + document.mozFullScreenElement; + +export const exitFullscreen = () => { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } +}; + +export const requestFullscreen = el => { + if (el.requestFullscreen) { + el.requestFullscreen(); + } else if (el.webkitRequestFullscreen) { + el.webkitRequestFullscreen(); + } else if (el.mozRequestFullScreen) { + el.mozRequestFullScreen(); + } +}; + +export const attachFullscreenListener = (listener) => { + if ('onfullscreenchange' in document) { + document.addEventListener('fullscreenchange', listener); + } else if ('onwebkitfullscreenchange' in document) { + document.addEventListener('webkitfullscreenchange', listener); + } else if ('onmozfullscreenchange' in document) { + document.addEventListener('mozfullscreenchange', listener); + } +}; + +export const detachFullscreenListener = (listener) => { + if ('onfullscreenchange' in document) { + document.removeEventListener('fullscreenchange', listener); + } else if ('onwebkitfullscreenchange' in document) { + document.removeEventListener('webkitfullscreenchange', listener); + } else if ('onmozfullscreenchange' in document) { + document.removeEventListener('mozfullscreenchange', listener); + } +}; diff --git a/app/javascript/flavours/glitch/features/ui/util/get_rect_from_entry.js b/app/javascript/flavours/glitch/features/ui/util/get_rect_from_entry.js new file mode 100644 index 000000000..c266cd7dc --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/util/get_rect_from_entry.js @@ -0,0 +1,21 @@ + +// Get the bounding client rect from an IntersectionObserver entry. +// This is to work around a bug in Chrome: https://crbug.com/737228 + +let hasBoundingRectBug; + +function getRectFromEntry(entry) { + if (typeof hasBoundingRectBug !== 'boolean') { + const boundingRect = entry.target.getBoundingClientRect(); + const observerRect = entry.boundingClientRect; + hasBoundingRectBug = boundingRect.height !== observerRect.height || + boundingRect.top !== observerRect.top || + boundingRect.width !== observerRect.width || + boundingRect.bottom !== observerRect.bottom || + boundingRect.left !== observerRect.left || + boundingRect.right !== observerRect.right; + } + return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect; +} + +export default getRectFromEntry; diff --git a/app/javascript/flavours/glitch/features/ui/util/intersection_observer_wrapper.js b/app/javascript/flavours/glitch/features/ui/util/intersection_observer_wrapper.js new file mode 100644 index 000000000..2b24c6583 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/util/intersection_observer_wrapper.js @@ -0,0 +1,57 @@ +// Wrapper for IntersectionObserver in order to make working with it +// a bit easier. We also follow this performance advice: +// "If you need to observe multiple elements, it is both possible and +// advised to observe multiple elements using the same IntersectionObserver +// instance by calling observe() multiple times." +// https://developers.google.com/web/updates/2016/04/intersectionobserver + +class IntersectionObserverWrapper { + + callbacks = {}; + observerBacklog = []; + observer = null; + + connect (options) { + const onIntersection = (entries) => { + entries.forEach(entry => { + const id = entry.target.getAttribute('data-id'); + if (this.callbacks[id]) { + this.callbacks[id](entry); + } + }); + }; + + this.observer = new IntersectionObserver(onIntersection, options); + this.observerBacklog.forEach(([ id, node, callback ]) => { + this.observe(id, node, callback); + }); + this.observerBacklog = null; + } + + observe (id, node, callback) { + if (!this.observer) { + this.observerBacklog.push([ id, node, callback ]); + } else { + this.callbacks[id] = callback; + this.observer.observe(node); + } + } + + unobserve (id, node) { + if (this.observer) { + delete this.callbacks[id]; + this.observer.unobserve(node); + } + } + + disconnect () { + if (this.observer) { + this.callbacks = {}; + this.observer.disconnect(); + this.observer = null; + } + } + +} + +export default IntersectionObserverWrapper; diff --git a/app/javascript/flavours/glitch/features/ui/util/optional_motion.js b/app/javascript/flavours/glitch/features/ui/util/optional_motion.js new file mode 100644 index 000000000..a7fbe6310 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/util/optional_motion.js @@ -0,0 +1,5 @@ +import { reduceMotion } from 'flavours/glitch/initial_state'; +import ReducedMotion from './reduced_motion'; +import Motion from 'react-motion/lib/Motion'; + +export default reduceMotion ? ReducedMotion : Motion; diff --git a/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js b/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js new file mode 100644 index 000000000..e36c512f3 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Switch, Route } from 'react-router-dom'; + +import ColumnLoading from 'flavours/glitch/features/ui/components/column_loading'; +import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; +import BundleContainer from 'flavours/glitch/features/ui/containers/bundle_container'; + +// Small wrapper to pass multiColumn to the route components +export class WrappedSwitch extends React.PureComponent { + + render () { + const { multiColumn, children } = this.props; + + return ( + + {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} + + ); + } + +} + +WrappedSwitch.propTypes = { + multiColumn: PropTypes.bool, + children: PropTypes.node, +}; + +// Small Wraper to extract the params from the route and pass +// them to the rendered component, together with the content to +// be rendered inside (the children) +export class WrappedRoute extends React.Component { + + static propTypes = { + component: PropTypes.func.isRequired, + content: PropTypes.node, + multiColumn: PropTypes.bool, + componentParams: PropTypes.object, + } + + static defaultProps = { + componentParams: {}, + }; + + renderComponent = ({ match }) => { + const { component, content, multiColumn, componentParams } = this.props; + + return ( + + {Component => {content}} + + ); + } + + renderLoading = () => { + return ; + } + + renderError = (props) => { + return ; + } + + render () { + const { component: Component, content, ...rest } = this.props; + + return ; + } + +} diff --git a/app/javascript/flavours/glitch/features/ui/util/reduced_motion.js b/app/javascript/flavours/glitch/features/ui/util/reduced_motion.js new file mode 100644 index 000000000..95519042b --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/util/reduced_motion.js @@ -0,0 +1,44 @@ +// Like react-motion's Motion, but reduces all animations to cross-fades +// for the benefit of users with motion sickness. +import React from 'react'; +import Motion from 'react-motion/lib/Motion'; +import PropTypes from 'prop-types'; + +const stylesToKeep = ['opacity', 'backgroundOpacity']; + +const extractValue = (value) => { + // This is either an object with a "val" property or it's a number + return (typeof value === 'object' && value && 'val' in value) ? value.val : value; +}; + +class ReducedMotion extends React.Component { + + static propTypes = { + defaultStyle: PropTypes.object, + style: PropTypes.object, + children: PropTypes.func, + } + + render() { + + const { style, defaultStyle, children } = this.props; + + Object.keys(style).forEach(key => { + if (stylesToKeep.includes(key)) { + return; + } + // If it's setting an x or height or scale or some other value, we need + // to preserve the end-state value without actually animating it + style[key] = defaultStyle[key] = extractValue(style[key]); + }); + + return ( + + {children} + + ); + } + +} + +export default ReducedMotion; diff --git a/app/javascript/flavours/glitch/features/ui/util/schedule_idle_task.js b/app/javascript/flavours/glitch/features/ui/util/schedule_idle_task.js new file mode 100644 index 000000000..b04d4a8ee --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/util/schedule_idle_task.js @@ -0,0 +1,29 @@ +// Wrapper to call requestIdleCallback() to schedule low-priority work. +// See https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API +// for a good breakdown of the concepts behind this. + +import Queue from 'tiny-queue'; + +const taskQueue = new Queue(); +let runningRequestIdleCallback = false; + +function runTasks(deadline) { + while (taskQueue.length && deadline.timeRemaining() > 0) { + taskQueue.shift()(); + } + if (taskQueue.length) { + requestIdleCallback(runTasks); + } else { + runningRequestIdleCallback = false; + } +} + +function scheduleIdleTask(task) { + taskQueue.push(task); + if (!runningRequestIdleCallback) { + runningRequestIdleCallback = true; + requestIdleCallback(runTasks); + } +} + +export default scheduleIdleTask; diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index 0a37cff14..cb4655f7f 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -4,7 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { is } from 'immutable'; import { throttle, debounce } from 'lodash'; import classNames from 'classnames'; -import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/utils/fullscreen'; +import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; import Icon from 'flavours/glitch/components/icon'; import Blurhash from 'flavours/glitch/components/blurhash'; diff --git a/app/javascript/flavours/glitch/utils/fullscreen.js b/app/javascript/flavours/glitch/utils/fullscreen.js deleted file mode 100644 index cf5d0cf98..000000000 --- a/app/javascript/flavours/glitch/utils/fullscreen.js +++ /dev/null @@ -1,46 +0,0 @@ -// APIs for normalizing fullscreen operations. Note that Edge uses -// the WebKit-prefixed APIs currently (as of Edge 16). - -export const isFullscreen = () => document.fullscreenElement || - document.webkitFullscreenElement || - document.mozFullScreenElement; - -export const exitFullscreen = () => { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } -}; - -export const requestFullscreen = el => { - if (el.requestFullscreen) { - el.requestFullscreen(); - } else if (el.webkitRequestFullscreen) { - el.webkitRequestFullscreen(); - } else if (el.mozRequestFullScreen) { - el.mozRequestFullScreen(); - } -}; - -export const attachFullscreenListener = (listener) => { - if ('onfullscreenchange' in document) { - document.addEventListener('fullscreenchange', listener); - } else if ('onwebkitfullscreenchange' in document) { - document.addEventListener('webkitfullscreenchange', listener); - } else if ('onmozfullscreenchange' in document) { - document.addEventListener('mozfullscreenchange', listener); - } -}; - -export const detachFullscreenListener = (listener) => { - if ('onfullscreenchange' in document) { - document.removeEventListener('fullscreenchange', listener); - } else if ('onwebkitfullscreenchange' in document) { - document.removeEventListener('webkitfullscreenchange', listener); - } else if ('onmozfullscreenchange' in document) { - document.removeEventListener('mozfullscreenchange', listener); - } -}; diff --git a/app/javascript/flavours/glitch/utils/get_rect_from_entry.js b/app/javascript/flavours/glitch/utils/get_rect_from_entry.js deleted file mode 100644 index c266cd7dc..000000000 --- a/app/javascript/flavours/glitch/utils/get_rect_from_entry.js +++ /dev/null @@ -1,21 +0,0 @@ - -// Get the bounding client rect from an IntersectionObserver entry. -// This is to work around a bug in Chrome: https://crbug.com/737228 - -let hasBoundingRectBug; - -function getRectFromEntry(entry) { - if (typeof hasBoundingRectBug !== 'boolean') { - const boundingRect = entry.target.getBoundingClientRect(); - const observerRect = entry.boundingClientRect; - hasBoundingRectBug = boundingRect.height !== observerRect.height || - boundingRect.top !== observerRect.top || - boundingRect.width !== observerRect.width || - boundingRect.bottom !== observerRect.bottom || - boundingRect.left !== observerRect.left || - boundingRect.right !== observerRect.right; - } - return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect; -} - -export default getRectFromEntry; diff --git a/app/javascript/flavours/glitch/utils/intersection_observer_wrapper.js b/app/javascript/flavours/glitch/utils/intersection_observer_wrapper.js deleted file mode 100644 index 2b24c6583..000000000 --- a/app/javascript/flavours/glitch/utils/intersection_observer_wrapper.js +++ /dev/null @@ -1,57 +0,0 @@ -// Wrapper for IntersectionObserver in order to make working with it -// a bit easier. We also follow this performance advice: -// "If you need to observe multiple elements, it is both possible and -// advised to observe multiple elements using the same IntersectionObserver -// instance by calling observe() multiple times." -// https://developers.google.com/web/updates/2016/04/intersectionobserver - -class IntersectionObserverWrapper { - - callbacks = {}; - observerBacklog = []; - observer = null; - - connect (options) { - const onIntersection = (entries) => { - entries.forEach(entry => { - const id = entry.target.getAttribute('data-id'); - if (this.callbacks[id]) { - this.callbacks[id](entry); - } - }); - }; - - this.observer = new IntersectionObserver(onIntersection, options); - this.observerBacklog.forEach(([ id, node, callback ]) => { - this.observe(id, node, callback); - }); - this.observerBacklog = null; - } - - observe (id, node, callback) { - if (!this.observer) { - this.observerBacklog.push([ id, node, callback ]); - } else { - this.callbacks[id] = callback; - this.observer.observe(node); - } - } - - unobserve (id, node) { - if (this.observer) { - delete this.callbacks[id]; - this.observer.unobserve(node); - } - } - - disconnect () { - if (this.observer) { - this.callbacks = {}; - this.observer.disconnect(); - this.observer = null; - } - } - -} - -export default IntersectionObserverWrapper; diff --git a/app/javascript/flavours/glitch/utils/optional_motion.js b/app/javascript/flavours/glitch/utils/optional_motion.js deleted file mode 100644 index a7fbe6310..000000000 --- a/app/javascript/flavours/glitch/utils/optional_motion.js +++ /dev/null @@ -1,5 +0,0 @@ -import { reduceMotion } from 'flavours/glitch/initial_state'; -import ReducedMotion from './reduced_motion'; -import Motion from 'react-motion/lib/Motion'; - -export default reduceMotion ? ReducedMotion : Motion; diff --git a/app/javascript/flavours/glitch/utils/react_router_helpers.js b/app/javascript/flavours/glitch/utils/react_router_helpers.js deleted file mode 100644 index e36c512f3..000000000 --- a/app/javascript/flavours/glitch/utils/react_router_helpers.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Switch, Route } from 'react-router-dom'; - -import ColumnLoading from 'flavours/glitch/features/ui/components/column_loading'; -import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; -import BundleContainer from 'flavours/glitch/features/ui/containers/bundle_container'; - -// Small wrapper to pass multiColumn to the route components -export class WrappedSwitch extends React.PureComponent { - - render () { - const { multiColumn, children } = this.props; - - return ( - - {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} - - ); - } - -} - -WrappedSwitch.propTypes = { - multiColumn: PropTypes.bool, - children: PropTypes.node, -}; - -// Small Wraper to extract the params from the route and pass -// them to the rendered component, together with the content to -// be rendered inside (the children) -export class WrappedRoute extends React.Component { - - static propTypes = { - component: PropTypes.func.isRequired, - content: PropTypes.node, - multiColumn: PropTypes.bool, - componentParams: PropTypes.object, - } - - static defaultProps = { - componentParams: {}, - }; - - renderComponent = ({ match }) => { - const { component, content, multiColumn, componentParams } = this.props; - - return ( - - {Component => {content}} - - ); - } - - renderLoading = () => { - return ; - } - - renderError = (props) => { - return ; - } - - render () { - const { component: Component, content, ...rest } = this.props; - - return ; - } - -} diff --git a/app/javascript/flavours/glitch/utils/reduced_motion.js b/app/javascript/flavours/glitch/utils/reduced_motion.js deleted file mode 100644 index 95519042b..000000000 --- a/app/javascript/flavours/glitch/utils/reduced_motion.js +++ /dev/null @@ -1,44 +0,0 @@ -// Like react-motion's Motion, but reduces all animations to cross-fades -// for the benefit of users with motion sickness. -import React from 'react'; -import Motion from 'react-motion/lib/Motion'; -import PropTypes from 'prop-types'; - -const stylesToKeep = ['opacity', 'backgroundOpacity']; - -const extractValue = (value) => { - // This is either an object with a "val" property or it's a number - return (typeof value === 'object' && value && 'val' in value) ? value.val : value; -}; - -class ReducedMotion extends React.Component { - - static propTypes = { - defaultStyle: PropTypes.object, - style: PropTypes.object, - children: PropTypes.func, - } - - render() { - - const { style, defaultStyle, children } = this.props; - - Object.keys(style).forEach(key => { - if (stylesToKeep.includes(key)) { - return; - } - // If it's setting an x or height or scale or some other value, we need - // to preserve the end-state value without actually animating it - style[key] = defaultStyle[key] = extractValue(style[key]); - }); - - return ( - - {children} - - ); - } - -} - -export default ReducedMotion; diff --git a/app/javascript/flavours/glitch/utils/schedule_idle_task.js b/app/javascript/flavours/glitch/utils/schedule_idle_task.js deleted file mode 100644 index b04d4a8ee..000000000 --- a/app/javascript/flavours/glitch/utils/schedule_idle_task.js +++ /dev/null @@ -1,29 +0,0 @@ -// Wrapper to call requestIdleCallback() to schedule low-priority work. -// See https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API -// for a good breakdown of the concepts behind this. - -import Queue from 'tiny-queue'; - -const taskQueue = new Queue(); -let runningRequestIdleCallback = false; - -function runTasks(deadline) { - while (taskQueue.length && deadline.timeRemaining() > 0) { - taskQueue.shift()(); - } - if (taskQueue.length) { - requestIdleCallback(runTasks); - } else { - runningRequestIdleCallback = false; - } -} - -function scheduleIdleTask(task) { - taskQueue.push(task); - if (!runningRequestIdleCallback) { - runningRequestIdleCallback = true; - requestIdleCallback(runTasks); - } -} - -export default scheduleIdleTask; -- cgit 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/javascript/flavours/glitch/features/compose/components/upload.js') 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