Rename flavours/glitch/util into flavours/glitch/utils

This commit is contained in:
Claire
2022-10-11 10:41:15 +02:00
parent 1be97ce17f
commit d3879c07b1
139 changed files with 139 additions and 139 deletions

View File

@@ -0,0 +1,183 @@
export function EmojiPicker () {
return import(/* webpackChunkName: "flavours/glitch/async/emoji_picker" */'flavours/glitch/utils/emoji/emoji_picker');
}
export function Compose () {
return import(/* webpackChunkName: "flavours/glitch/async/compose" */'flavours/glitch/features/compose');
}
export function Notifications () {
return import(/* webpackChunkName: "flavours/glitch/async/notifications" */'flavours/glitch/features/notifications');
}
export function HomeTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/home_timeline" */'flavours/glitch/features/home_timeline');
}
export function PublicTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/public_timeline" */'flavours/glitch/features/public_timeline');
}
export function CommunityTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'flavours/glitch/features/community_timeline');
}
export function HashtagTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/hashtag_timeline" */'flavours/glitch/features/hashtag_timeline');
}
export function ListTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/list_timeline" */'flavours/glitch/features/list_timeline');
}
export function Lists () {
return import(/* webpackChunkName: "flavours/glitch/async/lists" */'flavours/glitch/features/lists');
}
export function ListEditor () {
return import(/* webpackChunkName: "flavours/glitch/async/list_editor" */'flavours/glitch/features/list_editor');
}
export function PinnedAccountsEditor () {
return import(/* webpackChunkName: "flavours/glitch/async/pinned_accounts_editor" */'flavours/glitch/features/pinned_accounts_editor');
}
export function DirectTimeline() {
return import(/* webpackChunkName: "flavours/glitch/async/direct_timeline" */'flavours/glitch/features/direct_timeline');
}
export function Status () {
return import(/* webpackChunkName: "flavours/glitch/async/status" */'flavours/glitch/features/status');
}
export function GettingStarted () {
return import(/* webpackChunkName: "flavours/glitch/async/getting_started" */'flavours/glitch/features/getting_started');
}
export function KeyboardShortcuts () {
return import(/* webpackChunkName: "flavours/glitch/async/keyboard_shortcuts" */'flavours/glitch/features/keyboard_shortcuts');
}
export function PinnedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/pinned_statuses" */'flavours/glitch/features/pinned_statuses');
}
export function AccountTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/account_timeline" */'flavours/glitch/features/account_timeline');
}
export function AccountGallery () {
return import(/* webpackChunkName: "flavours/glitch/async/account_gallery" */'flavours/glitch/features/account_gallery');
}
export function Followers () {
return import(/* webpackChunkName: "flavours/glitch/async/followers" */'flavours/glitch/features/followers');
}
export function Following () {
return import(/* webpackChunkName: "flavours/glitch/async/following" */'flavours/glitch/features/following');
}
export function Reblogs () {
return import(/* webpackChunkName: "flavours/glitch/async/reblogs" */'flavours/glitch/features/reblogs');
}
export function Favourites () {
return import(/* webpackChunkName: "flavours/glitch/async/favourites" */'flavours/glitch/features/favourites');
}
export function FollowRequests () {
return import(/* webpackChunkName: "flavours/glitch/async/follow_requests" */'flavours/glitch/features/follow_requests');
}
export function GenericNotFound () {
return import(/* webpackChunkName: "flavours/glitch/async/generic_not_found" */'flavours/glitch/features/generic_not_found');
}
export function FavouritedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/favourited_statuses" */'flavours/glitch/features/favourited_statuses');
}
export function BookmarkedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/bookmarked_statuses" */'flavours/glitch/features/bookmarked_statuses');
}
export function Blocks () {
return import(/* webpackChunkName: "flavours/glitch/async/blocks" */'flavours/glitch/features/blocks');
}
export function DomainBlocks () {
return import(/* webpackChunkName: "flavours/glitch/async/domain_blocks" */'flavours/glitch/features/domain_blocks');
}
export function Mutes () {
return import(/* webpackChunkName: "flavours/glitch/async/mutes" */'flavours/glitch/features/mutes');
}
export function OnboardingModal () {
return import(/* webpackChunkName: "flavours/glitch/async/onboarding_modal" */'flavours/glitch/features/ui/components/onboarding_modal');
}
export function MuteModal () {
return import(/* webpackChunkName: "flavours/glitch/async/mute_modal" */'flavours/glitch/features/ui/components/mute_modal');
}
export function BlockModal () {
return import(/* webpackChunkName: "flavours/glitch/async/block_modal" */'flavours/glitch/features/ui/components/block_modal');
}
export function ReportModal () {
return import(/* webpackChunkName: "flavours/glitch/async/report_modal" */'flavours/glitch/features/ui/components/report_modal');
}
export function SettingsModal () {
return import(/* webpackChunkName: "flavours/glitch/async/settings_modal" */'flavours/glitch/features/local_settings');
}
export function MediaGallery () {
return import(/* webpackChunkName: "flavours/glitch/async/media_gallery" */'flavours/glitch/components/media_gallery');
}
export function Video () {
return import(/* webpackChunkName: "flavours/glitch/async/video" */'flavours/glitch/features/video');
}
export function Audio () {
return import(/* webpackChunkName: "features/glitch/async/audio" */'flavours/glitch/features/audio');
}
export function EmbedModal () {
return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'flavours/glitch/features/ui/components/embed_modal');
}
export function GettingStartedMisc () {
return import(/* webpackChunkName: "flavours/glitch/async/getting_started_misc" */'flavours/glitch/features/getting_started_misc');
}
export function ListAdder () {
return import(/* webpackChunkName: "features/glitch/async/list_adder" */'flavours/glitch/features/list_adder');
}
export function Tesseract () {
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
}
export function Directory () {
return import(/* webpackChunkName: "features/glitch/async/directory" */'flavours/glitch/features/directory');
}
export function FollowRecommendations () {
return import(/* webpackChunkName: "features/glitch/async/follow_recommendations" */'flavours/glitch/features/follow_recommendations');
}
export function CompareHistoryModal () {
return import(/*webpackChunkName: "flavours/glitch/async/compare_history_modal" */'flavours/glitch/features/ui/components/compare_history_modal');
}
export function FilterModal () {
return import(/*webpackChunkName: "flavours/glitch/async/filter_modal" */'flavours/glitch/features/ui/components/filter_modal');
}
export function Explore () {
return import(/* webpackChunkName: "flavours/glitch/async/explore" */'flavours/glitch/features/explore');
}

View File

@@ -0,0 +1,18 @@
export const preferencesLink = '/settings/preferences';
export const profileLink = '/settings/profile';
export const signOutLink = '/auth/sign_out';
export const privacyPolicyLink = '/privacy-policy';
export const accountAdminLink = (id) => `/admin/accounts/${id}`;
export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses?id=${status_id}`;
export const filterEditLink = (id) => `/filters/${id}/edit`;
export const relationshipsLink = '/relationships';
export const securityLink = '/auth/edit';
export const preferenceLink = (setting_name) => {
switch (setting_name) {
case 'user_setting_expand_spoilers':
case 'user_setting_disable_swiping':
return `/settings/preferences/appearance#${setting_name}`;
default:
return preferencesLink;
}
};

View File

@@ -0,0 +1,10 @@
export const decode = base64 => {
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};

View File

@@ -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 './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 }));
},
});
}

View File

@@ -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;
}
};

View File

@@ -0,0 +1,10 @@
import ready from './ready';
export let assetHost = '';
ready(() => {
const cdnHost = document.querySelector('meta[name=cdn-host]');
if (cdnHost) {
assetHost = cdnHost.content || '';
}
});

View File

@@ -0,0 +1,31 @@
import { expandSpoilers } from 'flavours/glitch/initial_state';
function _autoUnfoldCW(spoiler_text, skip_unfold_regex) {
if (!expandSpoilers)
return false;
if (!skip_unfold_regex)
return true;
let regex = null;
try {
regex = new RegExp(skip_unfold_regex.trim(), 'i');
} catch (e) {
// Bad regex, skip filters
return true;
}
return !regex.test(spoiler_text);
}
export function autoHideCW(settings, spoiler_text) {
return !_autoUnfoldCW(spoiler_text, settings.getIn(['content_warnings', 'filter']));
}
export function autoUnfoldCW(settings, status) {
if (!status)
return false;
return _autoUnfoldCW(status.get('spoiler_text'), settings.getIn(['content_warnings', 'filter']));
}

View File

@@ -0,0 +1,9 @@
import { urlRegex } from './url_regex';
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
export function countableText(inputText) {
return inputText
.replace(urlRegex, urlPlaceholder)
.replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3');
};

View File

@@ -0,0 +1,14 @@
// Package imports.
import { supportsPassiveEvents } from 'detect-passive-events';
// This will either be a passive lister options object (if passive
// events are supported), or `false`.
export const withPassive = supportsPassiveEvents ? { passive: true } : false;
// Focuses the root element.
export function focusRoot () {
let e;
if (document && (e = document.querySelector('.ui')) && (e = e.parentElement)) {
e.focus();
}
}

View File

@@ -0,0 +1,122 @@
// @preval
// http://www.unicode.org/Public/emoji/5.0/emoji-test.txt
// This file contains the compressed version of the emoji data from
// both emoji_map.json and from emoji-mart's emojiIndex and data objects.
// It's designed to be emitted in an array format to take up less space
// over the wire.
const { unicodeToFilename } = require('./unicode_to_filename');
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
const emojiMap = require('./emoji_map.json');
const { emojiIndex } = require('emoji-mart');
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
let data = require('emoji-mart/data/all.json');
if(data.compressed) {
data = emojiMartUncompress(data);
}
const emojiMartData = data;
const excluded = ['®', '©', '™'];
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
const shortcodeMap = {};
const shortCodesToEmojiData = {};
const emojisWithoutShortCodes = [];
Object.keys(emojiIndex.emojis).forEach(key => {
let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this
if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
emoji = emoji['1'];
}
shortcodeMap[emoji.native] = emoji.id;
});
const stripModifiers = unicode => {
skinTones.forEach(tone => {
unicode = unicode.replace(tone, '');
});
return unicode;
};
Object.keys(emojiMap).forEach(key => {
if (excluded.includes(key)) {
delete emojiMap[key];
return;
}
const normalizedKey = stripModifiers(key);
let shortcode = shortcodeMap[normalizedKey];
if (!shortcode) {
shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
}
const filename = emojiMap[key];
const filenameData = [key];
if (unicodeToFilename(key) !== filename) {
// filename can't be derived using unicodeToFilename
filenameData.push(filename);
}
if (typeof shortcode === 'undefined') {
emojisWithoutShortCodes.push(filenameData);
} else {
if (!Array.isArray(shortCodesToEmojiData[shortcode])) {
shortCodesToEmojiData[shortcode] = [[]];
}
shortCodesToEmojiData[shortcode][0].push(filenameData);
}
});
Object.keys(emojiIndex.emojis).forEach(key => {
let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this
if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
emoji = emoji['1'];
}
const { native } = emoji;
let { short_names, search, unified } = emojiMartData.emojis[key];
if (short_names[0] !== key) {
throw new Error('The compresser expects the first short_code to be the ' +
'key. It may need to be rewritten if the emoji change such that this ' +
'is no longer the case.');
}
short_names = short_names.slice(1); // first short name can be inferred from the key
const searchData = [native, short_names, search];
if (unicodeToUnifiedName(native) !== unified) {
// unified name can't be derived from unicodeToUnifiedName
searchData.push(unified);
}
if (!Array.isArray(shortCodesToEmojiData[key])) {
shortCodesToEmojiData[key] = [[]];
}
shortCodesToEmojiData[key].push(searchData);
});
// JSON.parse/stringify is to emulate what @preval is doing and avoid any
// inconsistent behavior in dev mode
module.exports = JSON.parse(JSON.stringify([
shortCodesToEmojiData,
emojiMartData.skins,
emojiMartData.categories,
emojiMartData.aliases,
emojisWithoutShortCodes,
]));

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,41 @@
// The output of this module is designed to mimic emoji-mart's
// "data" object, such that we can use it for a light version of emoji-mart's
// emojiIndex.search functionality.
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
const [ shortCodesToEmojiData, skins, categories, short_names ] = require('./emoji_compressed');
const emojis = {};
// decompress
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
let [
filenameData, // eslint-disable-line no-unused-vars
searchData,
] = shortCodesToEmojiData[shortCode];
let [
native,
short_names,
search,
unified,
] = searchData;
if (!unified) {
// unified name can be derived from unicodeToUnifiedName
unified = unicodeToUnifiedName(native);
}
short_names = [shortCode].concat(short_names);
emojis[shortCode] = {
native,
search,
short_names,
unified,
};
});
module.exports = {
emojis,
skins,
categories,
short_names,
};

View File

@@ -0,0 +1,185 @@
// This code is largely borrowed from:
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
import data from './emoji_mart_data_light';
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
let originalPool = {};
let index = {};
let emojisList = {};
let emoticonsList = {};
let customEmojisList = [];
for (let emoji in data.emojis) {
let emojiData = data.emojis[emoji];
let { short_names, emoticons } = emojiData;
let id = short_names[0];
if (emoticons) {
emoticons.forEach(emoticon => {
if (emoticonsList[emoticon]) {
return;
}
emoticonsList[emoticon] = id;
});
}
emojisList[id] = getSanitizedData(id);
originalPool[id] = emojiData;
}
function clearCustomEmojis(pool) {
customEmojisList.forEach((emoji) => {
let emojiId = emoji.id || emoji.short_names[0];
delete pool[emojiId];
delete emojisList[emojiId];
});
}
function addCustomToPool(custom, pool) {
if (customEmojisList.length) clearCustomEmojis(pool);
custom.forEach((emoji) => {
let emojiId = emoji.id || emoji.short_names[0];
if (emojiId && !pool[emojiId]) {
pool[emojiId] = getData(emoji);
emojisList[emojiId] = getSanitizedData(emoji);
}
});
customEmojisList = custom;
index = {};
}
function search(value, { emojisToShowFilter, maxResults, include, exclude, custom } = {}) {
if (custom !== undefined) {
if (customEmojisList !== custom)
addCustomToPool(custom, originalPool);
} else {
custom = [];
}
maxResults = maxResults || 75;
include = include || [];
exclude = exclude || [];
let results = null,
pool = originalPool;
if (value.length) {
if (value === '-' || value === '-1') {
return [emojisList['-1']];
}
let values = value.toLowerCase().split(/[\s|,\-_]+/),
allResults = [];
if (values.length > 2) {
values = [values[0], values[1]];
}
if (include.length || exclude.length) {
pool = {};
data.categories.forEach(category => {
let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
if (!isIncluded || isExcluded) {
return;
}
category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]);
});
if (custom.length) {
let customIsIncluded = include && include.length ? include.indexOf('custom') > -1 : true;
let customIsExcluded = exclude && exclude.length ? exclude.indexOf('custom') > -1 : false;
if (customIsIncluded && !customIsExcluded) {
addCustomToPool(custom, pool);
}
}
}
const searchValue = (value) => {
let aPool = pool,
aIndex = index,
length = 0;
for (let charIndex = 0; charIndex < value.length; charIndex++) {
const char = value[charIndex];
length++;
aIndex[char] = aIndex[char] || {};
aIndex = aIndex[char];
if (!aIndex.results) {
let scores = {};
aIndex.results = [];
aIndex.pool = {};
for (let id in aPool) {
let emoji = aPool[id],
{ search } = emoji,
sub = value.slice(0, length),
subIndex = search.indexOf(sub);
if (subIndex !== -1) {
let score = subIndex + 1;
if (sub === id) score = 0;
aIndex.results.push(emojisList[id]);
aIndex.pool[id] = emoji;
scores[id] = score;
}
}
aIndex.results.sort((a, b) => {
let aScore = scores[a.id],
bScore = scores[b.id];
return aScore - bScore;
});
}
aPool = aIndex.pool;
}
return aIndex.results;
};
if (values.length > 1) {
results = searchValue(value);
} else {
results = [];
}
allResults = values.map(searchValue).filter(a => a);
if (allResults.length > 1) {
allResults = intersect.apply(null, allResults);
} else if (allResults.length) {
allResults = allResults[0];
}
results = uniq(results.concat(allResults));
}
if (results) {
if (emojisToShowFilter) {
results = results.filter((result) => emojisToShowFilter(data.emojis[result.id]));
}
if (results && results.length > maxResults) {
results = results.slice(0, maxResults);
}
}
return results;
}
export { search };

View File

@@ -0,0 +1,7 @@
import Picker from 'emoji-mart/dist-es/components/picker/picker';
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
export {
Picker,
Emoji,
};

View File

@@ -0,0 +1,35 @@
// A mapping of unicode strings to an object containing the filename
// (i.e. the svg filename) and a shortCode intended to be shown
// as a "title" attribute in an HTML element (aka tooltip).
const [
shortCodesToEmojiData,
skins, // eslint-disable-line no-unused-vars
categories, // eslint-disable-line no-unused-vars
short_names, // eslint-disable-line no-unused-vars
emojisWithoutShortCodes,
] = require('./emoji_compressed');
const { unicodeToFilename } = require('./unicode_to_filename');
// decompress
const unicodeMapping = {};
function processEmojiMapData(emojiMapData, shortCode) {
let [ native, filename ] = emojiMapData;
if (!filename) {
// filename name can be derived from unicodeToFilename
filename = unicodeToFilename(native);
}
unicodeMapping[native] = {
shortCode: shortCode,
filename: filename,
};
}
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
let [ filenameData ] = shortCodesToEmojiData[shortCode];
filenameData.forEach(emojiMapData => processEmojiMapData(emojiMapData, shortCode));
});
emojisWithoutShortCodes.forEach(emojiMapData => processEmojiMapData(emojiMapData));
module.exports = unicodeMapping;

View File

@@ -0,0 +1,258 @@
// This code is largely borrowed from:
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js
import data from './emoji_mart_data_light';
const buildSearch = (data) => {
const search = [];
let addToSearch = (strings, split) => {
if (!strings) {
return;
}
(Array.isArray(strings) ? strings : [strings]).forEach((string) => {
(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => {
s = s.toLowerCase();
if (search.indexOf(s) === -1) {
search.push(s);
}
});
});
};
addToSearch(data.short_names, true);
addToSearch(data.name, true);
addToSearch(data.keywords, false);
addToSearch(data.emoticons, false);
return search.join(',');
};
const _String = String;
const stringFromCodePoint = _String.fromCodePoint || function () {
let MAX_SIZE = 0x4000;
let codeUnits = [];
let highSurrogate;
let lowSurrogate;
let index = -1;
let length = arguments.length;
if (!length) {
return '';
}
let result = '';
while (++index < length) {
let codePoint = Number(arguments[index]);
if (
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
codePoint < 0 || // not a valid Unicode code point
codePoint > 0x10FFFF || // not a valid Unicode code point
Math.floor(codePoint) !== codePoint // not an integer
) {
throw RangeError('Invalid code point: ' + codePoint);
}
if (codePoint <= 0xFFFF) { // BMP code point
codeUnits.push(codePoint);
} else { // Astral code point; split in surrogate halves
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
codePoint -= 0x10000;
highSurrogate = (codePoint >> 10) + 0xD800;
lowSurrogate = (codePoint % 0x400) + 0xDC00;
codeUnits.push(highSurrogate, lowSurrogate);
}
if (index + 1 === length || codeUnits.length > MAX_SIZE) {
result += String.fromCharCode.apply(null, codeUnits);
codeUnits.length = 0;
}
}
return result;
};
const _JSON = JSON;
const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/;
const SKINS = [
'1F3FA', '1F3FB', '1F3FC',
'1F3FD', '1F3FE', '1F3FF',
];
function unifiedToNative(unified) {
let unicodes = unified.split('-'),
codePoints = unicodes.map((u) => `0x${u}`);
return stringFromCodePoint.apply(null, codePoints);
}
function sanitize(emoji) {
let { name, short_names, skin_tone, skin_variations, emoticons, unified, custom, imageUrl } = emoji,
id = emoji.id || short_names[0],
colons = `:${id}:`;
if (custom) {
return {
id,
name,
colons,
emoticons,
custom,
imageUrl,
};
}
if (skin_tone) {
colons += `:skin-tone-${skin_tone}:`;
}
return {
id,
name,
colons,
emoticons,
unified: unified.toLowerCase(),
skin: skin_tone || (skin_variations ? 1 : null),
native: unifiedToNative(unified),
};
}
function getSanitizedData() {
return sanitize(getData(...arguments));
}
function getData(emoji, skin, set) {
let emojiData = {};
if (typeof emoji === 'string') {
let matches = emoji.match(COLONS_REGEX);
if (matches) {
emoji = matches[1];
if (matches[2]) {
skin = parseInt(matches[2]);
}
}
if (data.short_names.hasOwnProperty(emoji)) {
emoji = data.short_names[emoji];
}
if (data.emojis.hasOwnProperty(emoji)) {
emojiData = data.emojis[emoji];
}
} else if (emoji.id) {
if (data.short_names.hasOwnProperty(emoji.id)) {
emoji.id = data.short_names[emoji.id];
}
if (data.emojis.hasOwnProperty(emoji.id)) {
emojiData = data.emojis[emoji.id];
skin = skin || emoji.skin;
}
}
if (!Object.keys(emojiData).length) {
emojiData = emoji;
emojiData.custom = true;
if (!emojiData.search) {
emojiData.search = buildSearch(emoji);
}
}
emojiData.emoticons = emojiData.emoticons || [];
emojiData.variations = emojiData.variations || [];
if (emojiData.skin_variations && skin > 1 && set) {
emojiData = JSON.parse(_JSON.stringify(emojiData));
let skinKey = SKINS[skin - 1],
variationData = emojiData.skin_variations[skinKey];
if (!variationData.variations && emojiData.variations) {
delete emojiData.variations;
}
if (variationData[`has_img_${set}`]) {
emojiData.skin_tone = skin;
for (let k in variationData) {
let v = variationData[k];
emojiData[k] = v;
}
}
}
if (emojiData.variations && emojiData.variations.length) {
emojiData = JSON.parse(_JSON.stringify(emojiData));
emojiData.unified = emojiData.variations.shift();
}
return emojiData;
}
function uniq(arr) {
return arr.reduce((acc, item) => {
if (acc.indexOf(item) === -1) {
acc.push(item);
}
return acc;
}, []);
}
function intersect(a, b) {
const uniqA = uniq(a);
const uniqB = uniq(b);
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
}
function deepMerge(a, b) {
let o = {};
for (let key in a) {
let originalValue = a[key],
value = originalValue;
if (b.hasOwnProperty(key)) {
value = b[key];
}
if (typeof value === 'object') {
value = deepMerge(originalValue, value);
}
o[key] = value;
}
return o;
}
// https://github.com/sonicdoe/measure-scrollbar
function measureScrollbar() {
const div = document.createElement('div');
div.style.width = '100px';
div.style.height = '100px';
div.style.overflow = 'scroll';
div.style.position = 'absolute';
div.style.top = '-9999px';
document.body.appendChild(div);
const scrollbarWidth = div.offsetWidth - div.clientWidth;
document.body.removeChild(div);
return scrollbarWidth;
}
export {
getData,
getSanitizedData,
uniq,
intersect,
deepMerge,
unifiedToNative,
measureScrollbar,
};

View File

@@ -0,0 +1,116 @@
import { autoPlayGif, useSystemEmojiFont } from 'flavours/glitch/initial_state';
import unicodeMapping from './emoji_unicode_mapping_light';
import { assetHost } from 'flavours/glitch/utils/config';
import Trie from 'substring-trie';
const trie = new Trie(Object.keys(unicodeMapping));
// Convert to file names from emojis. (For different variation selector emojis)
const emojiFilenames = (emojis) => {
return emojis.map(v => unicodeMapping[v].filename);
};
// Emoji requiring extra borders depending on theme
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺', '📱', '📲', '🚲']);
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
const emojiFilename = (filename) => {
const borderedEmoji = (document.body && document.body.classList.contains('skin-mastodon-light')) ? lightEmoji : darkEmoji;
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
};
const emojify = (str, customEmojis = {}) => {
const tagCharsWithoutEmojis = '<&';
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
for (;;) {
let match, i = 0, tag;
while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || useSystemEmojiFont || !(match = trie.search(str.slice(i))))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
}
let rend, replacement = '';
if (i === str.length) {
break;
} else if (str[i] === ':') {
if (!(() => {
rend = str.indexOf(':', i + 1) + 1;
if (!rend) return false; // no pair of ':'
const lt = str.indexOf('<', i + 1);
if (!(lt === -1 || lt >= rend)) return false; // tag appeared before closing ':'
const shortname = str.slice(i, rend);
// now got a replacee as ':shortname:'
// if you want additional emoji handler, add statements below which set replacement and return true.
if (shortname in customEmojis) {
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = `<img draggable="false" class="emojione custom-emoji" alt="${shortname}" title="${shortname}" src="${filename}" data-original="${customEmojis[shortname].url}" data-static="${customEmojis[shortname].static_url}" />`;
return true;
}
return false;
})()) rend = ++i;
} else if (tag >= 0) { // <, &
rend = str.indexOf('>;'[tag], i + 1) + 1;
if (!rend) {
break;
}
if (tag === 0) {
if (invisible) {
if (str[i + 1] === '/') { // closing tag
if (!--invisible) {
tagChars = tagCharsWithEmojis;
}
} else if (str[rend - 2] !== '/') { // opening tag
invisible++;
}
} else {
if (str.startsWith('<span class="invisible">', i)) {
// avoid emojifying on invisible text
invisible = 1;
tagChars = tagCharsWithoutEmojis;
}
}
}
i = rend;
} else if (!useSystemEmojiFont) { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match];
const title = shortCode ? `:${shortCode}:` : '';
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${emojiFilename(filename)}.svg" />`;
rend = i + match.length;
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
if (str.codePointAt(rend) === 65038) {
rend += 1;
}
}
rtn += str.slice(0, i) + replacement;
str = str.slice(rend);
}
return rtn + str;
};
export default emojify;
export { unicodeMapping };
export const buildCustomEmojis = (customEmojis) => {
const emojis = [];
customEmojis.forEach(emoji => {
const shortcode = emoji.get('shortcode');
const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
const name = shortcode.replace(':', '');
emojis.push({
id: name,
name,
short_names: [name],
text: '',
emoticons: [],
keywords: [name],
imageUrl: url,
custom: true,
customCategory: emoji.get('category'),
});
});
return emojis;
};
export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom']));

View File

@@ -0,0 +1,26 @@
// taken from:
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
exports.unicodeToFilename = (str) => {
let result = '';
let charCode = 0;
let p = 0;
let i = 0;
while (i < str.length) {
charCode = str.charCodeAt(i++);
if (p) {
if (result.length > 0) {
result += '-';
}
result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16);
p = 0;
} else if (0xD800 <= charCode && charCode <= 0xDBFF) {
p = charCode;
} else {
if (result.length > 0) {
result += '-';
}
result += charCode.toString(16);
}
}
return result;
};

View File

@@ -0,0 +1,21 @@
function padLeft(str, num) {
while (str.length < num) {
str = '0' + str;
}
return str;
}
exports.unicodeToUnifiedName = (str) => {
let output = '';
for (let i = 0; i < str.length; i += 2) {
if (i > 0) {
output += '-';
}
output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4);
}
return output;
};

View File

@@ -0,0 +1,5 @@
import 'intersection-observer';
import 'requestidlecallback';
import objectFitImages from 'object-fit-images';
objectFitImages();

View File

@@ -0,0 +1,16 @@
export const toServerSideType = columnType => {
switch (columnType) {
case 'home':
case 'notifications':
case 'public':
case 'thread':
case 'account':
return columnType;
default:
if (columnType.indexOf('list:') > -1) {
return 'home';
} else {
return 'public'; // community, account, hashtag
}
}
};

View File

@@ -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);
}
};

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
export function recoverHashtags (recognizedTags, text) {
return recognizedTags.map(tag => {
const re = new RegExp(`(?:^|[^\/\)\w])#(${tag.name})`, 'i');
const matched_hashtag = text.match(re);
return matched_hashtag ? matched_hashtag[1] : null;
}
).filter(x => x !== null);
}

View File

@@ -0,0 +1,5 @@
export const unescapeHTML = (html) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, '');
return wrapper.textContent;
};

View File

@@ -0,0 +1,15 @@
import React from 'react';
// Copied from emoji-mart for consistency with emoji picker and since
// they don't export the icons in the package
export const loupeIcon = (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
<path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
</svg>
);
export const deleteIcon = (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
<path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
</svg>
);

View File

@@ -0,0 +1,10 @@
import punycode from 'punycode';
const IDNA_PREFIX = 'xn--';
export const decode = domain => {
return domain
.split('.')
.map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
.join('.');
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,5 @@
// This function returns the new value unless it is `null` or
// `undefined`, in which case it returns the old one.
export function overwrite (oldVal, newVal) {
return newVal === null || typeof newVal === 'undefined' ? oldVal : newVal;
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -0,0 +1,34 @@
import Rails from '@rails/ujs';
import { signOutLink } from 'flavours/glitch/utils/backend_links';
export const logOut = () => {
const form = document.createElement('form');
const methodInput = document.createElement('input');
methodInput.setAttribute('name', '_method');
methodInput.setAttribute('value', 'delete');
methodInput.setAttribute('type', 'hidden');
form.appendChild(methodInput);
const csrfToken = Rails.csrfToken();
const csrfParam = Rails.csrfParam();
if (csrfParam && csrfToken) {
const csrfInput = document.createElement('input');
csrfInput.setAttribute('name', csrfParam);
csrfInput.setAttribute('value', csrfToken);
csrfInput.setAttribute('type', 'hidden');
form.appendChild(csrfInput);
}
const submitButton = document.createElement('input');
submitButton.setAttribute('type', 'submit');
form.appendChild(submitButton);
form.method = 'post';
form.action = signOutLink;
form.style.display = 'none';
document.body.appendChild(form);
submitButton.click();
};

View File

@@ -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/utils/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;

View File

@@ -0,0 +1,30 @@
// Handles browser quirks, based on
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
const checkNotificationPromise = () => {
try {
// eslint-disable-next-line promise/catch-or-return
Notification.requestPermission().then();
} catch(e) {
return false;
}
return true;
};
const handlePermission = (permission, callback) => {
// Whatever the user answers, we make sure Chrome stores the information
if(!('permission' in Notification)) {
Notification.permission = permission;
}
callback(Notification.permission);
};
export const requestNotificationPermission = (callback) => {
if (checkNotificationPromise()) {
Notification.requestPermission().then((permission) => handlePermission(permission, callback)).catch(console.warn);
} else {
Notification.requestPermission((permission) => handlePermission(permission, callback));
}
};

View File

@@ -0,0 +1,79 @@
// @ts-check
export const DECIMAL_UNITS = Object.freeze({
ONE: 1,
TEN: 10,
HUNDRED: Math.pow(10, 2),
THOUSAND: Math.pow(10, 3),
MILLION: Math.pow(10, 6),
BILLION: Math.pow(10, 9),
TRILLION: Math.pow(10, 12),
});
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
/**
* @typedef {[number, number, number]} ShortNumber
* Array of: shorten number, unit of shorten number and maximum fraction digits
*/
/**
* @param {number} sourceNumber Number to convert to short number
* @returns {ShortNumber} Calculated short number
* @example
* shortNumber(5936);
* // => [5.936, 1000, 1]
*/
export function toShortNumber(sourceNumber) {
if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
return [sourceNumber, DECIMAL_UNITS.ONE, 0];
} else if (sourceNumber < DECIMAL_UNITS.MILLION) {
return [
sourceNumber / DECIMAL_UNITS.THOUSAND,
DECIMAL_UNITS.THOUSAND,
sourceNumber < TEN_THOUSAND ? 1 : 0,
];
} else if (sourceNumber < DECIMAL_UNITS.BILLION) {
return [
sourceNumber / DECIMAL_UNITS.MILLION,
DECIMAL_UNITS.MILLION,
sourceNumber < TEN_MILLIONS ? 1 : 0,
];
} else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
return [
sourceNumber / DECIMAL_UNITS.BILLION,
DECIMAL_UNITS.BILLION,
0,
];
}
return [sourceNumber, DECIMAL_UNITS.ONE, 0];
}
/**
* @param {number} sourceNumber Original number that is shortened
* @param {number} division The scale in which short number is displayed
* @returns {number} Number that can be used for plurals when short form used
* @example
* pluralReady(1793, DECIMAL_UNITS.THOUSAND)
* // => 1790
*/
export function pluralReady(sourceNumber, division) {
// eslint-disable-next-line eqeqeq
if (division == null || division < DECIMAL_UNITS.HUNDRED) {
return sourceNumber;
}
let closestScale = division / DECIMAL_UNITS.TEN;
return Math.trunc(sourceNumber / closestScale) * closestScale;
}
/**
* @param {number} num
* @returns {number}
*/
export function roundTo10(num) {
return Math.round(num * 0.1) / 0.1;
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -0,0 +1,5 @@
export const order = ['public', 'unlisted', 'private', 'direct'];
export function privacyPreference (a, b) {
return order[Math.max(order.indexOf(a), order.indexOf(b), 0)];
};

View File

@@ -0,0 +1,21 @@
// This function binds the given `handlers` to the `target`.
export function assignHandlers (target, handlers) {
if (!target || !handlers) {
return;
}
// We just bind each handler to the `target`.
const handle = target.handlers = {};
Object.keys(handlers).forEach(
key => handle[key] = handlers[key].bind(target)
);
}
// This function only returns the component if the result of calling
// `test` with `data` is `true`. Useful with funciton binding.
export function conditionalRender (test, data, component) {
return test(data) ? component : null;
}
// This object provides props to make the component not visible.
export const hiddenComponent = { style: { display: 'none' } };

View File

@@ -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 (
<Switch>
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
</Switch>
);
}
}
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 (
<BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
{Component => <Component params={match.params} multiColumn={multiColumn} {...componentParams}>{content}</Component>}
</BundleContainer>
);
}
renderLoading = () => {
return <ColumnLoading />;
}
renderError = (props) => {
return <BundleColumnError {...props} />;
}
render () {
const { component: Component, content, ...rest } = this.props;
return <Route {...rest} render={this.renderComponent} />;
}
}

View File

@@ -0,0 +1,32 @@
// @ts-check
/**
* @param {(() => void) | (() => Promise<void>)} callback
* @returns {Promise<void>}
*/
export default function ready(callback) {
return new Promise((resolve, reject) => {
function loaded() {
let result;
try {
result = callback();
} catch (err) {
reject(err);
return;
}
if (typeof result?.then === 'function') {
result.then(resolve).catch(reject);
} else {
resolve();
}
}
if (['interactive', 'complete'].includes(document.readyState)) {
loaded();
} else {
document.addEventListener('DOMContentLoaded', loaded);
}
});
}

View File

@@ -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 (
<Motion style={style} defaultStyle={defaultStyle}>
{children}
</Motion>
);
}
}
export default ReducedMotion;

View File

@@ -0,0 +1,8 @@
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
// Connects a component.
export function wrap (Component, mapStateToProps, mapDispatchToProps, options) {
const withIntl = typeof options === 'object' ? options.withIntl : !!options;
return (withIntl ? injectIntl : i => i)(connect(mapStateToProps, mapDispatchToProps)(Component));
}

View File

@@ -0,0 +1,189 @@
import EXIF from 'exif-js';
const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
const _browser_quirks = {};
// Some browsers will automatically draw images respecting their EXIF orientation
// while others won't, and the safest way to detect that is to examine how it
// is done on a known image.
// See https://github.com/w3c/csswg-drafts/issues/4666
// and https://github.com/blueimp/JavaScript-Load-Image/commit/1e4df707821a0afcc11ea0720ee403b8759f3881
const dropOrientationIfNeeded = (orientation) => new Promise(resolve => {
switch (_browser_quirks['image-orientation-automatic']) {
case true:
resolve(1);
break;
case false:
resolve(orientation);
break;
default:
// black 2x1 JPEG, with the following meta information set:
// - EXIF Orientation: 6 (Rotated 90° CCW)
const testImageURL =
'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' +
'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' +
'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' +
'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' +
'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' +
'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==';
const img = new Image();
img.onload = () => {
const automatic = (img.width === 1 && img.height === 2);
_browser_quirks['image-orientation-automatic'] = automatic;
resolve(automatic ? 1 : orientation);
};
img.onerror = () => {
_browser_quirks['image-orientation-automatic'] = false;
resolve(orientation);
};
img.src = testImageURL;
}
});
// Some browsers don't allow reading from a canvas and instead return all-white
// or randomized data. Use a pre-defined image to check if reading the canvas
// works.
const checkCanvasReliability = () => new Promise((resolve, reject) => {
switch(_browser_quirks['canvas-read-unreliable']) {
case true:
reject('Canvas reading unreliable');
break;
case false:
resolve();
break;
default:
// 2×2 GIF with white, red, green and blue pixels
const testImageURL =
'data:image/gif;base64,R0lGODdhAgACAKEDAAAA//8AAAD/AP///ywAAAAAAgACAAACA1wEBQA7';
const refData =
[255, 255, 255, 255, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0, 2, 2);
const imageData = context.getImageData(0, 0, 2, 2);
if (imageData.data.every((x, i) => refData[i] === x)) {
_browser_quirks['canvas-read-unreliable'] = false;
resolve();
} else {
_browser_quirks['canvas-read-unreliable'] = true;
reject('Canvas reading unreliable');
}
};
img.onerror = () => {
_browser_quirks['canvas-read-unreliable'] = true;
reject('Failed to load test image');
};
img.src = testImageURL;
}
});
const getImageUrl = inputFile => new Promise((resolve, reject) => {
if (window.URL && URL.createObjectURL) {
try {
resolve(URL.createObjectURL(inputFile));
} catch (error) {
reject(error);
}
return;
}
const reader = new FileReader();
reader.onerror = (...args) => reject(...args);
reader.onload = ({ target }) => resolve(target.result);
reader.readAsDataURL(inputFile);
});
const loadImage = inputFile => new Promise((resolve, reject) => {
getImageUrl(inputFile).then(url => {
const img = new Image();
img.onerror = (...args) => reject(...args);
img.onload = () => resolve(img);
img.src = url;
}).catch(reject);
});
const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
if (!['image/jpeg', 'image/webp'].includes(type)) {
resolve(1);
return;
}
EXIF.getData(img, () => {
const orientation = EXIF.getTag(img, 'Orientation');
if (orientation !== 1) {
dropOrientationIfNeeded(orientation).then(resolve).catch(() => resolve(orientation));
} else {
resolve(orientation);
}
});
});
const processImage = (img, { width, height, orientation, type = 'image/png' }) => new Promise(resolve => {
const canvas = document.createElement('canvas');
if (4 < orientation && orientation < 9) {
canvas.width = height;
canvas.height = width;
} else {
canvas.width = width;
canvas.height = height;
}
const context = canvas.getContext('2d');
switch (orientation) {
case 2: context.transform(-1, 0, 0, 1, width, 0); break;
case 3: context.transform(-1, 0, 0, -1, width, height); break;
case 4: context.transform(1, 0, 0, -1, 0, height); break;
case 5: context.transform(0, 1, 1, 0, 0, 0); break;
case 6: context.transform(0, 1, -1, 0, height, 0); break;
case 7: context.transform(0, -1, -1, 0, height, width); break;
case 8: context.transform(0, -1, 1, 0, 0, width); break;
}
context.drawImage(img, 0, 0, width, height);
canvas.toBlob(resolve, type);
});
const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) => {
const { width, height } = img;
const newWidth = Math.round(Math.sqrt(MAX_IMAGE_PIXELS * (width / height)));
const newHeight = Math.round(Math.sqrt(MAX_IMAGE_PIXELS * (height / width)));
checkCanvasReliability()
.then(getOrientation(img, type))
.then(orientation => processImage(img, {
width: newWidth,
height: newHeight,
orientation,
type,
}))
.then(resolve)
.catch(reject);
});
export default inputFile => new Promise((resolve) => {
if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') {
resolve(inputFile);
return;
}
loadImage(inputFile).then(img => {
if (img.width * img.height < MAX_IMAGE_PIXELS) {
resolve(inputFile);
return;
}
resizeImage(img, inputFile.type)
.then(resolve)
.catch(() => resolve(inputFile));
}).catch(() => resolve(inputFile));
});

View File

@@ -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;

View File

@@ -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);

View File

@@ -0,0 +1,34 @@
/** @type {number | null} */
let cachedScrollbarWidth = null;
/**
* @return {number}
*/
const getActualScrollbarWidth = () => {
const outer = document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.overflow = 'scroll';
document.body.appendChild(outer);
const inner = document.createElement('div');
outer.appendChild(inner);
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
outer.parentNode.removeChild(outer);
return scrollbarWidth;
};
/**
* @return {number}
*/
export const getScrollbarWidth = () => {
if (cachedScrollbarWidth !== null) {
return cachedScrollbarWidth;
}
const scrollbarWidth = getActualScrollbarWidth();
cachedScrollbarWidth = scrollbarWidth;
return scrollbarWidth;
};

View File

@@ -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');

View File

@@ -0,0 +1,265 @@
// @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;
};

View File

@@ -0,0 +1,30 @@
import regexSupplant from 'twitter-text/dist/lib/regexSupplant';
import validUrlPrecedingChars from 'twitter-text/dist/regexp/validUrlPrecedingChars';
import validDomain from 'twitter-text/dist/regexp/validDomain';
import validPortNumber from 'twitter-text/dist/regexp/validPortNumber';
import validUrlPath from 'twitter-text/dist/regexp/validUrlPath';
import validUrlQueryChars from 'twitter-text/dist/regexp/validUrlQueryChars';
import validUrlQueryEndingChars from 'twitter-text/dist/regexp/validUrlQueryEndingChars';
// The difference with twitter-text's extractURL is that the protocol isn't
// optional.
export const urlRegex = regexSupplant(
'(' + // $1 URL
'(#{validUrlPrecedingChars})' + // $2
'(https?:\\/\\/)' + // $3 Protocol
'(#{validDomain})' + // $4 Domain(s)
'(?::(#{validPortNumber}))?' + // $5 Port number (optional)
'(\\/#{validUrlPath}*)?' + // $6 URL Path
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String
')',
{
validUrlPrecedingChars,
validDomain,
validPortNumber,
validUrlPath,
validUrlQueryChars,
validUrlQueryEndingChars,
},
'gi',
);

View File

@@ -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);
};