Skip to content

Commit ab19931

Browse files
committed
Nested grouping
1 parent cbc4e68 commit ab19931

File tree

11 files changed

+192
-169
lines changed

11 files changed

+192
-169
lines changed

packages/api/src/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import useGetKeyByActivityId from './useGetKeyByActivityId';
2727
import useGetSendTimeoutForActivity from './useGetSendTimeoutForActivity';
2828
import useGrammars from './useGrammars';
2929
import useGroupActivities from '../providers/GroupActivities/useGroupActivities';
30+
import useGroupActivitiesByName from '../providers/GroupActivities/useGroupActivitiesByName';
3031
import useGroupTimestamp from './useGroupTimestamp';
3132
import useLanguage from './useLanguage';
3233
import useLastAcknowledgedActivityKey from './useLastAcknowledgedActivityKey';
@@ -101,6 +102,7 @@ export {
101102
useGetSendTimeoutForActivity,
102103
useGrammars,
103104
useGroupActivities,
105+
useGroupActivitiesByName,
104106
useGroupTimestamp,
105107
useLanguage,
106108
useLastAcknowledgedActivityKey,

packages/api/src/hooks/middleware/createDefaultGroupActivitiesMiddleware.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,31 @@ function shouldGroupSender(x: WebChatActivity, y: WebChatActivity): boolean {
6868
return roleX === roleY && idX === idY;
6969
}
7070

71+
const passthrough =
72+
next =>
73+
(...args) =>
74+
next(...args);
75+
7176
export default function createDefaultGroupActivitiesMiddleware({
7277
groupTimestamp,
7378
ponyfill
7479
}: {
7580
groupTimestamp: boolean | number;
7681
ponyfill: GlobalScopePonyfill;
77-
}): GroupActivitiesMiddleware {
78-
return () =>
79-
() =>
80-
({ activities }) => ({
81-
sender: bin(activities, shouldGroupSender),
82-
status: bin(activities, createShouldGroupTimestamp(groupTimestamp, ponyfill))
83-
});
82+
}): readonly GroupActivitiesMiddleware[] {
83+
return Object.freeze([
84+
type =>
85+
type === 'sender' || typeof type === 'undefined'
86+
? next =>
87+
({ activities }) => ({ ...next({ activities }), sender: bin(activities, shouldGroupSender) })
88+
: passthrough,
89+
type =>
90+
type === 'status' || typeof type === 'undefined'
91+
? next =>
92+
({ activities }) => ({
93+
...next({ activities }),
94+
status: bin(activities, createShouldGroupTimestamp(groupTimestamp, ponyfill))
95+
})
96+
: passthrough
97+
]);
8498
}
Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import React, { memo, useMemo, type ReactNode } from 'react';
2-
import createDefaultGroupActivitiesMiddleware from '../../hooks/middleware/createDefaultGroupActivitiesMiddleware';
3-
import type GroupActivitiesMiddleware from '../../types/GroupActivitiesMiddleware';
4-
import usePonyfill from '../Ponyfill/usePonyfill';
1+
import React, { memo, useCallback, useMemo, type ReactNode } from 'react';
52

3+
import { type WebChatActivity } from 'botframework-webchat-core';
4+
import { useRefFrom } from 'use-ref-from';
65
import applyMiddleware from '../../hooks/middleware/applyMiddleware';
6+
import createDefaultGroupActivitiesMiddleware from '../../hooks/middleware/createDefaultGroupActivitiesMiddleware';
77
import useStyleOptions from '../../hooks/useStyleOptions';
8+
import type GroupActivitiesMiddleware from '../../types/GroupActivitiesMiddleware';
89
import { type GroupActivities } from '../../types/GroupActivitiesMiddleware';
10+
import usePonyfill from '../Ponyfill/usePonyfill';
911
import GroupActivitiesContext, { type GroupActivitiesContextType } from './private/GroupActivitiesContext';
12+
import isGroupingValid from './private/isGroupingValid';
1013

1114
type GroupActivitiesComposerProps = Readonly<{
1215
children?: ReactNode | undefined;
@@ -15,20 +18,94 @@ type GroupActivitiesComposerProps = Readonly<{
1518

1619
function GroupActivitiesComposer({ children, groupActivitiesMiddleware }: GroupActivitiesComposerProps) {
1720
const [ponyfill] = usePonyfill();
18-
const [{ groupTimestamp }] = useStyleOptions();
21+
const [{ groupActivitiesBy, groupTimestamp }] = useStyleOptions();
22+
23+
const runMiddleware = useMemo<(type?: string | undefined) => GroupActivities>(
24+
() =>
25+
applyMiddleware(
26+
'group activities',
27+
...groupActivitiesMiddleware,
28+
...createDefaultGroupActivitiesMiddleware({ groupTimestamp, ponyfill }),
29+
() => () => () => ({})
30+
),
31+
[groupActivitiesMiddleware, groupTimestamp, ponyfill]
32+
);
33+
34+
const runAllMiddleware = useMemo(() => runMiddleware(), [runMiddleware]);
35+
36+
const groupActivities: GroupActivities = useCallback(
37+
({ activities }: { activities: readonly WebChatActivity[] }) => {
38+
const results = runAllMiddleware({ activities });
39+
const validatedResults = new Map<string, readonly (readonly WebChatActivity[])[]>();
1940

20-
const runMiddleware = applyMiddleware(
21-
'group activities',
22-
...groupActivitiesMiddleware,
23-
createDefaultGroupActivitiesMiddleware({ groupTimestamp, ponyfill })
41+
for (const [name, result] of Object.entries(results)) {
42+
if (isGroupingValid(activities, result)) {
43+
validatedResults.set(name, result);
44+
} else {
45+
validatedResults.set(
46+
name,
47+
activities.map(activity => Object.freeze([activity]))
48+
);
49+
}
50+
}
51+
52+
return Object.fromEntries(validatedResults);
53+
},
54+
[runAllMiddleware]
55+
);
56+
57+
const groupActivitiesByGroup: Map<string, GroupActivities> = useMemo(
58+
() =>
59+
new Map<string, GroupActivities>(
60+
groupActivitiesBy.map(groupingName => [groupingName, runMiddleware(groupingName)])
61+
),
62+
[groupActivitiesBy, runMiddleware]
2463
);
2564

26-
const groupActivities: GroupActivities = runMiddleware({});
65+
const groupActivitiesByGroupRef = useRefFrom(groupActivitiesByGroup);
66+
const groupActivitiesByRef = useRefFrom(groupActivitiesBy);
67+
68+
const groupActivitiesByName = useCallback<
69+
(activities: readonly WebChatActivity[], groupingName: string) => readonly (readonly WebChatActivity[])[]
70+
>(
71+
(activities, groupingName) => {
72+
const group = groupActivitiesByGroupRef.current.get(groupingName);
73+
74+
if (group) {
75+
const result = new Map(Object.entries(group({ activities })));
2776

28-
const context = useMemo<GroupActivitiesContextType>(() => ({ groupActivities }), [groupActivities]);
77+
if (result.has(groupingName)) {
78+
const groupingResult = result.get(groupingName);
79+
80+
if (isGroupingValid(activities, groupingResult)) {
81+
return groupingResult;
82+
}
83+
} else {
84+
console.warn(
85+
`botframework-webchat: groupActivitiesMiddleware('${groupingName}') does not return any results`
86+
);
87+
}
88+
} else {
89+
console.warn(
90+
`botframework-webchat: useGroupActivitiesBy can only be called with one of ${groupActivitiesByRef.current}, however "${groupingName}" was passed instead`
91+
);
92+
}
93+
94+
return Object.freeze(activities.map(activity => Object.freeze([activity])));
95+
},
96+
[groupActivitiesByGroupRef, groupActivitiesByRef]
97+
);
98+
99+
const context = useMemo<GroupActivitiesContextType>(
100+
() => ({
101+
groupActivities,
102+
groupActivitiesByName
103+
}),
104+
[groupActivities, groupActivitiesByName]
105+
);
29106

30107
return <GroupActivitiesContext.Provider value={context}>{children}</GroupActivitiesContext.Provider>;
31108
}
32109

33110
export default memo(GroupActivitiesComposer);
34-
export type { GroupActivitiesComposerProps };
111+
export { type GroupActivitiesComposerProps };

packages/api/src/providers/GroupActivities/private/GroupActivitiesContext.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import { type WebChatActivity } from 'botframework-webchat-core';
12
import { createContext } from 'react';
2-
import { type GroupActivities } from '../../../types/GroupActivitiesMiddleware';
33

44
type GroupActivitiesContextType = {
5-
groupActivities: GroupActivities;
5+
groupActivities: (options: { activities: readonly WebChatActivity[] }) => Readonly<{
6+
[key: string]: readonly (readonly WebChatActivity[])[];
7+
}>;
8+
groupActivitiesByName: (
9+
activities: readonly WebChatActivity[],
10+
groupingName: string
11+
) => readonly (readonly WebChatActivity[])[];
612
};
713

814
const GroupActivitiesContext = createContext<GroupActivitiesContextType>(
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { type WebChatActivity } from 'botframework-webchat-core';
2+
3+
export default function isGroupingValid(
4+
source: readonly WebChatActivity[],
5+
bins: readonly (readonly WebChatActivity[])[]
6+
): boolean {
7+
const set = new Set(source);
8+
9+
if (source.length !== set.size) {
10+
console.warn('botframework-webchat: Cannot validate activity grouping because some activities are duplicated');
11+
12+
return false;
13+
}
14+
15+
for (const bin of bins) {
16+
for (const activityInBin of bin) {
17+
if (!set.has(activityInBin)) {
18+
console.warn(
19+
'botframework-webchat: All binned items must be originate from the source list, check groupingActivityMiddleware to make sure it bin from the source list',
20+
{
21+
activityInBin
22+
}
23+
);
24+
25+
return false;
26+
}
27+
28+
set.delete(activityInBin);
29+
}
30+
}
31+
32+
if (set.size) {
33+
console.warn(
34+
'botframework-webchat: Not every activity is binned, check groupingActivityMiddleware to make sure it is binning every activity passed'
35+
);
36+
37+
return false;
38+
}
39+
40+
return true;
41+
}

packages/api/src/providers/GroupActivities/useGroupActivities.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { type WebChatActivity } from 'botframework-webchat-core';
2+
23
import useGroupActivitiesContext from './private/useGroupActivitiesContext';
34

45
type GroupedActivities = readonly (readonly WebChatActivity[])[];
56

67
export default function useGroupActivities(): ({
7-
activities
8-
}: Readonly<{ activities: readonly WebChatActivity[] }>) => Readonly<{
8+
activities,
9+
group
10+
}: Readonly<{
11+
activities: readonly WebChatActivity[];
12+
group?: string | undefined;
13+
}>) => Readonly<{
914
[key: string]: GroupedActivities;
1015
}> {
1116
return useGroupActivitiesContext().groupActivities;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { type WebChatActivity } from 'botframework-webchat-core';
2+
3+
import useGroupActivitiesContext from './private/useGroupActivitiesContext';
4+
5+
export default function useGroupActivitiesBy(): (
6+
activities: readonly WebChatActivity[],
7+
name: string
8+
) => readonly (readonly WebChatActivity[])[] {
9+
return useGroupActivitiesContext().groupActivitiesByName;
10+
}

packages/api/src/types/GroupActivitiesMiddleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type GroupActivities = CallFunction<
1010
>;
1111

1212
type GroupActivitiesMiddleware = FunctionMiddleware<
13-
[],
13+
[string],
1414
[Readonly<{ activities: readonly WebChatActivity[] }>],
1515
{ [key: string]: GroupedActivities }
1616
>;

packages/component/src/BasicTranscript.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import useFocus from './hooks/useFocus';
5353
import useStyleSet from './hooks/useStyleSet';
5454
import ChatHistoryDOMComposer from './providers/ChatHistoryDOM/ChatHistoryDOMComposer';
5555
import useActivityElementMapRef from './providers/ChatHistoryDOM/useActivityElementRef';
56-
import GroupedRenderingActivitiesComposer from './providers/GroupedRenderingActivities/GroupedRenderingActivitiesComposer2';
56+
import GroupedRenderingActivitiesComposer from './providers/GroupedRenderingActivities/GroupedRenderingActivitiesComposer';
5757
import useNumRenderingActivities from './providers/GroupedRenderingActivities/useNumRenderingActivities';
5858
import RenderingActivitiesComposer from './providers/RenderingActivities/RenderingActivitiesComposer';
5959
import TranscriptFocusComposer from './providers/TranscriptFocus/TranscriptFocusComposer';

packages/component/src/providers/GroupedRenderingActivities/GroupedRenderingActivitiesComposer.tsx

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ import { object, optional, parse, pipe, readonly, type InferOutput } from 'valib
66
import reactNode from '../../types/internal/reactNode';
77
import useRenderingActivities from '../RenderingActivities/useRenderingActivities';
88
import { type GroupedRenderingActivities } from './GroupedRenderingActivities';
9-
import group from './private/group';
109
import GroupedRenderingActivitiesContext, {
1110
type GroupedRenderingActivitiesContextType
1211
} from './private/GroupedRenderingActivitiesContext';
1312

14-
const { useGetKeyByActivity, useGroupActivities, useStyleOptions } = hooks;
13+
const { useGetKeyByActivity, useGroupActivitiesByName, useStyleOptions } = hooks;
1514

1615
const groupedRenderingActivitiesComposerPropsSchema = pipe(
1716
object({
@@ -22,45 +21,27 @@ const groupedRenderingActivitiesComposerPropsSchema = pipe(
2221

2322
type GroupedRenderingActivitiesComposerProps = InferOutput<typeof groupedRenderingActivitiesComposerPropsSchema>;
2423

25-
function validateAllEntriesTagged<T>(entries: readonly T[], bins: readonly (readonly T[])[]): boolean {
26-
return entries.every(entry => bins.some(bin => bin.includes(entry)));
27-
}
28-
2924
const GroupedRenderingActivitiesComposer = (props: GroupedRenderingActivitiesComposerProps) => {
3025
const { children } = parse(groupedRenderingActivitiesComposerPropsSchema, props);
3126

3227
const [{ groupActivitiesBy }] = useStyleOptions();
3328
const [activities] = useRenderingActivities();
3429
const getKeyByActivity = useGetKeyByActivity();
35-
const groupActivities = useGroupActivities();
30+
const groupActivitiesByName = useGroupActivitiesByName();
3631

3732
const numRenderingActivitiesState = useMemo<readonly [number]>(
3833
() => Object.freeze([activities.length] as const),
3934
[activities]
4035
);
4136

42-
const activitiesByGroupMap = useMemo<ReadonlyMap<string, readonly (readonly WebChatActivity[])[]>>(() => {
43-
const activitiesByGroupMap = Object.freeze(new Map(Object.entries(groupActivities({ activities }) || {})));
44-
45-
for (const [key, value] of activitiesByGroupMap) {
46-
if (!validateAllEntriesTagged(activities, value)) {
47-
console.warn(
48-
`botframework-webchat: Not every activities are grouped in the "${key}" property. Please fix "groupActivitiesMiddleware" and group every activities`
49-
);
50-
}
51-
}
52-
53-
return activitiesByGroupMap;
54-
}, [activities, groupActivities]);
55-
5637
const groupedRenderingActivitiesState = useMemo<readonly [readonly GroupedRenderingActivities[]]>(() => {
5738
const run = (
5839
activities: readonly WebChatActivity[],
5940
groups: readonly string[]
6041
): readonly GroupedRenderingActivities[] => {
6142
const [name, ...nextNames] = groups;
6243

63-
if (!name) {
44+
if (typeof name === 'undefined') {
6445
return Object.freeze([
6546
Object.freeze({
6647
activities,
@@ -71,24 +52,20 @@ const GroupedRenderingActivitiesComposer = (props: GroupedRenderingActivitiesCom
7152
]);
7253
}
7354

74-
const activitiesByGroup: readonly (readonly WebChatActivity[])[] =
75-
activitiesByGroupMap.get(name) ?? Object.freeze(activities.map(activity => Object.freeze([activity])));
76-
7755
return Object.freeze(
78-
group(activities, entry => Object.freeze(activitiesByGroup.find(group => group.includes(entry)))).map(
79-
groupedActivities =>
80-
Object.freeze({
81-
activities: Object.freeze(groupedActivities),
82-
children: run(groupedActivities, Object.freeze(nextNames)),
83-
key: getKeyByActivity(groupedActivities[0]),
84-
groupingName: name
85-
})
56+
groupActivitiesByName(activities, name).map(grouping =>
57+
Object.freeze({
58+
activities: grouping,
59+
children: run(grouping, nextNames),
60+
groupingName: name,
61+
key: getKeyByActivity(grouping[0])
62+
} satisfies GroupedRenderingActivities)
8663
)
8764
);
8865
};
8966

90-
return Object.freeze([run(activities, Object.freeze(groupActivitiesBy))] as const);
91-
}, [activities, activitiesByGroupMap, getKeyByActivity, groupActivitiesBy]);
67+
return Object.freeze([run(activities, groupActivitiesBy)]);
68+
}, [activities, getKeyByActivity, groupActivitiesBy, groupActivitiesByName]);
9269

9370
const context = useMemo<GroupedRenderingActivitiesContextType>(
9471
() =>

0 commit comments

Comments
 (0)