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,11 +1,8 @@
import { useEffect } from 'react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { useActivities } from '@/activities/hooks/useActivities';
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState';
import { timelineActivitiesNetworkingState } from '@/activities/timeline/states/timelineActivitiesNetworkingState';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
import {
@ -15,7 +12,6 @@ import {
AnimatedPlaceholderEmptyTitle,
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isDefined } from '~/utils/isDefined';
import { TimelineItemsContainer } from './TimelineItemsContainer';
@ -36,21 +32,10 @@ export const Timeline = ({
}: {
targetableObject: ActivityTargetableObject;
}) => {
const { activities, initialized, noActivities } = useActivities({
targetableObjects: [targetableObject],
activitiesFilters: {},
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
skip: !isDefined(targetableObject),
});
const setTimelineTargetableObject = useSetRecoilState(
objectShowPageTargetableObjectState,
const { initialized, noActivities } = useRecoilValue(
timelineActivitiesNetworkingState,
);
useEffect(() => {
setTimelineTargetableObject(targetableObject);
}, [targetableObject, setTimelineTargetableObject]);
const showEmptyState = noActivities;
const showLoadingState = !initialized;
@ -79,7 +64,7 @@ export const Timeline = ({
return (
<StyledMainContainer>
<TimelineItemsContainer activities={activities} />
<TimelineItemsContainer />
</StyledMainContainer>
);
};

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { Activity } from '@/activities/types/Activity';
import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityFirstLevelFamilySelector';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { IconCheckbox, IconNotes } from '@/ui/display/icon';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@ -136,28 +136,42 @@ const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
`;
type TimelineActivityProps = {
activity: Activity;
isLastActivity?: boolean;
activityId: string;
};
export const TimelineActivity = ({
activity,
isLastActivity,
activityId,
}: TimelineActivityProps) => {
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(activity.createdAt);
const exactCreatedAt = beautifyExactDateTime(activity.createdAt);
const activityForTimeline = useRecoilValue(
timelineActivityWithoutTargetsFamilyState(activityId),
);
const beautifiedCreatedAt = activityForTimeline
? beautifyPastDateRelativeToNow(activityForTimeline.createdAt)
: '';
const exactCreatedAt = activityForTimeline
? beautifyExactDateTime(activityForTimeline.createdAt)
: '';
const openActivityRightDrawer = useOpenActivityRightDrawer();
const theme = useTheme();
const activityFromStore = useRecoilValue(recordStoreFamilyState(activity.id));
const activityFromStore = useRecoilValue(
recordStoreFamilyState(activityForTimeline?.id ?? ''),
);
if (!activityForTimeline) {
return <></>;
}
return (
<>
<StyledTimelineItemContainer>
<StyledAvatarContainer>
<Avatar
avatarUrl={activity.author?.avatarUrl}
placeholder={activity.author?.name.firstName ?? ''}
avatarUrl={activityForTimeline.author?.avatarUrl}
placeholder={activityForTimeline.author?.name.firstName ?? ''}
size="sm"
type="rounded"
/>
@ -166,23 +180,26 @@ export const TimelineActivity = ({
<StyledItemTitleContainer>
<StyledItemAuthorText>
<span>
{activity.author?.name.firstName}{' '}
{activity.author?.name.lastName}
{activityForTimeline.author?.name.firstName}{' '}
{activityForTimeline.author?.name.lastName}
</span>
created a {activity.type.toLowerCase()}
created a {activityForTimeline.type.toLowerCase()}
</StyledItemAuthorText>
<StyledItemTitle>
<StyledIconContainer>
{activity.type === 'Note' && (
{activityForTimeline.type === 'Note' && (
<IconNotes size={theme.icon.size.sm} />
)}
{activity.type === 'Task' && (
{activityForTimeline.type === 'Task' && (
<IconCheckbox size={theme.icon.size.sm} />
)}
</StyledIconContainer>
{(activity.type === 'Note' || activity.type === 'Task') && (
{(activityForTimeline.type === 'Note' ||
activityForTimeline.type === 'Task') && (
<StyledActivityTitle
onClick={() => openActivityRightDrawer(activity)}
onClick={() =>
openActivityRightDrawer(activityForTimeline.id)
}
>
<StyledActivityLink
@ -195,11 +212,11 @@ export const TimelineActivity = ({
)}
</StyledItemTitle>
</StyledItemTitleContainer>
<StyledItemTitleDate id={`id-${activity.id}`}>
<StyledItemTitleDate id={`id-${activityForTimeline.id}`}>
{beautifiedCreatedAt}
</StyledItemTitleDate>
<StyledTooltip
anchorSelect={`#id-${activity.id}`}
anchorSelect={`#id-${activityForTimeline.id}`}
content={exactCreatedAt}
clickable
noArrow

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer';
import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { groupActivitiesByMonth } from '../utils/groupActivitiesByMonth';
@ -23,14 +24,12 @@ const StyledTimelineContainer = styled.div`
const StyledScrollWrapper = styled(ScrollWrapper)``;
export type TimelineItemsContainerProps = {
activities: ActivityForDrawer[];
};
export const TimelineItemsContainer = () => {
const timelineActivitiesForGroup = useRecoilValue(
timelineActivitiesForGroupState,
);
export const TimelineItemsContainer = ({
activities,
}: TimelineItemsContainerProps) => {
const groupedActivities = groupActivitiesByMonth(activities);
const groupedActivities = groupActivitiesByMonth(timelineActivitiesForGroup);
return (
<StyledScrollWrapper>

View File

@ -0,0 +1,131 @@
import { useEffect } from 'react';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { useActivities } from '@/activities/hooks/useActivities';
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
import { timelineActivitiesFammilyState } from '@/activities/timeline/states/timelineActivitiesFamilyState';
import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState';
import { timelineActivitiesNetworkingState } from '@/activities/timeline/states/timelineActivitiesNetworkingState';
import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityFirstLevelFamilySelector';
import { Activity } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { sortObjectRecordByDateField } from '@/object-record/utils/sortObjectRecordByDateField';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isDefined } from '~/utils/isDefined';
export const TimelineQueryEffect = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
}) => {
const setTimelineTargetableObject = useSetRecoilState(
objectShowPageTargetableObjectState,
);
useEffect(() => {
setTimelineTargetableObject(targetableObject);
}, [targetableObject, setTimelineTargetableObject]);
const { activities, initialized, noActivities } = useActivities({
targetableObjects: [targetableObject],
activitiesFilters: {},
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
skip: !isDefined(targetableObject),
});
const [timelineActivitiesNetworking, setTimelineActivitiesNetworking] =
useRecoilState(timelineActivitiesNetworkingState);
const [timelineActivitiesForGroup, setTimelineActivitiesForGroup] =
useRecoilState(timelineActivitiesForGroupState);
useEffect(() => {
if (!isDefined(targetableObject)) {
return;
}
const activitiesForGroup = activities
.map((activity) => ({
id: activity.id,
createdAt: activity.createdAt,
}))
.toSorted(sortObjectRecordByDateField('createdAt', 'DescNullsLast'));
const timelineActivitiesForGroupSorted =
timelineActivitiesForGroup.toSorted(
sortObjectRecordByDateField('createdAt', 'DescNullsLast'),
);
if (!isDeeplyEqual(activitiesForGroup, timelineActivitiesForGroupSorted)) {
setTimelineActivitiesForGroup(activitiesForGroup);
}
if (
!isDeeplyEqual(timelineActivitiesNetworking.initialized, initialized) ||
!isDeeplyEqual(timelineActivitiesNetworking.noActivities, noActivities)
) {
setTimelineActivitiesNetworking({
initialized,
noActivities,
});
}
}, [
activities,
initialized,
noActivities,
setTimelineActivitiesNetworking,
targetableObject,
timelineActivitiesNetworking,
timelineActivitiesForGroup,
setTimelineActivitiesForGroup,
]);
const updateTimelineActivities = useRecoilCallback(
({ snapshot, set }) =>
(newActivities: Activity[]) => {
for (const newActivity of newActivities) {
const currentActivity = snapshot
.getLoadable(timelineActivitiesFammilyState(newActivity.id))
.getValue();
if (!isDeeplyEqual(newActivity, currentActivity)) {
set(timelineActivitiesFammilyState(newActivity.id), newActivity);
}
const currentActivityWithoutTarget = snapshot
.getLoadable(
timelineActivityWithoutTargetsFamilyState(newActivity.id),
)
.getValue();
const newActivityWithoutTarget = {
id: newActivity.id,
title: newActivity.title,
createdAt: newActivity.createdAt,
author: newActivity.author,
type: newActivity.type,
};
if (
!isDeeplyEqual(
newActivityWithoutTarget,
currentActivityWithoutTarget,
)
) {
set(
timelineActivityWithoutTargetsFamilyState(newActivity.id),
newActivityWithoutTarget,
);
}
}
},
[],
);
useEffect(() => {
updateTimelineActivities(activities);
}, [activities, updateTimelineActivities]);
return <></>;
};

View File

@ -68,7 +68,7 @@ export const TimelineActivityGroup = ({
{group.items.map((activity, index) => (
<TimelineActivity
key={activity.id}
activity={activity}
activityId={activity.id}
isLastActivity={index === group.items.length - 1}
/>
))}