@telia-ace/widget-conversation 1.0.19 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,32 +58,34 @@ const agent = conversation.createAgent({
58
58
 
59
59
  ## Writing to the conversation
60
60
 
61
+ ***`print()` accepts an array, which is considered best practice when writing to the conversation. The previous way of posting an object is still currently be supported, but is considered to be deprecated.**
62
+
61
63
  ### Posting, updating and removing content
62
64
 
63
- When managing content in the conversation you'll first need to create an `Agent`-object and then use its `print()` function. There are serveral types of entries in order to accommedate different types of content.
65
+ When managing content in the conversation you'll first need to create an `Agent`-object and then use its `print()` function. There are serveral types of entries in order to accommodate different types of content.
64
66
 
65
- The `print(content: object)` function let's you return a `ConversationEntry` message that can be used to update and/or remove the content from the conversation.
67
+ The `print(items: ConversationMessageItem[])` function let's you return a `ConversationEntry` message that can be used to update and/or remove the content from the conversation.
66
68
 
67
69
  ##### Example of simple text message
68
70
 
69
71
  ```ts
70
72
  // print user message
71
- conversation.user.print('Lorem ipsum');
73
+ conversation.user.print([['text', 'Lorem ipsum']]);
72
74
 
73
75
  // print agent message
74
76
  const agent = conversation.createAgent();
75
- agent.print('Lorem ipsum');
77
+ agent.print([['text', 'Lorem ipsum']]);
76
78
  ```
77
79
 
78
80
  ##### Example of updating or removing content
79
81
 
80
82
  ```ts
81
83
  const agent = conversation.createAgent();
82
- const entry = agent.print('Hello world!');
84
+ const entry = agent.print([['text', 'Hello world!']]);
83
85
 
84
- entry.update({
86
+ entry.update([
85
87
  // ...
86
- });
88
+ ]);
87
89
 
88
90
  entry.remove();
89
91
  ```
@@ -92,24 +94,55 @@ entry.remove();
92
94
 
93
95
  You can post an entry into the conversation with actions. You can specify a title, body and available actions. This is only supported for the `Agent` and will be displayed as a system message.
94
96
 
95
- | Name | Type | Description |
96
- | --------- | -------- | ------------------------------------------------------------- |
97
- | `title` | `string` | Title for the entry. |
98
- | `body` | `string` | Body content for the entry. Supports HTML. |
99
- | `actions` | `object` | Key-value-pairs representing available actions for the entry. |
97
+ This can be either in the form of a "ButtonList" or an "LinkList" as demostrated below.
98
+
99
+ | Name | Type | Description |
100
+ | --------- | -------- | -------------------------------------------- |
101
+ | `header` | `string` | Header for the entry. |
102
+ | `actions` | `array` | Array of of available actions for the entry. |
100
103
 
101
104
  ##### Example of a list of actions
102
105
 
103
106
  ```ts
104
- agent.print({
105
- title: 'Download invoices',
106
- body: 'Click an invoice below to download a copy.',
107
- actions: {
108
- invoice_190201: 'Invoice 190201',
109
- invoice_190301: 'Invoice 190301',
110
- invoice_190401: 'Invoice 190401',
111
- },
112
- });
107
+ // example of button list
108
+ agent.print([
109
+ [
110
+ 'button-list',
111
+ {
112
+ header: 'Download invoices',
113
+ actions: [
114
+ {
115
+ actionKey: 'invoice_190201',
116
+ label: 'Invoice 190201',
117
+ },
118
+ {
119
+ actionKey: 'invoice_190301',
120
+ label: 'Invoice 190301',
121
+ },
122
+ ],
123
+ },
124
+ ],
125
+ ]);
126
+
127
+ // here we use a link list instead, but with the same actions
128
+ agent.print([
129
+ [
130
+ 'link-list',
131
+ {
132
+ header: 'Download invoices',
133
+ actions: [
134
+ {
135
+ actionKey: 'invoice_190201',
136
+ label: 'Invoice 190201',
137
+ },
138
+ {
139
+ actionKey: 'invoice_190301',
140
+ label: 'Invoice 190301',
141
+ },
142
+ ],
143
+ },
144
+ ],
145
+ ]);
113
146
  ```
114
147
 
115
148
  ### Entry with a form
@@ -152,6 +185,56 @@ agent.print({
152
185
  });
153
186
  ```
154
187
 
188
+ ### Entry with a video call
189
+
190
+ Use the `video-request` item type to create an entry representing an incoming video call. This entry can also have a form attached, which uses the `FormBuilder`-method and is handled by the `conversation.form` event. It is only supported for an `Agent` and will be displayed as a system message.
191
+
192
+ | Name | Type | Description |
193
+ | -------------- | -------- | ----------------------------------------------- |
194
+ | `header` | `string` | Header for the entry. |
195
+ | `acceptLabel` | `string` | Label to be shown for the "accept call button" |
196
+ | `declineLabel` | `string` | Label to be shown for the "decline call button" |
197
+
198
+ ##### Example of an entry for an incoming video call
199
+
200
+ ```ts
201
+ agent.print([
202
+ [
203
+ 'video-request',
204
+ {
205
+ header: 'Video call',
206
+ accept: 'Accept',
207
+ decline: 'Decline',
208
+ },
209
+ ],
210
+ ]);
211
+ ```
212
+
213
+ When the user presses one of these buttons to accept or decline the call, an action will be dispatched with actionKey `accept-video-call` or `decline-video-call`, depending on which button was clicked.
214
+
215
+ Listening to these actions can be done as following:
216
+
217
+ ```ts
218
+ component.actions.watch('conversation.action', (input, next) => {
219
+ if (input.actionKey === 'accept-video-call') {
220
+ // this will render an overlay over the widget,
221
+ // and prepare the widget for displaying the actual call
222
+ component.actions.dispatch('show-overlay', {});
223
+ } else if (input.actionKey === 'decline-video-call') {
224
+ // ...
225
+ }
226
+ return next(input);
227
+ });
228
+ ```
229
+
230
+ Use an overlay over the conversation to render your video call. The `show-overlay`-action, as described above, accomplishes this. After the overlay is rendered and ready it will dispatch another event: `overlay-mounted`. Use the HTML element this generates to render your video call.
231
+
232
+ ```ts
233
+ component.actions.watch('overlay-mounted', (overlayElem, next) => {
234
+ // the overlayElem here may be used to render the ongoing call
235
+ });
236
+ ```
237
+
155
238
  ### The loading and typing indicators
156
239
 
157
240
  When you need to fetch data from external resource you should use the `loading()` function on the `ConversationProvider` to inform the user that something is about to happen. Even in cases when the response is available immediately it gives a better user experience to present a loading indicator for a short while.
@@ -181,7 +264,7 @@ System message in the conversation are sent by using the `print` method on the `
181
264
  #### Example
182
265
 
183
266
  ```ts
184
- conversation.print('Hello world!');
267
+ conversation.print([['text', 'Hello world!']]);
185
268
  ```
186
269
 
187
270
  ## Reading from the conversation
@@ -4,5 +4,5 @@ declare type Props = {
4
4
  className?: string;
5
5
  agentData?: ConversationMessageSender;
6
6
  };
7
- declare const ConversationLoader: React.FC<Props>;
7
+ declare const ConversationLoader: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLDivElement>>;
8
8
  export default ConversationLoader;
@@ -0,0 +1,8 @@
1
+ import { DispatchAction } from '@telia-ace/widget-ui';
2
+ import { FC } from 'react';
3
+ import { VideoRequestProperties } from '../../../../types';
4
+ declare type Props = {
5
+ dispatch: DispatchAction;
6
+ } & VideoRequestProperties;
7
+ export declare const VideoRequest: FC<Props>;
8
+ export {};
@@ -29,9 +29,9 @@ var __objRest = (source, exclude) => {
29
29
  }
30
30
  return target;
31
31
  };
32
- import { Input, Button, borderTabStyle, SymbolBadge, useDispatch, useProperties, useScroll, Typography, invertedHoverButton, List, buttonCss, Html as Html$1, genericHtml, Lightbox, ItemList, itemList, useComponent, list, headingElement, usePrevious, useTransitionEnd, WidgetOverlayOutlet } from "@telia-ace/widget-ui";
32
+ import { Input, Button, borderTabStyle, SymbolBadge, useDispatch, useProperties, useScroll, Typography, invertedHoverButton, List, buttonCss, Html as Html$1, genericHtml, ItemList, itemList, useComponent, list, headingElement, WidgetOverlayOutlet } from "@telia-ace/widget-ui";
33
33
  import { appendClassNames, uuid } from "@telia-ace/widget-utilities";
34
- import React, { useState, useEffect, Suspense, useCallback, useRef } from "react";
34
+ import React, { useState, useEffect, forwardRef, Suspense, useMemo, useCallback, useRef } from "react";
35
35
  import styled, { css, keyframes } from "styled-components";
36
36
  import { FormComponent } from "@telia-ace/widget-forms";
37
37
  import { ConversationMessageListItemType, ConversationMessageType } from "./index.js";
@@ -146,7 +146,7 @@ const ConversationInput = ({ className }) => {
146
146
  "aria-label": inputPlaceholder,
147
147
  placeholder: inputPlaceholder,
148
148
  onFocusChange: setInputFocus
149
- }), /* @__PURE__ */ React.createElement(StyledButton, {
149
+ }), /* @__PURE__ */ React.createElement(StyledButton$1, {
150
150
  disabled: inputDisabled,
151
151
  onClick: submit,
152
152
  title: sendButtonLabel,
@@ -205,7 +205,7 @@ const StyledInput = styled(Input)`
205
205
  font-style: italic;
206
206
  }
207
207
  `;
208
- const StyledButton = styled(Button)`
208
+ const StyledButton$1 = styled(Button)`
209
209
  flex: 0 0 50px;
210
210
  align-self: stretch;
211
211
  background-color: transparent;
@@ -221,7 +221,7 @@ const StyledButton = styled(Button)`
221
221
  }};
222
222
  }
223
223
  `;
224
- const SecondaryActionButton = styled(StyledButton)`
224
+ const SecondaryActionButton = styled(StyledButton$1)`
225
225
  border-right: ${(p) => p.theme.border};
226
226
  margin: ${(p) => {
227
227
  var _a;
@@ -334,6 +334,10 @@ const LoaderAnimation = keyframes`
334
334
  const Loader = styled.div`
335
335
  display: flex;
336
336
  justify-content: center;
337
+ padding: 0 0 ${(p) => {
338
+ var _a;
339
+ return (_a = p.theme.sizes) == null ? void 0 : _a.normal;
340
+ }};
337
341
  > div {
338
342
  height: 10px;
339
343
  width: 10px;
@@ -355,6 +359,7 @@ const Loader = styled.div`
355
359
  const ContentBundle = styled.div`
356
360
  grid-area: content;
357
361
  max-width: 100%;
362
+ overflow: hidden;
358
363
  `;
359
364
  const MessageTitle = styled(Typography)`
360
365
  font-size: ${(p) => {
@@ -544,21 +549,14 @@ const StyledActionButton = styled.button`
544
549
  }}
545
550
  }
546
551
  `;
547
- const ConversationLoader = ({ className, agentData }) => {
548
- return /* @__PURE__ */ React.createElement(ContentBundle, null, /* @__PURE__ */ React.createElement(MessageContainer, null, /* @__PURE__ */ React.createElement(MessageContent, null, /* @__PURE__ */ React.createElement(Loader, null, /* @__PURE__ */ React.createElement("div", null), /* @__PURE__ */ React.createElement("div", null), /* @__PURE__ */ React.createElement("div", null)))));
549
- };
550
- const imageLightboxRenderer = (attrs) => /* @__PURE__ */ React.createElement(Lightbox, {
551
- attrs
552
+ const ConversationLoader = forwardRef(({ className, agentData }, ref) => {
553
+ return /* @__PURE__ */ React.createElement(ContentBundle, {
554
+ ref
555
+ }, /* @__PURE__ */ React.createElement(MessageContainer, null, /* @__PURE__ */ React.createElement(MessageContent, null, /* @__PURE__ */ React.createElement(Loader, null, /* @__PURE__ */ React.createElement("div", null), /* @__PURE__ */ React.createElement("div", null), /* @__PURE__ */ React.createElement("div", null)))));
552
556
  });
553
- const htmlParseInstructions = {
554
- transform: {
555
- img: imageLightboxRenderer
556
- }
557
- };
558
557
  const Html = ({ body }) => {
559
558
  return /* @__PURE__ */ React.createElement(StyledHtml, {
560
- html: body,
561
- htmlParseInstruction: htmlParseInstructions
559
+ html: body
562
560
  });
563
561
  };
564
562
  const StyledHtml = styled(Html$1)`
@@ -835,6 +833,9 @@ const StyledLinkButton = styled(Button)`
835
833
  var _a;
836
834
  return (_a = p.theme.colors) == null ? void 0 : _a.primary;
837
835
  }};
836
+ max-width: 100%;
837
+ text-overflow: ellipsis;
838
+ overflow: hidden;
838
839
 
839
840
  ${(p) => {
840
841
  var _a, _b;
@@ -877,6 +878,114 @@ const Text = ({ text }) => {
877
878
  const StyledTypography = styled(Typography)`
878
879
  ${messageContentPadding}
879
880
  `;
881
+ const VideoRequest = ({
882
+ header = "Video call",
883
+ acceptLabel = "Accept",
884
+ declineLabel = "Decline",
885
+ dispatch
886
+ }) => {
887
+ const actions = useMemo(() => {
888
+ return [
889
+ {
890
+ label: acceptLabel,
891
+ actionKey: "accept-video-call",
892
+ symbol: {
893
+ type: "Svg",
894
+ content: "video"
895
+ }
896
+ },
897
+ {
898
+ label: declineLabel,
899
+ actionKey: "decline-video-call",
900
+ symbol: {
901
+ type: "Svg",
902
+ content: "close"
903
+ }
904
+ }
905
+ ];
906
+ }, [acceptLabel, declineLabel]);
907
+ return /* @__PURE__ */ React.createElement(StyledItemList, {
908
+ header,
909
+ items: actions,
910
+ renderItem: (item) => {
911
+ const bgColor = item.actionKey === "accept-video-call" ? "#70c48e" : "#da5c85";
912
+ return /* @__PURE__ */ React.createElement("li", {
913
+ key: item.actionKey
914
+ }, /* @__PURE__ */ React.createElement(StyledButton, {
915
+ onClick: () => {
916
+ dispatch("action", item);
917
+ },
918
+ "aria-label": item.label,
919
+ title: item.label,
920
+ bgColor
921
+ }, /* @__PURE__ */ React.createElement(SymbolBadge, {
922
+ symbol: item.symbol,
923
+ size: 25
924
+ })));
925
+ }
926
+ });
927
+ };
928
+ const StyledItemList = styled(ItemList)`
929
+ ${itemList}
930
+ > ul {
931
+ display: flex;
932
+ flex-wrap: wrap;
933
+ gap: ${(p) => {
934
+ var _a;
935
+ return (_a = p.theme.sizes) == null ? void 0 : _a.normal;
936
+ }};
937
+ > li {
938
+ margin: 0;
939
+ }
940
+ }
941
+ ${messageContentPadding}
942
+ `;
943
+ const StyledButton = styled(Button)`
944
+ ${buttonCss};
945
+
946
+ background-color: ${(p) => p.bgColor};
947
+ display: flex;
948
+ justify-content: center;
949
+ align-items: center;
950
+ color: #ffffff;
951
+ width: 46px;
952
+ height: 46px;
953
+ border-radius: 100%;
954
+
955
+ &:focus {
956
+ ${(p) => {
957
+ var _a;
958
+ return ((_a = p.theme.accessibility) == null ? void 0 : _a.isTabbing) && css`
959
+ ${borderTabStyle}
960
+ background-color: transparent;
961
+
962
+ i,
963
+ svg {
964
+ color: ${(p2) => {
965
+ var _a2;
966
+ return (_a2 = p2.theme.colors) == null ? void 0 : _a2.primary;
967
+ }};
968
+
969
+ path {
970
+ stroke: ${(p2) => {
971
+ var _a2;
972
+ return (_a2 = p2.theme.colors) == null ? void 0 : _a2.primary;
973
+ }};
974
+ }
975
+ }
976
+ `;
977
+ }}
978
+ }
979
+
980
+ i,
981
+ svg {
982
+ color: #fff;
983
+
984
+ path {
985
+ stroke: #fff;
986
+ }
987
+ }
988
+ `;
880
989
  const mapItem = ([type, content]) => {
881
990
  return { type, content, id: uuid() };
882
991
  };
@@ -907,6 +1016,11 @@ const MessageList = ({ items, dispatch }) => {
907
1016
  dispatch
908
1017
  }));
909
1018
  }
1019
+ case ConversationMessageListItemType.VideoRequest: {
1020
+ return /* @__PURE__ */ React.createElement(VideoRequest, __spreadProps(__spreadValues({}, item.content), {
1021
+ dispatch
1022
+ }));
1023
+ }
910
1024
  default: {
911
1025
  return /* @__PURE__ */ React.createElement(Custom, {
912
1026
  type: item.type,
@@ -1036,56 +1150,31 @@ const Message = ({ content, isUser, stacked }) => {
1036
1150
  }));
1037
1151
  };
1038
1152
  const ConversationContent = () => {
1039
- const lastEntry = useRef(document.createElement("div"));
1153
+ const lastEntry = useRef(null);
1040
1154
  const [scrollableCss, viewElem] = useScroll(true);
1041
1155
  const { messages, loading, typingActors } = useProperties();
1042
- const [canSmooth, setCanSmooth] = useState(false);
1043
- const previousMessages = usePrevious(messages);
1044
- const previousTypingActors = usePrevious(typingActors);
1045
- const scrollView = useCallback((lastEntry2, smooth = true) => {
1046
- if (lastEntry2 !== null && lastEntry2.current !== null) {
1047
- if (messages && messages.length === 1) {
1048
- lastEntry2.current.focus();
1049
- }
1050
- if (lastEntry2.current.id.indexOf("typing_") !== -1 && viewElem.current.scrollTopMax - viewElem.current.scrollTop > lastEntry2.current.clientHeight + 100) {
1051
- return;
1052
- }
1053
- setTimeout(() => {
1054
- if (lastEntry2.current) {
1055
- lastEntry2.current.scrollIntoView({
1056
- block: "end",
1057
- behavior: smooth ? "smooth" : "auto"
1058
- });
1059
- }
1060
- }, 150);
1156
+ const scrollView = useCallback(() => {
1157
+ if (lastEntry.current) {
1158
+ lastEntry.current.scrollIntoView({
1159
+ block: "end",
1160
+ behavior: "smooth"
1161
+ });
1061
1162
  }
1062
1163
  }, [lastEntry]);
1063
1164
  useEffect(() => {
1064
- if (canSmooth) {
1065
- const messageCountChanged = messages.length !== (previousMessages == null ? void 0 : previousMessages.length);
1066
- const typingActorCountChanged = typingActors.length !== (previousTypingActors == null ? void 0 : previousTypingActors.length);
1067
- const messageContentCountChanged = messages.reduce((changed, message, index) => {
1068
- var _a;
1069
- if (((_a = previousMessages == null ? void 0 : previousMessages[index]) == null ? void 0 : _a.content.length) !== message.content.length) {
1070
- return true;
1071
- }
1072
- return changed;
1073
- }, false);
1074
- if (messageCountChanged || messageContentCountChanged || typingActorCountChanged) {
1075
- scrollView(lastEntry);
1076
- }
1077
- }
1165
+ const handler = setTimeout(scrollView, 250);
1166
+ return () => clearTimeout(handler);
1078
1167
  }, [messages, typingActors]);
1079
- useTransitionEnd(() => {
1080
- setCanSmooth(true);
1081
- scrollView(lastEntry, false);
1082
- });
1168
+ const isLoadingOrTyping = useMemo(() => {
1169
+ return !!typingActors.length || loading;
1170
+ }, [typingActors, loading]);
1083
1171
  const renderTypingActors = () => {
1084
1172
  if (typingActors && typingActors.length) {
1085
1173
  return typingActors.map((actor, index) => {
1174
+ const isLast = index === typingActors.length - 1;
1086
1175
  return /* @__PURE__ */ React.createElement(MessageEntry, {
1087
1176
  id: `typing_${actor.name}`,
1088
- ref: typingActors.length ? lastEntry : null,
1177
+ ref: loading ? null : isLast ? lastEntry : null,
1089
1178
  key: `typing_${actor.name}`
1090
1179
  }, /* @__PURE__ */ React.createElement(AgentMessage, {
1091
1180
  timestamp: Date.now(),
@@ -1101,13 +1190,16 @@ const ConversationContent = () => {
1101
1190
  ref: viewElem
1102
1191
  }, messages && messages.length ? messages.map((message, index) => {
1103
1192
  const { type, timestamp, sender, content } = message;
1193
+ const isLast = index === messages.length - 1;
1104
1194
  return /* @__PURE__ */ React.createElement(MessageEntry, {
1105
1195
  className: "humany-conversation-entry",
1106
1196
  tabIndex: -1,
1107
1197
  key: `${type}_${sender == null ? void 0 : sender.name}_${timestamp}_${content[0].key}`,
1108
- ref: index + 1 < messages.length ? null : typingActors.length ? null : lastEntry
1198
+ ref: isLoadingOrTyping ? null : isLast ? lastEntry : null
1109
1199
  }, /* @__PURE__ */ React.createElement(ConversationMessage, __spreadValues({}, message)));
1110
- }) : null, renderTypingActors(), loading && /* @__PURE__ */ React.createElement(MessageEntry, null, /* @__PURE__ */ React.createElement(ConversationLoader, null))), /* @__PURE__ */ React.createElement(ConversationInput, null));
1200
+ }) : null, renderTypingActors(), loading && /* @__PURE__ */ React.createElement(MessageEntry, null, /* @__PURE__ */ React.createElement(ConversationLoader, {
1201
+ ref: lastEntry
1202
+ }))), /* @__PURE__ */ React.createElement(ConversationInput, null));
1111
1203
  };
1112
1204
  const Scrollable = styled.div`
1113
1205
  display: flex;