Merge branch 'next-ts-conversions' into 'next'

Next: TypeScript conversions

See merge request soapbox-pub/soapbox-fe!1254
This commit is contained in:
Alex Gleason 2022-04-24 22:21:55 +00:00
commit 52e21651a1
35 changed files with 207 additions and 10 deletions

View file

@ -1,4 +1,4 @@
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React from 'react';
import { FormattedMessage } from 'react-intl';
@ -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 => {

View file

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

View file

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

Binary file not shown.

34
app/soapbox/is_mobile.ts Normal file
View file

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

Binary file not shown.

View file

@ -0,0 +1,29 @@
import { showAlertForError } from '../actions/alerts';
import type { AnyAction } from 'redux';
import type { ThunkMiddleware } from 'redux-thunk';
/** Whether the action is considered a failure. */
const isFailType = (type: string): boolean => type.endsWith('_FAIL');
/** Whether the action is a failure to fetch from browser storage. */
const isRememberFailType = (type: string): boolean => type.endsWith('_REMEMBER_FAIL');
/** Whether the error contains an Axios response. */
const hasResponse = (error: any): boolean => Boolean(error && error.response);
/** Whether the error should be shown to the user. */
const shouldShowError = ({ type, skipAlert, error }: AnyAction): boolean => {
return !skipAlert && hasResponse(error) && isFailType(type) && !isRememberFailType(type);
};
/** Middleware to display Redux errors to the user. */
export default function errorsMiddleware(): ThunkMiddleware {
return ({ dispatch }) => next => action => {
if (shouldShowError(action)) {
dispatch(showAlertForError(action.error));
}
return next(action);
};
}

View file

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

View file

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

Binary file not shown.

View file

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

Binary file not shown.

View file

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

Binary file not shown.

View file

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

Binary file not shown.

View file

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

View file

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

View file

@ -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",

View file

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