Skip to content

Commit cfc413e

Browse files
committed
opt: logical grouping static context
1 parent c06321e commit cfc413e

File tree

4 files changed

+51
-113
lines changed

4 files changed

+51
-113
lines changed

packages/component/src/Middleware/ActivityGrouping/ui/PartGrouping/private/PartGroupingActivity.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ function PartGroupingActivity(props: PartGroupingActivityProps) {
130130
[activities, getKeyByActivity]
131131
);
132132

133-
const { shouldSkipRender } = usePartGroupingLogicalGroup({
133+
usePartGroupingLogicalGroup({
134134
activityKeys,
135135
isCollapsed: !isGroupOpen
136136
});
@@ -154,11 +154,6 @@ function PartGroupingActivity(props: PartGroupingActivityProps) {
154154

155155
const topAlignedCallout = isZeroOrPositive(bubbleNubOffset);
156156

157-
// Skip render if this is the initial grouping setup
158-
if (shouldSkipRender) {
159-
return null;
160-
}
161-
162157
return (
163158
<FocusablePartGroupingActivity activity={firstActivity}>
164159
<StackedLayoutRoot

packages/component/src/Middleware/ActivityGrouping/ui/PartGrouping/private/usePartGroupingLogicalGroup.ts

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { useMemo, useRef } from 'react';
1+
import { useEffect, useMemo, useRef } from 'react';
2+
import { useRefFrom } from 'use-ref-from';
23

34
import { useAddLogicalGrouping } from '../../../../../providers/ActivityLogicalGrouping';
45

56
const getPartGropKey = (key: string) => `part-grouping-${key}`;
67

78
type UsePartGroupingLogicalGroupOptions = {
89
activityKeys: readonly string[];
9-
isGroupOpen: boolean;
10+
isCollapsed: boolean;
1011
};
1112

1213
type UsePartGroupingLogicalGroupReturn = {
@@ -21,34 +22,37 @@ type UsePartGroupingLogicalGroupReturn = {
2122
*/
2223
function usePartGroupingLogicalGroup({
2324
activityKeys,
24-
isGroupOpen
25+
isCollapsed
2526
}: UsePartGroupingLogicalGroupOptions): UsePartGroupingLogicalGroupReturn {
2627
const addLogicalGrouping = useAddLogicalGrouping();
27-
const isInitialGroupingRef = useRef(true);
28-
29-
const shouldSkipRender = useMemo(() => {
30-
const isInitialGrouping = isInitialGroupingRef.current;
31-
32-
addLogicalGrouping({
33-
// use activity key for group identifier as there should be only one logical group per activity
34-
id: getPartGropKey(activityKeys.at(0)),
35-
name: 'part',
36-
activityKeys: [...activityKeys],
37-
getGroupState: () => ({
38-
isCollapsed: !isGroupOpen
39-
})
40-
});
41-
42-
// If this is the initial grouping, mark it and skip render
43-
if (isInitialGrouping) {
44-
isInitialGroupingRef.current = false;
45-
return true;
46-
}
47-
48-
return false;
49-
}, [activityKeys, addLogicalGrouping, isGroupOpen]);
50-
51-
return { shouldSkipRender };
28+
29+
useMemo(
30+
() =>
31+
addLogicalGrouping({
32+
// use activity key for group identifier as there should be only one logical group per activity
33+
id: getPartGropKey(activityKeys.at(0)),
34+
name: 'part',
35+
activityKeys: [...activityKeys],
36+
getGroupState: () => ({
37+
isCollapsed
38+
})
39+
}),
40+
[activityKeys, addLogicalGrouping, isCollapsed]
41+
);
42+
43+
const addLogicalGroupingRef = useRef(addLogicalGrouping);
44+
const activityKeysRef = useRefFrom(activityKeys);
45+
46+
useEffect(
47+
() => () =>
48+
addLogicalGroupingRef.current({
49+
id: getPartGropKey(activityKeysRef.current.at(0)),
50+
name: 'part(removed)',
51+
activityKeys: [],
52+
getGroupState: () => ({ isCollapsed: false })
53+
}),
54+
[activityKeysRef, addLogicalGroupingRef]
55+
);
5256
}
5357

5458
export default usePartGroupingLogicalGroup;

packages/component/src/providers/ActivityLogicalGrouping/ActivityLogicalGroupingComposer.ts

Lines changed: 18 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { useMemoWithPrevious } from '@msinternal/botframework-webchat-react-hooks';
2-
import { memo, createElement, useCallback, useMemo, useState, type ReactNode } from 'react';
3-
import { useRefFrom } from 'use-ref-from';
1+
import { memo, createElement, useCallback, useMemo, type ReactNode, useRef } from 'react';
42

53
import ActivityLogicalGroupingContext, {
64
type ActivityLogicalGroupingContextType,
@@ -13,85 +11,42 @@ type ActivityLogicalGroupingComposerProps = Readonly<{
1311
}>;
1412

1513
const ActivityLogicalGroupingComposer = ({ children }: ActivityLogicalGroupingComposerProps) => {
16-
const [logicalGroupings, setLogicalGroupings] = useState<readonly LogicalGrouping[]>([]);
17-
const logicalGroupingsRef = useRefFrom(logicalGroupings);
14+
const logicalGroupingsRef = useRef<Map<string, LogicalGrouping>>(new Map());
15+
const activityToGroupMapRef = useRef<ReadonlyMap<string, string | undefined>>(new Map());
1816

19-
// Create a map from activity key to group key for quick lookups with reference stability
20-
const activityToGroupMap = useMemoWithPrevious(
21-
(prevMap?: ReadonlyMap<string, string>) => {
22-
const newMap = new Map<string, string>();
17+
const addLogicalGrouping = useCallback(
18+
(grouping: LogicalGrouping) => {
19+
const prevGroupingKeys = logicalGroupingsRef.current.get(grouping.id)?.activityKeys ?? [];
2320

24-
logicalGroupings.forEach(group => {
25-
group.activityKeys.forEach(activityKey => {
26-
activityKey && newMap.set(activityKey, group.id);
27-
});
28-
});
21+
logicalGroupingsRef.current.set(grouping.id, grouping);
2922

30-
// Check if the map content has actually changed
31-
if (prevMap && prevMap.size === newMap.size) {
32-
let hasChanged = false;
33-
for (const [key, value] of newMap) {
34-
if (prevMap.get(key) !== value) {
35-
hasChanged = true;
36-
break;
37-
}
38-
}
23+
const toRemoveSet = new Set(prevGroupingKeys).difference(new Set(grouping.activityKeys));
24+
const toAddSet = new Set(grouping.activityKeys).difference(new Set(prevGroupingKeys));
3925

40-
if (!hasChanged) {
41-
// Return the previous map to maintain reference stability
42-
return prevMap;
43-
}
44-
}
45-
46-
return newMap;
26+
activityToGroupMapRef.current = new Map([
27+
...[...activityToGroupMapRef.current].filter(([, value]) => !!value && !toRemoveSet.has(value)),
28+
...[...toAddSet].map(activityKey => [activityKey, grouping.id] as const)
29+
]);
4730
},
48-
[logicalGroupings]
31+
[logicalGroupingsRef]
4932
);
5033

51-
const activityToGroupMapRef = useRefFrom(activityToGroupMap);
52-
53-
const addLogicalGrouping = useCallback((grouping: LogicalGrouping) => {
54-
setLogicalGroupings(prev => {
55-
// Remove any existing grouping with the same ID and add the new one
56-
const filtered = prev.filter(existing => existing.id !== grouping.id);
57-
return Object.freeze([...filtered, grouping]);
58-
});
59-
}, []);
60-
6134
const getLogicalGroupKey = useCallback(
6235
(activityKey: string): string | undefined => activityToGroupMapRef.current.get(activityKey),
6336
[activityToGroupMapRef]
6437
);
6538

66-
const shouldFocusLogicalGroup = useCallback(
67-
(activityKey: string): boolean => {
68-
const groupKey = activityToGroupMapRef.current.get(activityKey);
69-
if (!groupKey) {
70-
return false;
71-
}
72-
73-
const group = logicalGroupingsRef.current.find(g => g.id === groupKey);
74-
if (!group || !group.getGroupState) {
75-
return false;
76-
}
77-
78-
const groupState = group.getGroupState();
79-
return !groupState.isCollapsed;
80-
},
81-
[activityToGroupMapRef, logicalGroupingsRef]
82-
);
83-
8439
const getGroupState = useCallback(
8540
(groupKey: string): GroupState | undefined => {
86-
const group = logicalGroupingsRef.current.find(g => g.id === groupKey);
41+
const group = logicalGroupingsRef.current.get(groupKey);
8742
return group?.getGroupState?.();
8843
},
8944
[logicalGroupingsRef]
9045
);
9146

9247
const getGroupBoundaries = useCallback(
9348
(groupKey: string): [string, string] => {
94-
const group = logicalGroupingsRef.current.find(g => g.id === groupKey);
49+
const group = logicalGroupingsRef.current.get(groupKey);
9550
if (!group || !group.activityKeys.length) {
9651
return [undefined, undefined];
9752
}
@@ -101,28 +56,14 @@ const ActivityLogicalGroupingComposer = ({ children }: ActivityLogicalGroupingCo
10156
[logicalGroupingsRef]
10257
);
10358

104-
const activityToGroupMapState = useMemo<readonly [ReadonlyMap<string, string>]>(
105-
() => Object.freeze([activityToGroupMap] as const),
106-
[activityToGroupMap]
107-
);
108-
10959
const contextValue: ActivityLogicalGroupingContextType = useMemo(
11060
() => ({
11161
addLogicalGrouping,
11262
getLogicalGroupKey,
113-
shouldFocusLogicalGroup,
11463
getGroupState,
115-
getGroupBoundaries,
116-
activityToGroupMapState
64+
getGroupBoundaries
11765
}),
118-
[
119-
addLogicalGrouping,
120-
getLogicalGroupKey,
121-
shouldFocusLogicalGroup,
122-
getGroupState,
123-
getGroupBoundaries,
124-
activityToGroupMapState
125-
]
66+
[addLogicalGrouping, getLogicalGroupKey, getGroupState, getGroupBoundaries]
12667
);
12768

12869
return createElement(ActivityLogicalGroupingContext.Provider, { value: contextValue }, children);

packages/component/src/providers/ActivityLogicalGrouping/private/ActivityLogicalGroupingContext.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ type LogicalGrouping = Readonly<{
1414
type ActivityLogicalGroupingContextType = Readonly<{
1515
addLogicalGrouping: (grouping: LogicalGrouping) => void;
1616
getLogicalGroupKey: (activityKey: string) => string | undefined;
17-
shouldFocusLogicalGroup: (activityKey: string) => boolean;
1817
getGroupState: (groupKey: string) => GroupState | undefined;
1918
getGroupBoundaries: (groupKey: string) => [string | undefined, string | undefined];
20-
activityToGroupMapState: readonly [ReadonlyMap<string, string>];
2119
}>;
2220

2321
const { contextComponentType, useContext } = createContextAndHook<ActivityLogicalGroupingContextType>(

0 commit comments

Comments
 (0)