Move more modules from flavours/glitch/utils to flavours/glitch
This commit is contained in:
@ -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 }));
|
||||
},
|
||||
});
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
import 'intersection-observer';
|
||||
import 'requestidlecallback';
|
||||
import objectFitImages from 'object-fit-images';
|
||||
|
||||
objectFitImages();
|
@ -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;
|
@ -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();
|
||||
}
|
@ -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;
|
@ -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<void>}
|
||||
*/
|
||||
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(<Mastodon {...props} />, 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;
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
@ -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');
|
@ -1,265 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import WebSocketClient from '@gamestdio/websocket';
|
||||
|
||||
/**
|
||||
* @type {WebSocketClient | undefined}
|
||||
*/
|
||||
let sharedConnection;
|
||||
|
||||
/**
|
||||
* @typedef Subscription
|
||||
* @property {string} channelName
|
||||
* @property {Object.<string, string>} params
|
||||
* @property {function(): void} onConnect
|
||||
* @property {function(StreamEvent): void} onReceive
|
||||
* @property {function(): void} onDisconnect
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef StreamEvent
|
||||
* @property {string} event
|
||||
* @property {object} payload
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Array.<Subscription>}
|
||||
*/
|
||||
const subscriptions = [];
|
||||
|
||||
/**
|
||||
* @type {Object.<string, number>}
|
||||
*/
|
||||
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.<string, string>} 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.<string, string>} 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;
|
||||
};
|
@ -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);
|
||||
};
|
Reference in New Issue
Block a user