Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client: Port to TypeScript #1457

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import js from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import eslintPluginReact from 'eslint-plugin-react';
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
import tseslint from 'typescript-eslint';

const config = {
languageOptions: {
Expand All @@ -22,7 +23,7 @@ const config = {
},
},

files: ['**/*.js', '**/*.jsx'],
files: ['**/*.ts', '**/*.tsx', '**/*.js'],

rules: {
'no-eval': 'error',
Expand All @@ -38,6 +39,8 @@ const config = {
],

'unicode-bom': 'error',

'@typescript-eslint/no-explicit-any': 0,
},
};

Expand All @@ -50,10 +53,11 @@ const eslintPluginReactHooksConfigsRecommended = {
rules: eslintPluginReactHooks.configs.recommended.rules,
};

export default [
export default tseslint.config(
js.configs.recommended,
eslintPluginReact.configs.flat.recommended,
eslintPluginReactHooksConfigsRecommended,
eslintConfigPrettier,
tseslint.configs.recommended,
config,
];
);
File renamed without changes.
2 changes: 1 addition & 1 deletion client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@
document.getElementById('js-loading-message').textContent = 'selfoss is still loading, please wait.';
</script>

<script src="js/index.js" onerror="document.getElementById('js-loading-message').textContent = 'Error loading selfoss JavaScript file. Please check the browser console or your web server logs.'" type="module"></script>
<script src="js/index.ts" onerror="document.getElementById('js-loading-message').textContent = 'Error loading selfoss JavaScript file. Please check the browser console or your web server logs.'" type="module"></script>
</body>
</html>
9 changes: 0 additions & 9 deletions client/js/Filter.js

This file was deleted.

8 changes: 8 additions & 0 deletions client/js/Filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Object describing how feed items are filtered in the view.
*/
export enum FilterType {
NEWEST = 'newest',
UNREAD = 'unread',
STARRED = 'starred',
}
21 changes: 16 additions & 5 deletions client/js/errors.js → client/js/errors.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
export class OfflineStorageNotAvailableError extends Error {
constructor(message = 'Offline storage is not available') {
public name: string;

constructor(message: string = 'Offline storage is not available') {
super(message);
this.name = 'OfflineStorageNotAvailableError';
}
}

export class TimeoutError extends Error {
constructor(message) {
public name: string;

constructor(message: string) {
super(message);
this.name = 'TimeoutError';
}
}

export class HttpError extends Error {
constructor(message) {
public name: string;
public response: Response;

constructor(message: string) {
super(message);
this.name = 'HttpError';
}
}

export class LoginError extends Error {
constructor(message) {
public name: string;

constructor(message: string) {
super(message);
this.name = 'LoginError';
}
}

export class UnexpectedStateError extends Error {
constructor(message) {
public name: string;

constructor(message: string) {
super(message);
this.name = 'UnexpectedStateError';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export class ValueChangeEvent extends Event {
constructor(value) {
export class ValueChangeEvent<T> extends Event {
public value: T;

constructor(value: T) {
super('change');
this.value = value;
}
Expand All @@ -8,14 +10,16 @@ export class ValueChangeEvent extends Event {
/**
* Object storing a value and allowing subscribing to its changes.
*/
export class ValueListenable extends EventTarget {
constructor(value) {
export class ValueListenable<T> extends EventTarget {
public value: T;

constructor(value: T) {
super();

this.value = value;
}

update(value) {
update(value: T) {
if (this.value !== value) {
this.value = value;

Expand Down
61 changes: 38 additions & 23 deletions client/js/helpers/ajax.js → client/js/helpers/ajax.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,67 @@
import formurlencoded from 'form-urlencoded';
import mergeDeepLeft from 'ramda/src/mergeDeepLeft.js';
import pipe from 'ramda/src/pipe.js';
import mergeDeepLeft from 'ramda/src/mergeDeepLeft';
import pipe from 'ramda/src/pipe';
import { HttpError, TimeoutError } from '../errors';

type Headers = {
[index: string]: string;
};

type FetchOptions = {
body?: string;
method?: 'GET' | 'POST' | 'DELETE';
headers?: Headers;
abortController?: AbortController;
timeout?: number;
failOnHttpErrors?: boolean;
};

/**
* Passing this function as a Promise handler will make the promise fail when the predicate is not true.
*/
export const rejectUnless = (pred) => (response) => {
if (pred(response)) {
return response;
} else {
const err = new HttpError(response.statusText);
err.response = response;
throw err;
}
};
export const rejectUnless =
(pred: (response: Response) => boolean) => (response: Response) => {
if (pred(response)) {
return response;
} else {
const err = new HttpError(response.statusText);
err.response = response;
throw err;
}
};

/**
* fetch API considers a HTTP error a successful state.
* Passing this function as a Promise handler will make the promise fail when HTTP error occurs.
*/
export const rejectIfNotOkay = (response) => {
return rejectUnless((response) => response.ok)(response);
export const rejectIfNotOkay = (response: Response) => {
return rejectUnless((response: Response) => response.ok)(response);
};

/**
* Override fetch options.
*/
export const options =
(newOpts) =>
(newOpts: FetchOptions) =>
(fetch) =>
(url, opts = {}) =>
(url: string, opts: FetchOptions = {}) =>
fetch(url, mergeDeepLeft(opts, newOpts));

/**
* Override just a single fetch option.
*/
export const option = (name, value) => options({ [name]: value });
export const option = (name: string, value) => options({ [name]: value });

/**
* Override just headers in fetch.
*/
export const headers = (value) => option('headers', value);
export const headers = (value: Headers) => option('headers', value);

/**
* Override just a single header in fetch.
*/
export const header = (name, value) => headers({ [name]: value });
export const header = (name: string, value: string) =>
headers({ [name]: value });

/**
* Lift a wrapper function so that it can wrap a function returning more than just a Promise.
Expand Down Expand Up @@ -78,7 +93,7 @@ export const liftToPromiseField =
*/
export const makeAbortableFetch =
(fetch) =>
(url, opts = {}) => {
(url: string, opts: FetchOptions = {}) => {
const controller = opts.abortController || new AbortController();
const promise = fetch(url, {
signal: controller.signal,
Expand All @@ -94,7 +109,7 @@ export const makeAbortableFetch =
*/
export const makeFetchWithTimeout =
(abortableFetch) =>
(url, opts = {}) => {
(url: string, opts: FetchOptions = {}) => {
// offline db consistency requires ajax calls to fail reliably,
// so we enforce a default timeout on ajax calls
const { timeout = 60000, ...rest } = opts;
Expand Down Expand Up @@ -130,7 +145,7 @@ export const makeFetchWithTimeout =
*/
export const makeFetchFailOnHttpErrors =
(fetch) =>
(url, opts = {}) => {
(url: string, opts: FetchOptions = {}) => {
const { failOnHttpErrors = true, ...rest } = opts;
const promise = fetch(url, rest);

Expand All @@ -146,7 +161,7 @@ export const makeFetchFailOnHttpErrors =
*/
export const makeFetchSupportGetBody =
(fetch) =>
(url, opts = {}) => {
(url: string, opts: FetchOptions = {}) => {
const { body, method, ...rest } = opts;

let newUrl = url;
Expand All @@ -162,7 +177,7 @@ export const makeFetchSupportGetBody =
// append the body to the query string
newUrl = `${main}${separator}${body.toString()}#${fragments.join('#')}`;
// remove the body since it has been moved to URL
newOpts = { method, rest };
newOpts = { method, ...rest };
}

return fetch(newUrl, newOpts);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { useListenableValue } from './hooks';
import { useMemo } from 'react';
import selfoss from '../selfoss-base';

export function useLoggedIn() {
export function useLoggedIn(): boolean {
return useListenableValue(selfoss.loggedin);
}

export function useAllowedToRead() {
export function useAllowedToRead(): boolean {
const loggedIn = useLoggedIn();

return useMemo(() => selfoss.isAllowedToRead(), [loggedIn]);
}

export function useAllowedToUpdate() {
export function useAllowedToUpdate(): boolean {
const loggedIn = useLoggedIn();

return useMemo(() => selfoss.isAllowedToUpdate(), [loggedIn]);
}

export function useAllowedToWrite() {
export function useAllowedToWrite(): boolean {
const loggedIn = useLoggedIn();

return useMemo(() => selfoss.isAllowedToWrite(), [loggedIn]);
Expand Down
16 changes: 8 additions & 8 deletions client/js/helpers/color.js → client/js/helpers/color.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
/**
* Get dark OR bright color depending the color contrast.
*
* @param string hexColor color (hex) value
* @param string darkColor dark color value
* @param string brightColor bright color value
* @param hexColor color (hex) value
* @param darkColor dark color value
* @param brightColor bright color value
*
* @return string dark OR bright color value
* @return dark OR bright color value
*
* @see https://24ways.org/2010/calculating-color-contrast/
*/
export function colorByBrightness(
hexColor,
darkColor = '#555',
brightColor = '#EEE',
) {
hexColor: string,
darkColor: string = '#555',
brightColor: string = '#EEE',
): string {
// Strip hash sign.
const color = hexColor.substr(1);
const r = parseInt(color.substr(0, 2), 16);
Expand Down
3 changes: 0 additions & 3 deletions client/js/helpers/configuration.js

This file was deleted.

20 changes: 10 additions & 10 deletions client/js/helpers/hooks.js → client/js/helpers/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router';
import { useMediaMatch } from 'rooks';
import { useLocation } from '../helpers/uri';
import { ValueListenable } from './ValueListenable';

/**
* Changes its return value whenever the value of forceReload field
* in the location state increases.
*/
export function useShouldReload() {
export function useShouldReload(): number {
const location = useLocation();
const forceReload = location?.state?.forceReload;
const [oldForceReload, setOldForceReload] = useState(forceReload);
const [oldForceReload, setOldForceReload] = useState<number | undefined>(
forceReload,
);

if (oldForceReload !== forceReload) {
setOldForceReload(forceReload);
Expand All @@ -30,18 +33,15 @@ export function useShouldReload() {
return reloadCounter;
}

export function useIsSmartphone() {
export function useIsSmartphone(): boolean {
return useMediaMatch('(max-width: 641px)');
}

/**
* @param {ValueListenable}
*/
export function useListenableValue(valueListenable) {
const [value, setValue] = useState(valueListenable.value);
export function useListenableValue<T>(valueListenable: ValueListenable<T>): T {
const [value, setValue] = useState<T>(valueListenable.value);

useEffect(() => {
const listener = (event) => {
const listener = (event: { value: T }) => {
setValue(event.value);
};

Expand Down
Loading
Loading