Skip to content

Commit 960a2ad

Browse files
committed
opt: logical grouping should emit state updates
1 parent cfc413e commit 960a2ad

File tree

7 files changed

+89
-61
lines changed

7 files changed

+89
-61
lines changed

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,21 @@ import { reactNode } from '@msinternal/botframework-webchat-react-valibot';
22
import { hooks } from 'botframework-webchat-api';
33
import { getOrgSchemaMessage, type WebChatActivity } from 'botframework-webchat-core';
44
import React, { memo, useCallback, useMemo, useState } from 'react';
5-
import { array, minLength, object, optional, pipe, readonly, transform, any, parse, type InferOutput } from 'valibot';
6-
7-
import usePartGroupingLogicalGroup, { getPartGropKey } from './usePartGroupingLogicalGroup';
5+
import {
6+
any,
7+
array,
8+
minLength,
9+
object,
10+
optional,
11+
parse,
12+
pipe,
13+
readonly,
14+
string,
15+
transform,
16+
type InferOutput
17+
} from 'valibot';
18+
19+
import usePartGroupingLogicalGroup from './usePartGroupingLogicalGroup';
820
import CollapsibleGrouping from '../CollapsibleGrouping';
921
import useActivityElementMapRef from '../../../../../providers/ChatHistoryDOM/useActivityElementRef';
1022
import StackedLayoutMain from '../../../../../Activity/StackedLayoutMain';
@@ -53,6 +65,7 @@ const partGroupingFocusableActivityPropsSchema = pipe(
5365
transform(value => value as WebChatActivity),
5466
readonly()
5567
),
68+
groupKey: string(),
5669
children: optional(reactNode())
5770
}),
5871
readonly()
@@ -62,16 +75,14 @@ type FocusablePartGroupingActivityProps = InferOutput<typeof partGroupingFocusab
6275

6376
function FocusablePartGroupingActivity(props: FocusablePartGroupingActivityProps) {
6477
const [activeDescendantId] = useActiveDescendantId();
65-
const { activity, children } = parse(partGroupingFocusableActivityPropsSchema, props);
78+
const { activity, children, groupKey } = parse(partGroupingFocusableActivityPropsSchema, props);
6679

6780
const getGroupDescendantIdByActivityKey = useGetGroupDescendantIdByActivityKey();
6881
const focusByGroupKey = useFocusByGroupKey();
6982

7083
const getKeyByActivity = useGetKeyByActivity();
7184
const groupingActivityDescendantId = getGroupDescendantIdByActivityKey(getKeyByActivity(activity));
7285

73-
const groupKey = useMemo(() => getPartGropKey(getKeyByActivity(activity)), [getKeyByActivity, activity]);
74-
7586
const focusSelf = useCallback<(withFocus?: boolean) => void>(
7687
(withFocus?: boolean) => focusByGroupKey(groupKey, withFocus),
7788
[groupKey, focusByGroupKey]
@@ -130,7 +141,7 @@ function PartGroupingActivity(props: PartGroupingActivityProps) {
130141
[activities, getKeyByActivity]
131142
);
132143

133-
usePartGroupingLogicalGroup({
144+
const groupKey = usePartGroupingLogicalGroup({
134145
activityKeys,
135146
isCollapsed: !isGroupOpen
136147
});
@@ -155,7 +166,7 @@ function PartGroupingActivity(props: PartGroupingActivityProps) {
155166
const topAlignedCallout = isZeroOrPositive(bubbleNubOffset);
156167

157168
return (
158-
<FocusablePartGroupingActivity activity={firstActivity}>
169+
<FocusablePartGroupingActivity activity={firstActivity} groupKey={groupKey}>
159170
<StackedLayoutRoot
160171
hideAvatar={hasAvatar && !showAvatar}
161172
isGroup={true}
Lines changed: 21 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,41 @@
1-
import { useEffect, useMemo, useRef } from 'react';
1+
import { useCallback, useEffect, useMemo } from 'react';
22
import { useRefFrom } from 'use-ref-from';
3+
import random from 'math-random';
34

4-
import { useAddLogicalGrouping } from '../../../../../providers/ActivityLogicalGrouping';
5-
6-
const getPartGropKey = (key: string) => `part-grouping-${key}`;
5+
import { useAddLogicalGrouping, useRemoveLogicalGrouping } from '../../../../../providers/ActivityLogicalGrouping';
6+
import { useStateWithRef } from 'use-state-with-ref';
77

88
type UsePartGroupingLogicalGroupOptions = {
99
activityKeys: readonly string[];
1010
isCollapsed: boolean;
1111
};
1212

13-
type UsePartGroupingLogicalGroupReturn = {
14-
shouldSkipRender: boolean;
15-
};
16-
17-
/**
18-
* Custom hook for managing part grouping logical groups.
19-
*
20-
* @param options - Configuration options for the grouping
21-
* @returns Object containing shouldSkipRender flag
22-
*/
23-
function usePartGroupingLogicalGroup({
24-
activityKeys,
25-
isCollapsed
26-
}: UsePartGroupingLogicalGroupOptions): UsePartGroupingLogicalGroupReturn {
13+
function usePartGroupingLogicalGroup({ activityKeys, isCollapsed }: UsePartGroupingLogicalGroupOptions): string {
14+
// eslint-disable-next-line no-magic-numbers
15+
const [partGroupKey, _setPartKey, partKeyRef] = useStateWithRef(() => `part-${random().toString(36).slice(2, 11)}`);
2716
const addLogicalGrouping = useAddLogicalGrouping();
17+
const removeLogicalGrouping = useRemoveLogicalGrouping();
18+
19+
const isCollapsedRef = useRefFrom(isCollapsed);
20+
21+
const getGroupState = useCallback(() => ({ isCollapsed: isCollapsedRef.current }), [isCollapsedRef]);
2822

2923
useMemo(
3024
() =>
3125
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-
})
26+
key: partKeyRef.current,
27+
activityKeys: Array.from(activityKeys),
28+
getGroupState
3929
}),
40-
[activityKeys, addLogicalGrouping, isCollapsed]
30+
[activityKeys, addLogicalGrouping, getGroupState, partKeyRef]
4131
);
4232

43-
const addLogicalGroupingRef = useRef(addLogicalGrouping);
44-
const activityKeysRef = useRefFrom(activityKeys);
33+
const removeLogicalGroupingRef = useRefFrom(removeLogicalGrouping);
4534

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-
);
35+
useEffect(() => () => removeLogicalGroupingRef.current(partKeyRef.current), [partKeyRef, removeLogicalGroupingRef]);
36+
37+
return partGroupKey;
5638
}
5739

5840
export default usePartGroupingLogicalGroup;
59-
export { type UsePartGroupingLogicalGroupOptions, type UsePartGroupingLogicalGroupReturn, getPartGropKey };
41+
export { type UsePartGroupingLogicalGroupOptions };

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,45 @@ import ActivityLogicalGroupingContext, {
55
type LogicalGrouping,
66
type GroupState
77
} from './private/ActivityLogicalGroupingContext';
8+
import useStateWithOptimisticRef from './private/useStateWithOptimisticRef';
89

910
type ActivityLogicalGroupingComposerProps = Readonly<{
1011
children?: ReactNode | undefined;
1112
}>;
1213

1314
const ActivityLogicalGroupingComposer = ({ children }: ActivityLogicalGroupingComposerProps) => {
1415
const logicalGroupingsRef = useRef<Map<string, LogicalGrouping>>(new Map());
15-
const activityToGroupMapRef = useRef<ReadonlyMap<string, string | undefined>>(new Map());
16+
const [_activityToGroupMap, setActivityToGroupMap, activityToGroupMapRef] = useStateWithOptimisticRef<
17+
ReadonlyMap<string, string | undefined>
18+
>(new Map());
1619

1720
const addLogicalGrouping = useCallback(
1821
(grouping: LogicalGrouping) => {
19-
const prevGroupingKeys = logicalGroupingsRef.current.get(grouping.id)?.activityKeys ?? [];
22+
const prevGroupingKeys = logicalGroupingsRef.current.get(grouping.key)?.activityKeys ?? [];
2023

21-
logicalGroupingsRef.current.set(grouping.id, grouping);
24+
logicalGroupingsRef.current.set(grouping.key, grouping);
2225

2326
const toRemoveSet = new Set(prevGroupingKeys).difference(new Set(grouping.activityKeys));
2427
const toAddSet = new Set(grouping.activityKeys).difference(new Set(prevGroupingKeys));
2528

26-
activityToGroupMapRef.current = new Map([
27-
...[...activityToGroupMapRef.current].filter(([, value]) => !!value && !toRemoveSet.has(value)),
28-
...[...toAddSet].map(activityKey => [activityKey, grouping.id] as const)
29-
]);
29+
setActivityToGroupMap(
30+
new Map(
31+
[].concat(
32+
Array.from(activityToGroupMapRef.current).filter(([, value]) => !!value && !toRemoveSet.has(value)),
33+
Array.from(toAddSet).map(activityKey => [activityKey, grouping.key] as const)
34+
)
35+
)
36+
);
3037
},
31-
[logicalGroupingsRef]
38+
[activityToGroupMapRef, setActivityToGroupMap]
39+
);
40+
41+
const removeLogicalGrouping = useCallback(
42+
(key: string) => {
43+
logicalGroupingsRef.current.delete(key);
44+
setActivityToGroupMap(new Map([...activityToGroupMapRef.current].filter(([, value]) => value !== key)));
45+
},
46+
[activityToGroupMapRef, setActivityToGroupMap]
3247
);
3348

3449
const getLogicalGroupKey = useCallback(
@@ -61,9 +76,10 @@ const ActivityLogicalGroupingComposer = ({ children }: ActivityLogicalGroupingCo
6176
addLogicalGrouping,
6277
getLogicalGroupKey,
6378
getGroupState,
64-
getGroupBoundaries
79+
getGroupBoundaries,
80+
removeLogicalGrouping
6581
}),
66-
[addLogicalGrouping, getLogicalGroupKey, getGroupState, getGroupBoundaries]
82+
[addLogicalGrouping, getLogicalGroupKey, getGroupState, getGroupBoundaries, removeLogicalGrouping]
6783
);
6884

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { default as ActivityLogicalGroupingComposer } from './ActivityLogicalGroupingComposer';
22
export { default as useAddLogicalGrouping } from './useAddLogicalGrouping';
3+
export { default as useRemoveLogicalGrouping } from './useRemoveLogicalGrouping';
34
export { default as useGetLogicalGroupKey } from './useGetLogicalGroupKey';
45
export { default as useGetGroupState } from './useGetGroupState';
56
export type { ActivityLogicalGroupingComposerProps } from './ActivityLogicalGroupingComposer';

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ type GroupState = Readonly<{
55
}>;
66

77
type LogicalGrouping = Readonly<{
8-
id: string;
9-
name: string;
8+
key: string;
109
activityKeys: string[];
1110
getGroupState?: () => GroupState;
1211
}>;
1312

1413
type ActivityLogicalGroupingContextType = Readonly<{
1514
addLogicalGrouping: (grouping: LogicalGrouping) => void;
16-
getLogicalGroupKey: (activityKey: string) => string | undefined;
17-
getGroupState: (groupKey: string) => GroupState | undefined;
1815
getGroupBoundaries: (groupKey: string) => [string | undefined, string | undefined];
16+
getGroupState: (groupKey: string) => GroupState | undefined;
17+
getLogicalGroupKey: (activityKey: string) => string | undefined;
18+
removeLogicalGrouping: (key: string) => void;
1919
}>;
2020

2121
const { contextComponentType, useContext } = createContextAndHook<ActivityLogicalGroupingContextType>(
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useCallback, useRef, useState } from 'react';
2+
3+
export default function useStateWithOptimisticRef<T>(initialState: T | (() => T)) {
4+
const [state, setState] = useState(initialState);
5+
const stateRef = useRef<T>(state);
6+
7+
const setStateWithRefUpdate = useCallback((newState: T) => {
8+
setState(newState);
9+
stateRef.current = newState;
10+
}, []);
11+
12+
return [state, setStateWithRefUpdate, stateRef] as const;
13+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { useActivityLogicalGroupingContext } from './private/ActivityLogicalGroupingContext';
2+
3+
export default function useRemoveLogicalGrouping(): (key: string) => void {
4+
return useActivityLogicalGroupingContext().removeLogicalGrouping;
5+
}

0 commit comments

Comments
 (0)