Add some explanatory comments

This commit is contained in:
Ginger 2025-09-24 10:44:17 -04:00
parent 458b1c0172
commit f9b0d8c86f
No known key found for this signature in database
3 changed files with 56 additions and 5 deletions

View file

@ -9,12 +9,23 @@ import React, {
import { deepCompare } from 'matrix-js-sdk/lib/utils';
import { ExtendedProfile } from '../../../../hooks/useExtendedProfile';
/// These types ensure the element functions are actually able to manipulate
/// the profile fields they're mapped to. The <C> generic parameter represents
/// extra "context" props which are passed to every element.
// strip the index signature from ExtendedProfile using mapped type magic.
// keeping the index signature causes weird typechecking issues further down the line
// plus there should never be field elements passed with keys which don't exist in ExtendedProfile.
type ExtendedProfileKeys = keyof {
[Property in keyof ExtendedProfile as string extends Property
? never
: Property]: ExtendedProfile[Property];
};
// these are the props which all field elements must accept.
// this is split into `RawProps` and `Props` so we can type `V` instead of
// spraying `ExtendedProfile[K]` all over the place.
// don't use this directly, use the `ProfileFieldElementProps` type instead
type ProfileFieldElementRawProps<V, C> = {
defaultValue: V;
value: V;
@ -26,6 +37,7 @@ export type ProfileFieldElementProps<
C
> = ProfileFieldElementRawProps<ExtendedProfile[K], C>;
// the map of extended profile keys to field element functions
type ProfileFieldElements<C> = {
[Property in ExtendedProfileKeys]?: FunctionComponent<ProfileFieldElementProps<Property, C>>;
};
@ -42,6 +54,12 @@ type ProfileFieldContextProps<C> = {
context: C;
};
/// This element manages the pending state of the profile field widgets.
/// It takes the default values of each field, as well as a map associating a profile field key
/// with an element _function_ (not a rendered element!) that will be used to edit that field.
/// It renders the editor elements internally using React.createElement and passes the rendered
/// elements into the child UI. This allows it to handle the pending state entirely by itself,
/// and provides strong typechecking.
export function ProfileFieldContext<C>({
fieldDefaults,
fieldElements: fieldElementConstructors,
@ -49,11 +67,14 @@ export function ProfileFieldContext<C>({
context,
}: ProfileFieldContextProps<C>): ReactNode {
const [fields, setFields] = useState<ExtendedProfile>(fieldDefaults);
// this callback also runs when fieldDefaults changes,
// which happens when the profile is saved and the pending fields become the new defaults
const reset = useCallback(() => {
setFields(fieldDefaults);
}, [fieldDefaults]);
// set the pending values to the defaults on the first render
useEffect(() => {
reset();
}, [reset]);
@ -72,6 +93,7 @@ export function ProfileFieldContext<C>({
() =>
Object.entries(fields).find(
([key, value]) =>
// deep comparison is necessary here because field values can be any JSON type
deepCompare(fieldDefaults[key as keyof ExtendedProfile], value)
) !== undefined,
[fields, fieldDefaults]
@ -86,6 +108,8 @@ export function ProfileFieldContext<C>({
setValue: (value) => setField(key, value),
key,
};
// element can be undefined if the field defaults didn't include its key,
// which means the HS doesn't support setting that field
if (element !== undefined) {
return React.createElement(element, props);
}