Merge branch 'next-ts-conversions' into 'next'
Next: TypeScript conversions See merge request soapbox-pub/soapbox-fe!1254
This commit is contained in:
commit
52e21651a1
35 changed files with 207 additions and 10 deletions
Binary file not shown.
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
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 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 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 features = getFeatures(instance);
|
||||||
|
|
||||||
const makeMenu = (): Menu => {
|
const makeMenu = (): Menu => {
|
||||||
|
|
Binary file not shown.
|
@ -37,7 +37,9 @@ const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const maxPixels = 400 * 400;
|
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) => {
|
resizeImage(rawFile, maxPixels).then((file) => {
|
||||||
const url = file ? URL.createObjectURL(file) : account?.avatar as string;
|
const url = file ? URL.createObjectURL(file) : account?.avatar as string;
|
||||||
|
|
|
@ -38,7 +38,9 @@ const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const maxPixels = 1920 * 1080;
|
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) => {
|
resizeImage(rawFile, maxPixels).then((file) => {
|
||||||
const url = file ? URL.createObjectURL(file) : account?.header as string;
|
const url = file ? URL.createObjectURL(file) : account?.header as string;
|
||||||
|
|
Binary file not shown.
Binary file not shown.
34
app/soapbox/is_mobile.ts
Normal file
34
app/soapbox/is_mobile.ts
Normal 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.
29
app/soapbox/middleware/errors.ts
Normal file
29
app/soapbox/middleware/errors.ts
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -17,6 +17,8 @@ export const store = createStore(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type Store = typeof store;
|
||||||
|
|
||||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||||
// https://redux.js.org/usage/usage-with-typescript
|
// https://redux.js.org/usage/usage-with-typescript
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
|
|
@ -16,10 +16,9 @@ export const getDomain = (account: Account): string => {
|
||||||
return domain ? domain : getDomainFromURL(account);
|
return domain ? domain : getDomainFromURL(account);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBaseURL = (account: ImmutableMap<string, any>): string => {
|
export const getBaseURL = (account: Account): string => {
|
||||||
try {
|
try {
|
||||||
const url = account.get('url');
|
return new URL(account.url).origin;
|
||||||
return new URL(url).origin;
|
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19
app/soapbox/utils/numbers.tsx
Normal file
19
app/soapbox/utils/numbers.tsx
Normal 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.
38
app/soapbox/utils/quirks.ts
Normal file
38
app/soapbox/utils/quirks.ts
Normal 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.
Binary file not shown.
Binary file not shown.
44
app/soapbox/utils/state.ts
Normal file
44
app/soapbox/utils/state.ts
Normal 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.
13
app/soapbox/utils/static.ts
Normal file
13
app/soapbox/utils/static.ts
Normal 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);
|
||||||
|
};
|
|
@ -2,7 +2,8 @@ import { isIntegerId } from 'soapbox/utils/numbers';
|
||||||
|
|
||||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
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 {
|
try {
|
||||||
// Pulled from Pleroma's media parser
|
// Pulled from Pleroma's media parser
|
||||||
const selector = 'a:not(.mention,.hashtag,.attachment,[rel~="tag"])';
|
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));
|
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
|
// 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));
|
return status.media_attachments.some(({ id }) => isIntegerId(id));
|
||||||
};
|
};
|
||||||
|
|
Binary file not shown.
|
@ -74,6 +74,8 @@
|
||||||
"@types/http-link-header": "^1.0.3",
|
"@types/http-link-header": "^1.0.3",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/lodash": "^4.14.180",
|
"@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/qrcode.react": "^1.0.2",
|
||||||
"@types/react-datepicker": "^4.4.0",
|
"@types/react-datepicker": "^4.4.0",
|
||||||
"@types/react-helmet": "^6.1.5",
|
"@types/react-helmet": "^6.1.5",
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -2144,6 +2144,16 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
|
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
|
||||||
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
|
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":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
|
|
Loading…
Reference in a new issue