Improved activity editor re-renders (#4149)

* Refactor task count

* Fixed show page rerender

* Less rerenders and way better title and body UX

* Finished breaking down activity editor subscriptions

* Removed console.log

* Last console.log

* Fixed bugs and cleaned
This commit is contained in:
Lucas Bordeau
2024-02-23 17:54:27 +01:00
committed by GitHub
parent 5de1c2c31d
commit fb920a92e7
48 changed files with 1114 additions and 527 deletions

View File

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { BlockNoteEditor } from '@blocknote/core';
import { useBlockNote } from '@blocknote/react';
import styled from '@emotion/styled';
@ -9,6 +9,7 @@ import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
import { Activity } from '@/activities/types/Activity';
@ -37,17 +38,27 @@ const StyledBlockNoteStyledContainer = styled.div`
`;
type ActivityBodyEditorProps = {
activity: Activity;
activityId: string;
fillTitleFromBody: boolean;
};
export const ActivityBodyEditor = ({
activity,
activityId,
fillTitleFromBody,
}: ActivityBodyEditorProps) => {
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
const activity = activityInStore as Activity | null;
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
activityTitleHasBeenSetFamilyState({
activityId: activity.id,
activityId: activityId,
}),
);
const [activityBody, setActivityBody] = useRecoilState(
activityBodyFamilyState({
activityId: activityId,
}),
);
@ -67,27 +78,31 @@ export const ActivityBodyEditor = ({
const { upsertActivity } = useUpsertActivity();
const persistBodyDebounced = useDebouncedCallback((newBody: string) => {
upsertActivity({
activity,
input: {
body: newBody,
},
});
}, 500);
const persistTitleAndBodyDebounced = useDebouncedCallback(
(newTitle: string, newBody: string) => {
if (activity) {
upsertActivity({
activity,
input: {
title: newTitle,
body: newBody,
},
});
}
}, 300);
setActivityTitleHasBeenSet(true);
const persistTitleAndBodyDebounced = useDebouncedCallback(
(newTitle: string, newBody: string) => {
if (activity) {
upsertActivity({
activity,
input: {
title: newTitle,
body: newBody,
},
});
setActivityTitleHasBeenSet(true);
}
},
500,
200,
);
const updateTitleAndBody = useCallback(
@ -104,28 +119,6 @@ export const ActivityBodyEditor = ({
canCreateActivityState,
);
const handleBodyChange = useCallback(
(activityBody: string) => {
if (!canCreateActivity) {
setCanCreateActivity(true);
}
if (!activityTitleHasBeenSet && fillTitleFromBody) {
updateTitleAndBody(activityBody);
} else {
persistBodyDebounced(activityBody);
}
},
[
fillTitleFromBody,
persistBodyDebounced,
activityTitleHasBeenSet,
updateTitleAndBody,
setCanCreateActivity,
canCreateActivity,
],
);
const slashMenuItems = getSlashMenu();
const [uploadFile] = useUploadFileMutation();
@ -148,63 +141,105 @@ export const ActivityBodyEditor = ({
return imageUrl;
};
const editor: BlockNoteEditor<typeof blockSpecs> | null = useBlockNote({
initialContent:
isNonEmptyString(activity.body) && activity.body !== '{}'
? JSON.parse(activity.body)
: undefined,
domAttributes: { editor: { class: 'editor' } },
onEditorContentChange: useRecoilCallback(
({ snapshot, set }) =>
(editor: BlockNoteEditor) => {
const newStringifiedBody =
JSON.stringify(editor.topLevelBlocks) ?? '';
const handlePersistBody = useCallback(
(activityBody: string) => {
if (!canCreateActivity) {
setCanCreateActivity(true);
}
set(recordStoreFamilyState(activity.id), (oldActivity) => {
if (!activityTitleHasBeenSet && fillTitleFromBody) {
updateTitleAndBody(activityBody);
} else {
persistBodyDebounced(activityBody);
}
},
[
fillTitleFromBody,
persistBodyDebounced,
activityTitleHasBeenSet,
updateTitleAndBody,
setCanCreateActivity,
canCreateActivity,
],
);
const handleBodyChange = useRecoilCallback(
({ snapshot, set }) =>
(newStringifiedBody: string) => {
set(recordStoreFamilyState(activityId), (oldActivity) => {
return {
...oldActivity,
id: activityId,
body: newStringifiedBody,
};
});
modifyActivityFromCache(activityId, {
body: () => {
return newStringifiedBody;
},
});
const activityTitleHasBeenSet = snapshot
.getLoadable(
activityTitleHasBeenSetFamilyState({
activityId: activityId,
}),
)
.getValue();
const blockBody = JSON.parse(newStringifiedBody);
const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string;
if (!activityTitleHasBeenSet && fillTitleFromBody) {
set(recordStoreFamilyState(activityId), (oldActivity) => {
return {
...oldActivity,
id: activity.id,
body: newStringifiedBody,
id: activityId,
title: newTitleFromBody,
};
});
modifyActivityFromCache(activity.id, {
body: () => {
return newStringifiedBody;
modifyActivityFromCache(activityId, {
title: () => {
return newTitleFromBody;
},
});
}
const activityTitleHasBeenSet = snapshot
.getLoadable(
activityTitleHasBeenSetFamilyState({
activityId: activity.id,
}),
)
.getValue();
handlePersistBody(newStringifiedBody);
},
[activityId, fillTitleFromBody, modifyActivityFromCache, handlePersistBody],
);
const blockBody = JSON.parse(newStringifiedBody);
const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string;
const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500);
if (!activityTitleHasBeenSet && fillTitleFromBody) {
set(recordStoreFamilyState(activity.id), (oldActivity) => {
return {
...oldActivity,
id: activity.id,
title: newTitleFromBody,
};
});
const handleEditorChange = (newEditor: BlockNoteEditor) => {
const newStringifiedBody = JSON.stringify(newEditor.topLevelBlocks) ?? '';
modifyActivityFromCache(activity.id, {
title: () => {
return newTitleFromBody;
},
});
}
setActivityBody(newStringifiedBody);
handleBodyChange(newStringifiedBody);
},
[activity, fillTitleFromBody, modifyActivityFromCache, handleBodyChange],
),
handleBodyChangeDebounced(newStringifiedBody);
};
const initialBody = useMemo(() => {
if (isNonEmptyString(activityBody) && activityBody !== '{}') {
return JSON.parse(activityBody);
} else if (
activity &&
isNonEmptyString(activity.body) &&
activity?.body !== '{}'
) {
return JSON.parse(activity.body);
} else {
return undefined;
}
}, [activity, activityBody]);
const editor: BlockNoteEditor<typeof blockSpecs> | null = useBlockNote({
initialContent: initialBody,
domAttributes: { editor: { class: 'editor' } },
onEditorContentChange: handleEditorChange,
slashMenuItems,
blockSpecs: blockSpecs,
uploadFile: handleUploadAttachment,