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: Upgrade to react 19 #1511

Draft
wants to merge 8 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
4 changes: 4 additions & 0 deletions client/js/selfoss-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ const selfoss = {
basePath,
appRef: (app) => {
selfoss.app = app;

return () => {
selfoss.app = null;
};
},
configuration,
}),
Expand Down
13 changes: 9 additions & 4 deletions client/js/templates/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ function PureApp({
const entriesRef = useCallback((entriesPage) => {
setEntriesPage(entriesPage);
selfoss.entriesPage = entriesPage;

return () => {
setEntriesPage(null);
selfoss.entriesPage = null;
};
}, []);

const [title, setTitle] = useState(null);
Expand Down Expand Up @@ -812,8 +817,8 @@ export default class App extends React.Component {

render() {
return (
<ConfigurationContext.Provider value={this.props.configuration}>
<LocalizationContext.Provider value={this._}>
<ConfigurationContext value={this.props.configuration}>
<LocalizationContext value={this._}>
<PureApp
navSourcesExpanded={this.state.navSourcesExpanded}
setNavSourcesExpanded={this.setNavSourcesExpanded}
Expand All @@ -836,8 +841,8 @@ export default class App extends React.Component {
tags={this.state.tags}
reloadAll={this.reloadAll}
/>
</LocalizationContext.Provider>
</ConfigurationContext.Provider>
</LocalizationContext>
</ConfigurationContext>
);
}
}
Expand Down
22 changes: 9 additions & 13 deletions client/js/templates/EntriesPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import React, {
useEffect,
useMemo,
useState,
forwardRef,
} from 'react';
import PropTypes from 'prop-types';
import { Link, useLocation, useParams } from 'react-router';
Expand Down Expand Up @@ -1255,16 +1254,14 @@ StateHolder.propTypes = {
unreadItemsCount: PropTypes.number.isRequired,
};

const StateHolderOuter = forwardRef(function StateHolderOuter(
{
configuration,
setNavExpanded,
navSourcesExpanded,
setGlobalUnreadCount,
unreadItemsCount,
},
export default function StateHolderOuter({
configuration,
setNavExpanded,
navSourcesExpanded,
setGlobalUnreadCount,
unreadItemsCount,
ref,
) {
}) {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
Expand All @@ -1282,14 +1279,13 @@ const StateHolderOuter = forwardRef(function StateHolderOuter(
unreadItemsCount={unreadItemsCount}
/>
);
});
}

StateHolderOuter.propTypes = {
ref: PropTypes.func.isRequired,
configuration: PropTypes.object.isRequired,
setNavExpanded: PropTypes.func.isRequired,
navSourcesExpanded: PropTypes.bool.isRequired,
setGlobalUnreadCount: PropTypes.func.isRequired,
unreadItemsCount: PropTypes.number.isRequired,
};

export default StateHolderOuter;
103 changes: 50 additions & 53 deletions client/js/templates/HashPassword.jsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,48 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useState } from 'react';
import React, {
startTransition,
useActionState,
useCallback,
useEffect,
} from 'react';
import { useNavigate } from 'react-router';
import { useInput } from 'rooks';
import { LoadingState } from '../requests/LoadingState';
import { HttpError } from '../errors';
import { hashPassword } from '../requests/common';

export default function HashPassword({ setTitle }) {
const [state, setState] = useState(LoadingState.INITIAL);
const [hashedPassword, setHashedPassword] = useState('');
const [error, setError] = useState(null);
const passwordEntry = useInput('');

const navigate = useNavigate();

const [
/** @type {({} | { hashedPassword: string } | { error: Error })} */
state,
submitAction,
isPending,
] = useActionState(async (_previousState, formData) => {
try {
const password = formData.get('password').trim();
const hashedPassword = await hashPassword(password);
return { hashedPassword };
} catch (error) {
if (error instanceof HttpError && error.response.status === 403) {
navigate('/sign/in', {
error: 'Generating a new password hash requires being logged in or not setting “password” in selfoss configuration.',
returnLocation: '/password',
});
return {};
}
return { error };
}
}, {});

const submit = useCallback(
(event) => {
// Unlike `action` prop, `onSubmit` avoids clearing the form on submit.
// https://github.com/facebook/react/issues/29034#issuecomment-2143595195
event.preventDefault();

setState(LoadingState.LOADING);
hashPassword(passwordEntry.value.trim())
.then((hashedPassword) => {
setHashedPassword(hashedPassword);
setState(LoadingState.SUCCESS);
})
.catch((error) => {
if (
error instanceof HttpError &&
error.response.status === 403
) {
navigate('/sign/in', {
error: 'Generating a new password hash requires being logged in or not setting “password” in selfoss configuration.',
returnLocation: '/password',
});
return;
}
setError(error);
setState(LoadingState.ERROR);
});
const formData = new FormData(event.target);
startTransition(() => submitAction(formData));
},
[navigate, passwordEntry.value],
[submitAction],
);

useEffect(() => {
Expand All @@ -50,22 +53,21 @@ export default function HashPassword({ setTitle }) {
};
}, [setTitle]);

const message =
state === LoadingState.SUCCESS ? (
<p className="error">
<label>
Generated Password (insert this into config.ini):
<input type="text" value={hashedPassword} readOnly />
</label>
</p>
) : state === LoadingState.ERROR ? (
<p className="error">
Unexpected happened.
<details>
<pre>${JSON.stringify(error)}</pre>
</details>
</p>
) : null;
const message = isPending ? null : 'hashedPassword' in state ? (
<p className="error">
<label>
Generated Password (insert this into config.ini):
<input type="text" value={state.hashedPassword} readOnly />
</label>
</p>
) : 'error' in state ? (
<p className="error">
Unexpected happened.
<details>
<pre>${JSON.stringify(state.error)}</pre>
</details>
</p>
) : null;

return (
<form action="" method="post" onSubmit={submit}>
Expand All @@ -80,7 +82,6 @@ export default function HashPassword({ setTitle }) {
name="password"
autoComplete="new-password"
accessKey="p"
{...passwordEntry}
/>
</li>
<li className="message-container" aria-live="assertive">
Expand All @@ -91,13 +92,9 @@ export default function HashPassword({ setTitle }) {
<input
className="button"
type="submit"
value={
state === LoadingState.LOADING
? 'Hashing password…'
: 'Compute hash'
}
value={isPending ? 'Hashing password…' : 'Compute hash'}
accessKey="g"
disabled={state === LoadingState.LOADING}
disabled={isPending}
/>
</li>
</ul>
Expand Down
Loading
Loading