Skip to content

Commit 3a2a57a

Browse files
committed
feat: activity actions decorator
1 parent 4fa88a6 commit 3a2a57a

11 files changed

+326
-38
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
4+
<head>
5+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
6+
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script>
7+
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
8+
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
9+
<script crossorigin="anonymous" src="/test-harness.js"></script>
10+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
11+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
12+
<script crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
13+
<style>
14+
.flair {
15+
border-radius: inherit;
16+
border: solid 2px red;
17+
}
18+
19+
.loader {
20+
border-bottom: solid 4px blue;
21+
}
22+
23+
.actions {
24+
display: flex;
25+
gap: 4px;
26+
}
27+
28+
.actions > * {
29+
border: solid 2px green !important;
30+
}
31+
</style>
32+
</head>
33+
34+
<body>
35+
<main id="webchat"></main>
36+
<script type="text/babel">
37+
run(async function () {
38+
const {
39+
React,
40+
ReactDOM: { render },
41+
WebChat: {
42+
decorator: { DecoratorComposer },
43+
FluentThemeProvider,
44+
ReactWebChat
45+
}
46+
} = window; // Imports in UMD fashion.
47+
48+
function Flair({ children }) {
49+
return <div className="flair">{children}</div>;
50+
}
51+
52+
function Loader({ children }) {
53+
return <div className="loader">{children}</div>;
54+
}
55+
56+
function Actions({ children }) {
57+
return <div className="actions">{children}</div>;
58+
}
59+
60+
const decoratorMiddleware = [
61+
init =>
62+
init === 'activity border' &&
63+
(next => request => (request.livestreamingState === 'completing' ? Flair : next(request))),
64+
init =>
65+
init === 'activity border' &&
66+
(next => request => (request.livestreamingState === 'preparing' ? Loader : next(request))),
67+
init =>
68+
init === 'activity actions' &&
69+
(next => request => (request.activity.entities?.[0].keywords.includes('highlighted') ? Actions : next(request)))
70+
];
71+
72+
const { directLine, store } = testHelpers.createDirectLineEmulator();
73+
74+
const App = () => (
75+
<ReactWebChat
76+
directLine={directLine}
77+
store={store}
78+
styleOptions={{
79+
bubbleBorderRadius: 10,
80+
typingAnimationBackgroundImage: `url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAUACgDASIAAhEBAxEB/8QAGgABAQACAwAAAAAAAAAAAAAAAAYCBwMFCP/EACsQAAECBQIEBQUAAAAAAAAAAAECAwAEBQYRBxITIjFBMlFhccFScoGh8f/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwD0lctx023JVD9UeKOIcNoSNylkdcCMbauSmXHLOPUx8r4ZAcQtO1SM9Mj5iO1gtWo1syc7S2zMKYSptbIPNgnII8/5HBpRZ9RpaKjNVVCpUzLPAQ1nmA7qPl6fmAondRrcaqhkVTiiQrYXgglsH7vnpHc3DcNNoEimaqT4Q2s4bCRuUs+gEaLd05uNFVMmiS3o3YEwFDhlP1Z7e3WLzUuzahUKHRk0zM07TmeApvOFLGEjcM9+Xp6wFnbN0Uu5GnF0x4qW1je2tO1Sc9Djy9oRD6QWlU6PPzVSqjRlgtksttKPMcqBKiO3h/cIDacIQgEIQgEIQgP/2Q==')`
81+
}}
82+
/>
83+
);
84+
85+
render(
86+
<FluentThemeProvider>
87+
<DecoratorComposer middleware={decoratorMiddleware}>
88+
<App />
89+
</DecoratorComposer>
90+
</FluentThemeProvider>,
91+
document.getElementById('webchat')
92+
);
93+
94+
await pageConditions.uiConnected();
95+
96+
await directLine.emulateIncomingActivity({
97+
channelData: { streamSequence: 1, streamType: 'informative' },
98+
from: {
99+
id: 'u-00001',
100+
name: 'Bot',
101+
role: 'bot'
102+
},
103+
id: 't-00001',
104+
text: 'Working on it...',
105+
type: 'typing'
106+
});
107+
108+
await pageConditions.numActivitiesShown(1);
109+
await host.snapshot('local');
110+
111+
const attachments = [
112+
{
113+
content: {
114+
type: 'AdaptiveCard',
115+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
116+
version: '1.5',
117+
actions: [
118+
{ type: 'Action.Submit', title: 'Button 1' },
119+
{
120+
type: 'Action.ShowCard',
121+
title: 'Show card',
122+
card: {
123+
type: 'AdaptiveCard',
124+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
125+
version: '1.5',
126+
actions: [
127+
{ type: 'Action.Submit', title: 'Button 2' },
128+
{ type: 'Action.Submit', title: 'Button 3' }
129+
]
130+
}
131+
}
132+
]
133+
},
134+
contentType: 'application/vnd.microsoft.card.adaptive'
135+
}
136+
];
137+
await directLine.emulateIncomingActivity({
138+
attachments,
139+
channelData: { streamId: 't-00001', streamType: 'final' },
140+
from: {
141+
id: 'u-00001',
142+
name: 'Bot',
143+
role: 'bot'
144+
},
145+
id: 'm-00001',
146+
text: 'Work completed!'
147+
});
148+
149+
await pageConditions.numActivitiesShown(1);
150+
151+
152+
await directLine.emulateIncomingActivity({
153+
from: {
154+
role: "bot"
155+
},
156+
id: "a-00002",
157+
type: "message",
158+
text: "This is compleded feedback action example.",
159+
entities: [
160+
{
161+
'@context': 'https://schema.org',
162+
'@id': '',
163+
'@type': 'Message',
164+
type: 'https://schema.org/Message',
165+
keywords: ['AIGeneratedContent', 'AllowCopy', 'highlighted'],
166+
potentialAction: [
167+
{
168+
"@type": "LikeAction",
169+
actionStatus: "CompletedActionStatus",
170+
target: {
171+
"@type": "EntryPoint",
172+
urlTemplate: "ms-directline://postback?interaction=like"
173+
}
174+
},
175+
{
176+
"@type": "DislikeAction",
177+
actionStatus: "PotentialActionStatus",
178+
result: {
179+
"@type": "Review",
180+
reviewBody: "I don't like it.",
181+
"reviewBody-input": {
182+
"@type": "PropertyValueSpecification",
183+
valueMinLength: 3,
184+
valueName: "reason"
185+
}
186+
},
187+
target: {
188+
"@type": "EntryPoint",
189+
urlTemplate: "ms-directline://postback?interaction=dislike{&reason}"
190+
}
191+
}
192+
]
193+
}
194+
]
195+
});
196+
197+
await pageConditions.numActivitiesShown(2);
198+
199+
// THEN: Should render the activity.
200+
await host.snapshot('local');
201+
});
202+
</script>
203+
</body>
204+
205+
</html>
Loading
Loading

packages/api/src/decorator/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { DecoratorComposer } from './private/DecoratorComposer';
22
export { default as ActivityDecorator } from './private/ActivityDecorator';
3+
export { default as ActivityActionsDecorator } from './private/ActivityActionsDecorator';
34
export { type DecoratorMiddleware } from './private/createDecoratorComposer';
45
export { default as ActivityDecoratorRequest } from './private/activityDecoratorRequest';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { type WebChatActivity } from 'botframework-webchat-core';
2+
import React, { Fragment, memo, type ReactNode } from 'react';
3+
import { ActivityActionsDecoratorMiddlewareProxy } from './ActivityActionsDecoratorMiddleware';
4+
import useActivityDecoratorRequest from './useActivityDecoratorRequest';
5+
6+
const ActivityActionsDecoratorFallback = memo(({ children }) => <Fragment>{children}</Fragment>);
7+
8+
ActivityActionsDecoratorFallback.displayName = 'ActivityActionsDecoratorFallback';
9+
10+
function ActivityActionsDecorator({
11+
activity,
12+
children
13+
}: Readonly<{ activity?: WebChatActivity; children?: ReactNode }>) {
14+
const request = useActivityDecoratorRequest(activity);
15+
16+
return (
17+
<ActivityActionsDecoratorMiddlewareProxy fallbackComponent={ActivityActionsDecoratorFallback} request={request}>
18+
{children}
19+
</ActivityActionsDecoratorMiddlewareProxy>
20+
);
21+
}
22+
23+
export default memo(ActivityActionsDecorator);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { type EmptyObject } from 'type-fest';
2+
3+
import ActivityDecoratorRequest from './activityDecoratorRequest';
4+
import templateMiddleware from './templateMiddleware';
5+
6+
const {
7+
initMiddleware: initActivityActionsDecoratorMiddleware,
8+
Provider: ActivityActionsDecoratorMiddlewareProvider,
9+
Proxy: ActivityActionsDecoratorMiddlewareProxy,
10+
types
11+
} = templateMiddleware<typeof activityActionsDecoratorTypeName, ActivityDecoratorRequest, EmptyObject>(
12+
'ActivityActionsDecoratorMiddleware'
13+
);
14+
15+
type ActivityActionsDecoratorMiddleware = typeof types.middleware;
16+
type ActivityActionsDecoratorMiddlewareInit = typeof types.init;
17+
type ActivityActionsDecoratorMiddlewareProps = typeof types.props;
18+
type ActivityActionsDecoratorMiddlewareRequest = typeof types.request;
19+
20+
const activityActionsDecoratorTypeName = 'activity actions' as const;
21+
22+
export {
23+
ActivityActionsDecoratorMiddlewareProvider,
24+
ActivityActionsDecoratorMiddlewareProxy,
25+
activityActionsDecoratorTypeName,
26+
initActivityActionsDecoratorMiddleware,
27+
type ActivityActionsDecoratorMiddleware,
28+
type ActivityActionsDecoratorMiddlewareInit,
29+
type ActivityActionsDecoratorMiddlewareProps,
30+
type ActivityActionsDecoratorMiddlewareRequest
31+
};

packages/api/src/decorator/private/ActivityDecorator.tsx

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,14 @@
1-
import { getActivityLivestreamingMetadata, type WebChatActivity } from 'botframework-webchat-core';
2-
import React, { Fragment, memo, useMemo, type ReactNode } from 'react';
3-
import { ActivityDecoratorRequest } from '..';
1+
import { type WebChatActivity } from 'botframework-webchat-core';
2+
import React, { Fragment, memo, type ReactNode } from 'react';
43
import { ActivityBorderDecoratorMiddlewareProxy } from './ActivityBorderDecoratorMiddleware';
4+
import useActivityDecoratorRequest from './useActivityDecoratorRequest';
55

66
const ActivityDecoratorFallback = memo(({ children }) => <Fragment>{children}</Fragment>);
77

88
ActivityDecoratorFallback.displayName = 'ActivityDecoratorFallback';
99

10-
const supportedActivityRoles: ActivityDecoratorRequest['from'][] = ['bot', 'channel', 'user', undefined];
11-
1210
function ActivityDecorator({ activity, children }: Readonly<{ activity?: WebChatActivity; children?: ReactNode }>) {
13-
const request = useMemo<ActivityDecoratorRequest>(() => {
14-
const { type } = getActivityLivestreamingMetadata(activity) || {};
15-
16-
return {
17-
livestreamingState:
18-
type === 'final activity'
19-
? 'completing'
20-
: type === 'informative message'
21-
? 'preparing'
22-
: type === 'interim activity'
23-
? 'ongoing'
24-
: undefined,
25-
from: supportedActivityRoles.includes(activity?.from?.role) ? activity?.from?.role : undefined
26-
};
27-
}, [activity]);
11+
const request = useActivityDecoratorRequest(activity);
2812

2913
return (
3014
<ActivityBorderDecoratorMiddlewareProxy fallbackComponent={ActivityDecoratorFallback} request={request}>

packages/api/src/decorator/private/activityDecoratorRequest.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { type WebChatActivity } from 'botframework-webchat-core';
2+
13
type ActivityDecoratorRequestType = {
24
/**
35
* Decorate the activity as it participate in a livestreaming session.
@@ -18,6 +20,8 @@ type ActivityDecoratorRequestType = {
1820
* - `undefined` - the sender is unknown
1921
*/
2022
from: 'bot' | 'channel' | `user` | undefined;
23+
24+
activity: WebChatActivity;
2125
};
2226

2327
export default ActivityDecoratorRequestType;

packages/api/src/decorator/private/createDecoratorComposer.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ import {
55
initActivityBorderDecoratorMiddleware,
66
type ActivityBorderDecoratorMiddleware
77
} from './ActivityBorderDecoratorMiddleware';
8+
import {
9+
ActivityActionsDecoratorMiddleware,
10+
ActivityActionsDecoratorMiddlewareProvider,
11+
activityActionsDecoratorTypeName,
12+
initActivityActionsDecoratorMiddleware
13+
} from './ActivityActionsDecoratorMiddleware';
814

9-
type DecoratorMiddlewareInit = typeof activityBorderDecoratorTypeName;
15+
type DecoratorMiddlewareInit = typeof activityBorderDecoratorTypeName | typeof activityActionsDecoratorTypeName;
1016

1117
export type DecoratorComposerComponent = (
1218
props: Readonly<{
@@ -17,7 +23,7 @@ export type DecoratorComposerComponent = (
1723

1824
export type DecoratorMiddleware = (
1925
init: DecoratorMiddlewareInit
20-
) => ReturnType<ActivityBorderDecoratorMiddleware> | false;
26+
) => ReturnType<ActivityBorderDecoratorMiddleware | ActivityActionsDecoratorMiddleware> | false;
2127

2228
const EMPTY_ARRAY = [];
2329

@@ -28,9 +34,16 @@ export default (): DecoratorComposerComponent =>
2834
[middleware]
2935
);
3036

37+
const actionsMiddlewares = useMemo(
38+
() => initActivityActionsDecoratorMiddleware(middleware, activityActionsDecoratorTypeName),
39+
[middleware]
40+
);
41+
3142
return (
3243
<ActivityBorderDecoratorMiddlewareProvider middleware={borderMiddlewares}>
33-
{children}
44+
<ActivityActionsDecoratorMiddlewareProvider middleware={actionsMiddlewares}>
45+
{children}
46+
</ActivityActionsDecoratorMiddlewareProvider>
3447
</ActivityBorderDecoratorMiddlewareProvider>
3548
);
3649
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { WebChatActivity, getActivityLivestreamingMetadata } from 'botframework-webchat-core';
2+
import { useMemo } from 'react';
3+
import { ActivityDecoratorRequest } from '..';
4+
5+
const supportedActivityRoles: ActivityDecoratorRequest['from'][] = ['bot', 'channel', 'user', undefined];
6+
7+
export default function useActivityDecoratorRequest(activity: WebChatActivity) {
8+
return useMemo<ActivityDecoratorRequest>(() => {
9+
const { type } = getActivityLivestreamingMetadata(activity) || {};
10+
11+
return {
12+
activity,
13+
livestreamingState:
14+
type === 'final activity'
15+
? 'completing'
16+
: type === 'informative message'
17+
? 'preparing'
18+
: type === 'interim activity'
19+
? 'ongoing'
20+
: undefined,
21+
from: supportedActivityRoles.includes(activity?.from?.role) ? activity?.from?.role : undefined
22+
};
23+
}, [activity]);
24+
}

0 commit comments

Comments
 (0)