Convert a bunch of files to TypeScript

merge-requests/1254/head
Alex Gleason 2022-04-24 14:28:07 -05:00
rodzic 3b55a5a9c7
commit 7038d6a844
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
32 zmienionych plików z 312 dodań i 186 usunięć

Wyświetl plik

@ -3,9 +3,12 @@
import 'intl';
import 'intl/locale-data/jsonp/en';
import 'es6-symbol/implement';
// @ts-ignore: No types
import includes from 'array-includes';
// @ts-ignore: No types
import isNaN from 'is-nan';
import assign from 'object-assign';
// @ts-ignore: No types
import values from 'object.values';
import { decode as decodeBase64 } from './utils/base64';
@ -30,7 +33,7 @@ if (!HTMLCanvasElement.prototype.toBlob) {
const BASE64_MARKER = ';base64,';
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value(callback, type = 'image/png', quality) {
value(callback: any, type = 'image/png', quality: any) {
const dataURL = this.toDataURL(type, quality);
let data;

Wyświetl plik

@ -22,7 +22,7 @@ const SidebarNavigation = () => {
const followRequestsCount = useAppSelector((state) => state.user_lists.getIn(['follow_requests', 'items'], ImmutableOrderedSet()).count());
// const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
const baseURL = account ? getBaseURL(ImmutableMap(account)) : '';
const baseURL = account ? getBaseURL(account) : '';
const features = getFeatures(instance);
const makeMenu = (): Menu => {

Wyświetl plik

@ -2,6 +2,6 @@
import 'intersection-observer';
import 'requestidlecallback';
import objectFitImages from 'object-fit-images';
import objectFitImages from 'object-fit-images';
objectFitImages();

Wyświetl plik

@ -37,7 +37,9 @@ const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const maxPixels = 400 * 400;
const [rawFile] = event.target.files || [] as any;
const rawFile = event.target.files?.item(0);
if (!rawFile) return;
resizeImage(rawFile, maxPixels).then((file) => {
const url = file ? URL.createObjectURL(file) : account?.avatar as string;

Wyświetl plik

@ -38,7 +38,9 @@ const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const maxPixels = 1920 * 1080;
const [rawFile] = event.target.files || [] as any;
const rawFile = event.target.files?.item(0);
if (!rawFile) return;
resizeImage(rawFile, maxPixels).then((file) => {
const url = file ? URL.createObjectURL(file) : account?.header as string;

Wyświetl plik

@ -4,17 +4,20 @@
*/
import { changeSettingImmediate } from 'soapbox/actions/settings';
export const createGlobals = store => {
import type { Store } from 'soapbox/store';
/** Add Soapbox globals to the window. */
export const createGlobals = (store: Store) => {
const Soapbox = {
// Become a developer with `Soapbox.isDeveloper()`
isDeveloper: (bool = true) => {
/** Become a developer with `Soapbox.isDeveloper()` */
isDeveloper: (bool = true): boolean => {
if (![true, false].includes(bool)) {
throw `Invalid option ${bool}. Must be true or false.`;
}
store.dispatch(changeSettingImmediate(['isDeveloper'], bool));
store.dispatch(changeSettingImmediate(['isDeveloper'], bool) as any);
return bool;
},
};
window.Soapbox = Soapbox;
(window as any).Soapbox = Soapbox;
};

Wyświetl plik

@ -1,29 +0,0 @@
'use strict';
import { supportsPassiveEvents } from 'detect-passive-events';
const LAYOUT_BREAKPOINT = 630;
export function isMobile(width) {
return width <= LAYOUT_BREAKPOINT;
}
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
let userTouching = false;
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
function touchListener() {
userTouching = true;
window.removeEventListener('touchstart', touchListener, listenerOptions);
}
window.addEventListener('touchstart', touchListener, listenerOptions);
export function isUserTouching() {
return userTouching;
}
export function isIOS() {
return iOS;
}

Wyświetl plik

@ -0,0 +1,34 @@
'use strict';
import { supportsPassiveEvents } from 'detect-passive-events';
/** Breakpoint at which the application is considered "mobile". */
const LAYOUT_BREAKPOINT = 630;
/** Check if the width is small enough to be considered "mobile". */
export function isMobile(width: number) {
return width <= LAYOUT_BREAKPOINT;
}
/** Whether the device is iOS (best guess). */
const iOS: boolean = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
let userTouching = false;
const listenerOptions = supportsPassiveEvents ? { passive: true } as EventListenerOptions : false;
function touchListener(): void {
userTouching = true;
window.removeEventListener('touchstart', touchListener, listenerOptions);
}
window.addEventListener('touchstart', touchListener, listenerOptions);
/** Whether the user has touched the screen since the page loaded. */
export function isUserTouching(): boolean {
return userTouching;
}
/** Whether the device is iOS (best guess). */
export function isIOS(): boolean {
return iOS;
}

Wyświetl plik

@ -3,9 +3,14 @@
* @module soapbox/precheck
*/
/** Whether a page title was inserted with SSR. */
const hasTitle = Boolean(document.querySelector('title'));
/** Whether pre-rendered data exists in Mastodon's format. */
const hasPrerenderPleroma = Boolean(document.getElementById('initial-results'));
/** Whether pre-rendered data exists in Pleroma's format. */
const hasPrerenderMastodon = Boolean(document.getElementById('initial-state'));
/** Whether initial data was loaded into the page by server-side-rendering (SSR). */
export const isPrerendered = hasTitle || hasPrerenderPleroma || hasPrerenderMastodon;

Wyświetl plik

@ -4,11 +4,13 @@ import { Record as ImmutableRecord } from 'immutable';
import { INSTANCE_FETCH_FAIL } from 'soapbox/actions/instance';
import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
instance_fetch_failed: false,
});
export default function meta(state = ReducerRecord(), action) {
export default function meta(state = ReducerRecord(), action: AnyAction) {
switch(action.type) {
case INSTANCE_FETCH_FAIL:
return state.set('instance_fetch_failed', true);

Wyświetl plik

@ -14,7 +14,8 @@
const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg;
export function isRtl(text) {
/** Check if text is right-to-left (eg Arabic). */
export function isRtl(text: string): boolean {
if (text.length === 0) {
return false;
}

Wyświetl plik

@ -2,15 +2,17 @@
export default class Settings {
constructor(keyBase = null) {
keyBase: string | null = null;
constructor(keyBase: string | null = null) {
this.keyBase = keyBase;
}
generateKey(id) {
generateKey(id: string) {
return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id;
}
set(id, data) {
set(id: string, data: any) {
const key = this.generateKey(id);
try {
const encodedData = JSON.stringify(data);
@ -21,17 +23,17 @@ export default class Settings {
}
}
get(id) {
get(id: string) {
const key = this.generateKey(id);
try {
const rawData = localStorage.getItem(key);
return JSON.parse(rawData);
return rawData ? JSON.parse(rawData) : null;
} catch (e) {
return null;
}
}
remove(id) {
remove(id: string) {
const data = this.get(id);
if (data) {
const key = this.generateKey(id);
@ -46,5 +48,8 @@ export default class Settings {
}
/** Remember push notification settings. */
export const pushNotificationsSetting = new Settings('soapbox_push_notification_data');
/** Remember hashtag usage. */
export const tagHistory = new Settings('soapbox_tag_history');

Wyświetl plik

@ -17,6 +17,8 @@ export const store = createStore(
),
);
export type Store = typeof store;
// Infer the `RootState` and `AppDispatch` types from the store itself
// https://redux.js.org/usage/usage-with-typescript
export type RootState = ReturnType<typeof store.getState>;

Wyświetl plik

@ -16,10 +16,9 @@ export const getDomain = (account: Account): string => {
return domain ? domain : getDomainFromURL(account);
};
export const getBaseURL = (account: ImmutableMap<string, any>): string => {
export const getBaseURL = (account: Account): string => {
try {
const url = account.get('url');
return new URL(url).origin;
return new URL(account.url).origin;
} catch {
return '';
}

Wyświetl plik

@ -1,14 +1,23 @@
// Adapted from Pleroma FE
// https://git.pleroma.social/pleroma/pleroma-fe/-/blob/ef5bbc4e5f84bb9e8da76a0440eea5d656d36977/src/services/favicon_service/favicon_service.js
type Favicon = {
favcanvas: HTMLCanvasElement,
favimg: HTMLImageElement,
favcontext: CanvasRenderingContext2D | null,
favicon: HTMLLinkElement,
};
/** Service to draw and update a notifications dot on the favicon */
const createFaviconService = () => {
const favicons = [];
const favicons: Favicon[] = [];
const faviconWidth = 128;
const faviconHeight = 128;
const badgeRadius = 24;
const initFaviconService = () => {
const nodes = document.querySelectorAll('link[rel="icon"]');
/** Start the favicon service */
const initFaviconService = (): void => {
const nodes: NodeListOf<HTMLLinkElement> = document.querySelectorAll('link[rel="icon"]');
nodes.forEach(favicon => {
if (favicon) {
const favcanvas = document.createElement('canvas');
@ -23,9 +32,11 @@ const createFaviconService = () => {
});
};
const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0;
/** Check if the image is loaded */
const isImageLoaded = (img: HTMLImageElement): boolean => img.complete && img.naturalHeight !== 0;
const clearFaviconBadge = () => {
/** Reset the favicon image to its initial state */
const clearFaviconBadge = (): void => {
if (favicons.length === 0) return;
favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {
if (!favimg || !favcontext || !favicon) return;
@ -37,7 +48,8 @@ const createFaviconService = () => {
});
};
const drawFaviconBadge = () => {
/** Replace the favicon image with one that has a notification dot */
const drawFaviconBadge = (): void => {
if (favicons.length === 0) return;
clearFaviconBadge();
favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {

Wyświetl plik

@ -1,6 +1,6 @@
import { processHtml } from './tiny_post_html_processor';
export const addGreentext = html => {
export const addGreentext = (html: string): string => {
// Copied from Pleroma FE
// https://git.pleroma.social/pleroma/pleroma-fe/-/blob/19475ba356c3fd6c54ca0306d3ae392358c212d1/src/components/status_content/status_content.js#L132
return processHtml(html, (string) => {

Wyświetl plik

@ -1,16 +1,20 @@
/** Convert HTML to a plaintext representation, preserving whitespace. */
// NB: This function can still return unsafe HTML
export const unescapeHTML = (html) => {
export const unescapeHTML = (html: string): string => {
const wrapper = document.createElement('div');
wrapper.innerHTML = html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, '');
return wrapper.textContent;
return wrapper.textContent || '';
};
export const stripCompatibilityFeatures = html => {
/** Remove compatibility markup for features Soapbox supports. */
export const stripCompatibilityFeatures = (html: string): string => {
const node = document.createElement('div');
node.innerHTML = html;
const selectors = [
// Quote posting
'.quote-inline',
// Explicit mentions
'.recipients-inline',
];

Wyświetl plik

@ -1,16 +0,0 @@
import React from 'react';
import { FormattedNumber } from 'react-intl';
export const isNumber = number => typeof number === 'number' && !isNaN(number);
export const shortNumberFormat = number => {
if (!isNumber(number)) return '•';
if (number < 1000) {
return <FormattedNumber value={number} />;
} else {
return <span><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</span>;
}
};
export const isIntegerId = id => new RegExp(/^-?[0-9]+$/g).test(id);

Wyświetl plik

@ -0,0 +1,19 @@
import React from 'react';
import { FormattedNumber } from 'react-intl';
/** Check if a value is REALLY a number. */
export const isNumber = (number: unknown): boolean => typeof number === 'number' && !isNaN(number);
/** Display a number nicely for the UI, eg 1000 becomes 1K. */
export const shortNumberFormat = (number: any): React.ReactNode => {
if (!isNumber(number)) return '•';
if (number < 1000) {
return <FormattedNumber value={number} />;
} else {
return <span><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</span>;
}
};
/** Check if an entity ID is an integer (eg not a FlakeId). */
export const isIntegerId = (id: string): boolean => new RegExp(/^-?[0-9]+$/g).test(id);

Wyświetl plik

@ -1,17 +0,0 @@
import { createSelector } from 'reselect';
import { parseVersion, PLEROMA, MITRA } from './features';
// For solving bugs between API implementations
export const getQuirks = createSelector([
instance => parseVersion(instance.get('version')),
], (v) => {
return {
invertedPagination: v.software === PLEROMA,
noApps: v.software === MITRA,
noOAuthForm: v.software === MITRA,
};
});
export const getNextLinkName = getState =>
getQuirks(getState().get('instance')).invertedPagination ? 'prev' : 'next';

Wyświetl plik

@ -0,0 +1,38 @@
/* eslint sort-keys: "error" */
import { createSelector } from 'reselect';
import { parseVersion, PLEROMA, MITRA } from './features';
import type { RootState } from 'soapbox/store';
import type { Instance } from 'soapbox/types/entities';
/** For solving bugs between API implementations. */
export const getQuirks = createSelector([
(instance: Instance) => parseVersion(instance.version),
], (v) => {
return {
/**
* The `next` and `prev` Link headers are backwards for blocks and mutes.
* @see GET /api/v1/blocks
* @see GET /api/v1/mutes
*/
invertedPagination: v.software === PLEROMA,
/**
* Apps are not supported by the API, and should not be created during login or registration.
* @see POST /api/v1/apps
* @see POST /oauth/token
*/
noApps: v.software === MITRA,
/**
* There is no OAuth form available for login.
* @see GET /oauth/authorize
*/
noOAuthForm: v.software === MITRA,
};
});
/** Shortcut for inverted pagination quirk. */
export const getNextLinkName = (getState: () => RootState) =>
getQuirks(getState().instance).invertedPagination ? 'prev' : 'next';

Wyświetl plik

@ -1,14 +1,19 @@
/* eslint-disable no-case-declarations */
const DEFAULT_MAX_PIXELS = 1920 * 1080;
const _browser_quirks = {};
interface BrowserCanvasQuirks {
'image-orientation-automatic'?: boolean,
'canvas-read-unreliable'?: boolean,
}
const _browser_quirks: BrowserCanvasQuirks = {};
// 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 => {
const dropOrientationIfNeeded = (orientation: number) => new Promise<number>(resolve => {
switch (_browser_quirks['image-orientation-automatic']) {
case true:
resolve(1);
@ -40,10 +45,12 @@ const dropOrientationIfNeeded = (orientation) => new Promise(resolve => {
}
});
// 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) => {
// /**
// *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<void>((resolve, reject) => {
// switch(_browser_quirks['canvas-read-unreliable']) {
// case true:
// reject('Canvas reading unreliable');
@ -61,9 +68,9 @@ const dropOrientationIfNeeded = (orientation) => new Promise(resolve => {
// 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)) {
// 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 {
@ -79,7 +86,9 @@ const dropOrientationIfNeeded = (orientation) => new Promise(resolve => {
// }
// });
const getImageUrl = inputFile => new Promise((resolve, reject) => {
/** Convert the file into a local blob URL. */
const getImageUrl = (inputFile: File) => new Promise<string>((resolve, reject) => {
// @ts-ignore: This is a browser capabilities check.
if (window.URL?.createObjectURL) {
try {
resolve(URL.createObjectURL(inputFile));
@ -91,29 +100,32 @@ const getImageUrl = inputFile => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = (...args) => reject(...args);
reader.onload = ({ target }) => resolve(target.result);
reader.onload = ({ target }) => resolve((target?.result || '') as string);
reader.readAsDataURL(inputFile);
});
const loadImage = inputFile => new Promise((resolve, reject) => {
/** Get an image element from a file. */
const loadImage = (inputFile: File) => new Promise<HTMLImageElement>((resolve, reject) => {
getImageUrl(inputFile).then(url => {
const img = new Image();
img.onerror = (...args) => reject(...args);
img.onerror = (...args) => reject([...args]);
img.onload = () => resolve(img);
img.src = url;
}).catch(reject);
});
const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
/** Get the exif orientation for the image. */
const getOrientation = (img: HTMLImageElement, type = 'image/png') => new Promise<number>(resolve => {
if (!['image/jpeg', 'image/webp'].includes(type)) {
resolve(1);
return;
}
import(/* webpackChunkName: "features/compose" */'exif-js').then(({ default: EXIF }) => {
// @ts-ignore: The TypeScript definition is wrong.
EXIF.getData(img, () => {
const orientation = EXIF.getTag(img, 'Orientation');
if (orientation !== 1) {
@ -125,7 +137,22 @@ const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
}).catch(() => {});
});
const processImage = (img, { width, height, orientation, type = 'image/png', name = 'resized.png' }) => new Promise(resolve => {
const processImage = (
img: HTMLImageElement,
{
width,
height,
orientation,
type = 'image/png',
name = 'resized.png',
} : {
width: number,
height: number,
orientation: number,
type?: string,
name?: string,
},
) => new Promise<File>((resolve, reject) => {
const canvas = document.createElement('canvas');
if (4 < orientation && orientation < 9) {
@ -138,6 +165,11 @@ const processImage = (img, { width, height, orientation, type = 'image/png', nam
const context = canvas.getContext('2d');
if (!context) {
reject(context);
return;
}
switch (orientation) {
case 2: context.transform(-1, 0, 0, 1, width, 0); break;
case 3: context.transform(-1, 0, 0, -1, width, height); break;
@ -151,11 +183,19 @@ const processImage = (img, { width, height, orientation, type = 'image/png', nam
context.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => {
if (!blob) {
reject(blob);
return;
}
resolve(new File([blob], name, { type, lastModified: new Date().getTime() }));
}, type);
});
const resizeImage = (img, inputFile, maxPixels) => new Promise((resolve, reject) => {
const resizeImage = (
img: HTMLImageElement,
inputFile: File,
maxPixels: number,
) => new Promise<File>((resolve, reject) => {
const { width, height } = img;
const type = inputFile.type || 'image/png';
@ -177,7 +217,8 @@ const resizeImage = (img, inputFile, maxPixels) => new Promise((resolve, reject)
.catch(reject);
});
export default (inputFile, maxPixels = DEFAULT_MAX_PIXELS) => new Promise((resolve) => {
/** Resize an image to the maximum number of pixels. */
export default (inputFile: File, maxPixels = DEFAULT_MAX_PIXELS) => new Promise<File>((resolve) => {
if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') {
resolve(inputFile);
return;

Wyświetl plik

@ -1,15 +1,15 @@
// Returns `true` if the node contains only emojis, up to a limit
export const onlyEmoji = (node, limit = 1, ignoreMentions = true) => {
/** Returns `true` if the node contains only emojis, up to a limit */
export const onlyEmoji = (node: HTMLElement, limit = 1, ignoreMentions = true): boolean => {
if (!node) return false;
try {
// Remove mentions before checking content
if (ignoreMentions) {
node = node.cloneNode(true);
node.querySelectorAll('a.mention').forEach(m => m.parentNode.removeChild(m));
node = node.cloneNode(true) as HTMLElement;
node.querySelectorAll('a.mention').forEach(m => m.parentNode?.removeChild(m));
}
if (node.textContent.replace(new RegExp(' ', 'g'), '') !== '') return false;
if (node.textContent?.replace(new RegExp(' ', 'g'), '') !== '') return false;
const emojis = Array.from(node.querySelectorAll('img.emojione'));
if (emojis.length === 0) return false;
if (emojis.length > limit) return false;

Wyświetl plik

@ -1,42 +0,0 @@
/**
* State: general Redux state utility functions.
* @module soapbox/utils/state
*/
import { getSoapboxConfig } from'soapbox/actions/soapbox';
import { BACKEND_URL } from 'soapbox/build_config';
import { isPrerendered } from 'soapbox/precheck';
import { getBaseURL as getAccountBaseURL } from 'soapbox/utils/accounts';
import { isURL } from 'soapbox/utils/auth';
export const displayFqn = state => {
const soapbox = getSoapboxConfig(state);
return soapbox.get('displayFqn');
};
export const federationRestrictionsDisclosed = state => {
return state.hasIn(['instance', 'pleroma', 'metadata', 'federation', 'mrf_policies']);
};
/**
* Determine whether Soapbox FE is running in standalone mode.
* Standalone mode runs separately from any backend and can login anywhere.
* @param {object} state
* @returns {boolean}
*/
export const isStandalone = state => {
const instanceFetchFailed = state.getIn(['meta', 'instance_fetch_failed'], false);
return isURL(BACKEND_URL) ? false : (!isPrerendered && instanceFetchFailed);
};
/**
* Get the baseURL of the instance.
* @param {object} state
* @returns {string} url
*/
export const getBaseURL = state => {
const me = state.get('me');
const account = state.getIn(['accounts', me]);
return isURL(BACKEND_URL) ? BACKEND_URL : getAccountBaseURL(account);
};

Wyświetl plik

@ -0,0 +1,44 @@
/**
* State: general Redux state utility functions.
* @module soapbox/utils/state
*/
import { getSoapboxConfig } from'soapbox/actions/soapbox';
import * as BuildConfig from 'soapbox/build_config';
import { isPrerendered } from 'soapbox/precheck';
import { isURL } from 'soapbox/utils/auth';
import type { RootState } from 'soapbox/store';
/** Whether to display the fqn instead of the acct. */
export const displayFqn = (state: RootState): boolean => {
return getSoapboxConfig(state).displayFqn;
};
/** Whether the instance exposes instance blocks through the API. */
export const federationRestrictionsDisclosed = (state: RootState): boolean => {
return state.instance.pleroma.hasIn(['metadata', 'federation', 'mrf_policies']);
};
/**
* Determine whether Soapbox FE is running in standalone mode.
* Standalone mode runs separately from any backend and can login anywhere.
*/
export const isStandalone = (state: RootState): boolean => {
const instanceFetchFailed = state.meta.instance_fetch_failed;
return isURL(BuildConfig.BACKEND_URL) ? false : (!isPrerendered && instanceFetchFailed);
};
const getHost = (url: any): string => {
try {
return new URL(url).origin;
} catch {
return '';
}
};
/** Get the baseURL of the instance. */
export const getBaseURL = (state: RootState): string => {
const account = state.accounts.get(state.me);
return isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : getHost(account?.url);
};

Wyświetl plik

@ -1,12 +0,0 @@
/**
* Static: functions related to static files.
* @module soapbox/utils/static
*/
import { join } from 'path';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
export const joinPublicPath = (...paths) => {
return join(FE_SUBDIRECTORY, ...paths);
};

Wyświetl plik

@ -0,0 +1,13 @@
/**
* Static: functions related to static files.
* @module soapbox/utils/static
*/
import { join } from 'path';
import * as BuildConfig from 'soapbox/build_config';
/** Gets the path to a file with build configuration being considered. */
export const joinPublicPath = (...paths: string[]): string => {
return join(BuildConfig.FE_SUBDIRECTORY, ...paths);
};

Wyświetl plik

@ -2,7 +2,8 @@ import { isIntegerId } from 'soapbox/utils/numbers';
import type { Status as StatusEntity } from 'soapbox/types/entities';
export const getFirstExternalLink = (status: StatusEntity) => {
/** Grab the first external link from a status. */
export const getFirstExternalLink = (status: StatusEntity): HTMLAnchorElement | null => {
try {
// Pulled from Pleroma's media parser
const selector = 'a:not(.mention,.hashtag,.attachment,[rel~="tag"])';
@ -14,11 +15,13 @@ export const getFirstExternalLink = (status: StatusEntity) => {
}
};
export const shouldHaveCard = (status: StatusEntity) => {
/** Whether the status is expected to have a Card after it loads. */
export const shouldHaveCard = (status: StatusEntity): boolean => {
return Boolean(getFirstExternalLink(status));
};
/** Whether the media IDs on this status have integer IDs (opposed to FlakeIds). */
// https://gitlab.com/soapbox-pub/soapbox-fe/-/merge_requests/1087
export const hasIntegerMediaIds = (status: StatusEntity) => {
export const hasIntegerMediaIds = (status: StatusEntity): boolean => {
return status.media_attachments.some(({ id }) => isIntegerId(id));
};

Wyświetl plik

@ -1,32 +1,30 @@
// Copied from Pleroma FE
// https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
type Processor = (html: string) => string;
/**
* This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and
* allows it to be processed, useful for greentexting, mostly
* allows it to be processed, useful for greentexting, mostly.
*
* known issue: doesn't handle CDATA so nested CDATA might not work well
*
* @param {Object} input - input data
* @param {(string) => string} processor - function that will be called on every line
* @return {string} processed html
* known issue: doesn't handle CDATA so nested CDATA might not work well.
*/
export const processHtml = (html, processor) => {
export const processHtml = (html: string, processor: Processor): string => {
const handledTags = new Set(['p', 'br', 'div']);
const openCloseTags = new Set(['p', 'div']);
let buffer = ''; // Current output buffer
const level = []; // How deep we are in tags and which tags were there
const level: string[] = []; // How deep we are in tags and which tags were there
let textBuffer = ''; // Current line content
let tagBuffer = null; // Current tag buffer, if null = we are not currently reading a tag
// Extracts tag name from tag, i.e. <span a="b"> => span
const getTagName = (tag) => {
const getTagName = (tag: string): string | null => {
const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag);
return result && (result[1] || result[2]);
};
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
const flush = (): void => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer.trim().length > 0) {
buffer += processor(textBuffer);
} else {
@ -35,18 +33,18 @@ export const processHtml = (html, processor) => {
textBuffer = '';
};
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
const handleBr = (tag: string): void => { // handles single newlines/linebreaks/selfclosing
flush();
buffer += tag;
};
const handleOpen = (tag) => { // handles opening tags
const handleOpen = (tag: string): void => { // handles opening tags
flush();
buffer += tag;
level.push(tag);
};
const handleClose = (tag) => { // handles closing tags
const handleClose = (tag: string): void => { // handles closing tags
flush();
buffer += tag;
if (level[level.length - 1] === tag) {
@ -65,7 +63,7 @@ export const processHtml = (html, processor) => {
const tagFull = tagBuffer;
tagBuffer = null;
const tagName = getTagName(tagFull);
if (handledTags.has(tagName)) {
if (tagName && handledTags.has(tagName)) {
if (tagName === 'br') {
handleBr(tagFull);
} else if (openCloseTags.has(tagName)) {

Wyświetl plik

@ -74,6 +74,8 @@
"@types/http-link-header": "^1.0.3",
"@types/jest": "^27.4.1",
"@types/lodash": "^4.14.180",
"@types/object-assign": "^4.0.30",
"@types/object-fit-images": "^3.2.3",
"@types/qrcode.react": "^1.0.2",
"@types/react-datepicker": "^4.4.0",
"@types/react-helmet": "^6.1.5",

Wyświetl plik

@ -2144,6 +2144,16 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
"@types/object-assign@^4.0.30":
version "4.0.30"
resolved "https://registry.yarnpkg.com/@types/object-assign/-/object-assign-4.0.30.tgz#8949371d5a99f4381ee0f1df0a9b7a187e07e652"
integrity sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI=
"@types/object-fit-images@^3.2.3":
version "3.2.3"
resolved "https://registry.yarnpkg.com/@types/object-fit-images/-/object-fit-images-3.2.3.tgz#aa17a1cb4ac113ba81ce62f901177c9ccd5194f5"
integrity sha512-kpBPy4HIzbM1o3v+DJrK4V5NgUpcUg/ayzjixOVHQNukpdEUYDIaeDrnYJUSemQXWX5mKeEnxDRU1nACAWYnvg==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"