botframework-webchat-core 4.14.1 → 4.15.2-main.20220413.af6e8a3

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.
Files changed (238) hide show
  1. package/.eslintrc.yml +4 -198
  2. package/.prettierrc.yml +1 -1
  3. package/babel.config.json +1 -1
  4. package/lib/actions/clearSuggestedActions.js +1 -1
  5. package/lib/actions/connect.js +1 -1
  6. package/lib/actions/connectionStatusUpdate.js +1 -1
  7. package/lib/actions/deleteActivity.d.ts +13 -0
  8. package/lib/actions/deleteActivity.d.ts.map +1 -0
  9. package/lib/actions/deleteActivity.js +5 -3
  10. package/lib/actions/disconnect.js +1 -1
  11. package/lib/actions/dismissNotification.js +1 -1
  12. package/lib/actions/emitTypingIndicator.js +1 -1
  13. package/lib/actions/incomingActivity.d.ts +14 -0
  14. package/lib/actions/incomingActivity.d.ts.map +1 -0
  15. package/lib/actions/incomingActivity.js +5 -3
  16. package/lib/actions/markActivity.d.ts +17 -0
  17. package/lib/actions/markActivity.d.ts.map +1 -0
  18. package/lib/actions/markActivity.js +5 -3
  19. package/lib/actions/postActivity.d.ts +52 -0
  20. package/lib/actions/postActivity.d.ts.map +1 -0
  21. package/lib/actions/postActivity.js +5 -3
  22. package/lib/actions/queueIncomingActivity.js +1 -1
  23. package/lib/actions/sagaError.js +1 -1
  24. package/lib/actions/sendEvent.js +1 -1
  25. package/lib/actions/sendFiles.js +1 -1
  26. package/lib/actions/sendMessage.js +1 -1
  27. package/lib/actions/sendMessageBack.js +1 -1
  28. package/lib/actions/sendPostBack.js +1 -1
  29. package/lib/actions/setDictateInterims.js +1 -1
  30. package/lib/actions/setDictateState.js +1 -1
  31. package/lib/actions/setLanguage.js +1 -1
  32. package/lib/actions/setNotification.js +2 -2
  33. package/lib/actions/setReferenceGrammarID.js +1 -1
  34. package/lib/actions/setSendBox.js +1 -1
  35. package/lib/actions/setSendTimeout.js +1 -1
  36. package/lib/actions/setSendTypingIndicator.js +1 -1
  37. package/lib/actions/setSuggestedActions.js +1 -1
  38. package/lib/actions/startDictate.js +1 -1
  39. package/lib/actions/startSpeakingActivity.js +1 -1
  40. package/lib/actions/stopDictate.js +1 -1
  41. package/lib/actions/stopSpeakingActivity.js +1 -1
  42. package/lib/actions/submitSendBox.js +1 -1
  43. package/lib/actions/updateConnectionStatus.js +1 -1
  44. package/lib/constants/ActivityClientState.js +1 -1
  45. package/lib/createStore.d.ts.map +1 -1
  46. package/lib/createStore.js +5 -5
  47. package/lib/definitions/speakingActivity.js +4 -2
  48. package/lib/index.d.ts +17 -15
  49. package/lib/index.d.ts.map +1 -1
  50. package/lib/index.js +12 -3
  51. package/lib/reducer.d.ts +1 -2
  52. package/lib/reducer.d.ts.map +1 -1
  53. package/lib/reducer.js +1 -4
  54. package/lib/reducers/activities.d.ts +10 -0
  55. package/lib/reducers/activities.d.ts.map +1 -0
  56. package/lib/reducers/activities.js +65 -45
  57. package/lib/reducers/notifications.js +21 -14
  58. package/lib/reducers/typing.js +13 -7
  59. package/lib/sagas/connectionStatusToNotificationSaga.js +1 -1
  60. package/lib/sagas/effects/observeOnce.js +1 -1
  61. package/lib/sagas/effects/whileConnected.js +1 -1
  62. package/lib/sagas/observeActivitySaga.js +21 -3
  63. package/lib/sagas/postActivitySaga.js +51 -72
  64. package/lib/sagas/queueIncomingActivitySaga.js +1 -1
  65. package/lib/sagas/sendFilesToPostActivitySaga.js +4 -4
  66. package/lib/sagas/sendMessageBackToPostActivitySaga.js +1 -1
  67. package/lib/sagas/sendTypingIndicatorOnSetSendBoxSaga.js +1 -1
  68. package/lib/sagas/speakActivityAndStartDictateOnIncomingActivityFromOthersSaga.js +1 -1
  69. package/lib/sagas/startSpeakActivityOnPostActivitySaga.js +1 -1
  70. package/lib/sagas/stopSpeakingActivityOnInputSaga.js +1 -1
  71. package/lib/sagas/submitSendBoxSaga.js +1 -1
  72. package/lib/selectors/activities.js +1 -1
  73. package/lib/selectors/combineSelectors.js +9 -4
  74. package/lib/selectors/dictateState.js +1 -1
  75. package/lib/selectors/language.js +1 -1
  76. package/lib/selectors/notifications.js +1 -1
  77. package/lib/selectors/sendBoxValue.js +1 -1
  78. package/lib/selectors/sendTimeout.js +1 -1
  79. package/lib/selectors/sendTypingIndicator.js +1 -1
  80. package/lib/selectors/shouldSpeakIncomingActivity.js +1 -1
  81. package/lib/types/AnyAnd.d.ts +2 -0
  82. package/lib/types/AnyAnd.d.ts.map +1 -0
  83. package/lib/types/AnyAnd.js +6 -0
  84. package/lib/types/OneOrMany.js +4 -0
  85. package/lib/types/WebChatActivity.d.ts +81 -0
  86. package/lib/types/WebChatActivity.d.ts.map +1 -0
  87. package/lib/types/WebChatActivity.js +6 -0
  88. package/lib/types/external/DirectLineActivity.d.ts +2 -2
  89. package/lib/types/external/DirectLineActivity.d.ts.map +1 -1
  90. package/lib/types/external/DirectLineActivity.js +4 -0
  91. package/lib/types/external/DirectLineAnimationCard.d.ts +5 -2
  92. package/lib/types/external/DirectLineAnimationCard.d.ts.map +1 -1
  93. package/lib/types/external/DirectLineAnimationCard.js +4 -0
  94. package/lib/types/external/DirectLineAttachment.d.ts +8 -2
  95. package/lib/types/external/DirectLineAttachment.d.ts.map +1 -1
  96. package/lib/types/external/DirectLineAttachment.js +4 -0
  97. package/lib/types/external/DirectLineAudioCard.d.ts +5 -2
  98. package/lib/types/external/DirectLineAudioCard.d.ts.map +1 -1
  99. package/lib/types/external/DirectLineAudioCard.js +4 -0
  100. package/lib/types/external/DirectLineBasicCardEssence.d.ts +12 -0
  101. package/lib/types/external/DirectLineBasicCardEssence.d.ts.map +1 -0
  102. package/lib/types/external/DirectLineBasicCardEssence.js +6 -0
  103. package/lib/types/external/DirectLineCardAction.d.ts +1 -1
  104. package/lib/types/external/DirectLineCardAction.d.ts.map +1 -1
  105. package/lib/types/external/DirectLineCardAction.js +4 -0
  106. package/lib/types/external/DirectLineCardImage.d.ts +8 -0
  107. package/lib/types/external/DirectLineCardImage.d.ts.map +1 -0
  108. package/lib/types/external/DirectLineCardImage.js +6 -0
  109. package/lib/types/external/DirectLineHeroCard.d.ts +5 -2
  110. package/lib/types/external/DirectLineHeroCard.d.ts.map +1 -1
  111. package/lib/types/external/DirectLineHeroCard.js +4 -0
  112. package/lib/types/external/DirectLineJSBotConnection.d.ts +1 -1
  113. package/lib/types/external/DirectLineJSBotConnection.d.ts.map +1 -1
  114. package/lib/types/external/DirectLineJSBotConnection.js +4 -0
  115. package/lib/types/external/DirectLineMediaCardEssence.d.ts +21 -0
  116. package/lib/types/external/DirectLineMediaCardEssence.d.ts.map +1 -0
  117. package/lib/types/external/DirectLineMediaCardEssence.js +6 -0
  118. package/lib/types/external/DirectLineOAuthCard.d.ts +7 -2
  119. package/lib/types/external/DirectLineOAuthCard.d.ts.map +1 -1
  120. package/lib/types/external/DirectLineOAuthCard.js +4 -0
  121. package/lib/types/external/DirectLineReceiptCard.d.ts +27 -2
  122. package/lib/types/external/DirectLineReceiptCard.d.ts.map +1 -1
  123. package/lib/types/external/DirectLineReceiptCard.js +4 -0
  124. package/lib/types/external/DirectLineSignInCard.d.ts +7 -2
  125. package/lib/types/external/DirectLineSignInCard.d.ts.map +1 -1
  126. package/lib/types/external/DirectLineSignInCard.js +4 -0
  127. package/lib/types/external/DirectLineSuggestedAction.d.ts +6 -2
  128. package/lib/types/external/DirectLineSuggestedAction.d.ts.map +1 -1
  129. package/lib/types/external/DirectLineSuggestedAction.js +4 -0
  130. package/lib/types/external/DirectLineThumbnailCard.d.ts +5 -2
  131. package/lib/types/external/DirectLineThumbnailCard.d.ts.map +1 -1
  132. package/lib/types/external/DirectLineThumbnailCard.js +4 -0
  133. package/lib/types/external/DirectLineVideoCard.d.ts +5 -2
  134. package/lib/types/external/DirectLineVideoCard.d.ts.map +1 -1
  135. package/lib/types/external/DirectLineVideoCard.js +4 -0
  136. package/lib/types/external/Observable.js +6 -0
  137. package/lib/types/internal/Notification.js +6 -0
  138. package/lib/types/internal/ReduxState.js +6 -0
  139. package/lib/utils/dateToLocaleISOString.js +5 -5
  140. package/lib/utils/deleteKey.js +2 -3
  141. package/lib/utils/isForbiddenPropertyName.d.ts +2 -0
  142. package/lib/utils/isForbiddenPropertyName.d.ts.map +1 -0
  143. package/lib/utils/isForbiddenPropertyName.js +21 -0
  144. package/lib/utils/sleep.js +1 -1
  145. package/lib/utils/uniqueID.js +1 -1
  146. package/package.json +20 -23
  147. package/src/__tests__/detectSlowConnectionSaga.spec.js +1 -1
  148. package/src/__tests__/observeOnce.spec.js +3 -3
  149. package/src/actions/deleteActivity.ts +19 -0
  150. package/src/actions/incomingActivity.ts +21 -0
  151. package/src/actions/markActivity.ts +23 -0
  152. package/src/actions/postActivity.ts +48 -0
  153. package/src/actions/setNotification.js +8 -2
  154. package/src/createStore.ts +4 -4
  155. package/src/definitions/speakingActivity.js +1 -1
  156. package/src/index.ts +19 -14
  157. package/src/reducer.ts +0 -2
  158. package/src/reducers/activities.ts +172 -0
  159. package/src/reducers/notifications.js +22 -16
  160. package/src/reducers/typing.js +6 -5
  161. package/src/sagas/connectionStatusToNotificationSaga.js +1 -1
  162. package/src/sagas/effects/{observeOnce.js → observeOnce.ts} +6 -4
  163. package/src/sagas/effects/{whileConnected.js → whileConnected.ts} +20 -1
  164. package/src/sagas/{observeActivitySaga.js → observeActivitySaga.ts} +25 -6
  165. package/src/sagas/{postActivitySaga.js → postActivitySaga.ts} +57 -48
  166. package/src/sagas/queueIncomingActivitySaga.js +40 -39
  167. package/src/sagas/sendFilesToPostActivitySaga.js +1 -1
  168. package/src/sagas/sendMessageBackToPostActivitySaga.js +0 -1
  169. package/src/sagas/sendTypingIndicatorOnSetSendBoxSaga.js +1 -1
  170. package/src/sagas/speakActivityAndStartDictateOnIncomingActivityFromOthersSaga.js +1 -1
  171. package/src/sagas/startSpeakActivityOnPostActivitySaga.js +1 -1
  172. package/src/sagas/stopSpeakingActivityOnInputSaga.js +1 -1
  173. package/src/sagas/submitSendBoxSaga.js +1 -1
  174. package/src/selectors/activities.ts +12 -0
  175. package/src/selectors/combineSelectors.ts +21 -0
  176. package/src/selectors/dictateState.ts +3 -0
  177. package/src/selectors/language.ts +3 -0
  178. package/src/selectors/notifications.ts +6 -0
  179. package/src/selectors/sendBoxValue.ts +3 -0
  180. package/src/selectors/sendTimeout.ts +3 -0
  181. package/src/selectors/sendTypingIndicator.ts +3 -0
  182. package/src/selectors/shouldSpeakIncomingActivity.ts +3 -0
  183. package/src/types/AnyAnd.ts +1 -0
  184. package/src/types/WebChatActivity.ts +154 -0
  185. package/src/types/external/DirectLineActivity.ts +4 -3
  186. package/src/types/external/DirectLineAnimationCard.ts +6 -4
  187. package/src/types/external/DirectLineAttachment.ts +10 -5
  188. package/src/types/external/DirectLineAudioCard.ts +6 -4
  189. package/src/types/external/DirectLineBasicCardEssence.ts +14 -0
  190. package/src/types/external/DirectLineCardAction.ts +1 -1
  191. package/src/types/external/DirectLineCardImage.ts +9 -0
  192. package/src/types/external/DirectLineHeroCard.ts +6 -4
  193. package/src/types/external/DirectLineJSBotConnection.ts +1 -2
  194. package/src/types/external/DirectLineMediaCardEssence.ts +19 -0
  195. package/src/types/external/DirectLineOAuthCard.ts +7 -4
  196. package/src/types/external/DirectLineReceiptCard.ts +30 -4
  197. package/src/types/external/DirectLineSignInCard.ts +8 -4
  198. package/src/types/external/DirectLineSuggestedAction.ts +6 -4
  199. package/src/types/external/DirectLineThumbnailCard.ts +6 -4
  200. package/src/types/external/DirectLineVideoCard.ts +6 -4
  201. package/src/types/external/Observable.ts +69 -0
  202. package/src/types/internal/Notification.ts +10 -0
  203. package/src/types/internal/ReduxState.ts +16 -0
  204. package/src/utils/dateToLocaleISOString.chatham.spec.js +1 -0
  205. package/src/utils/dateToLocaleISOString.japan.spec.js +1 -0
  206. package/src/utils/dateToLocaleISOString.pacific.spec.js +1 -0
  207. package/src/utils/{dateToLocaleISOString.js → dateToLocaleISOString.ts} +6 -6
  208. package/src/utils/dateToLocaleISOString.utc.spec.js +2 -0
  209. package/src/utils/deleteKey.ts +9 -0
  210. package/src/utils/isForbiddenPropertyName.spec.js +6 -0
  211. package/src/utils/isForbiddenPropertyName.ts +33 -0
  212. package/src/utils/{sleep.js → sleep.ts} +1 -1
  213. package/src/utils/uniqueID.ts +7 -0
  214. package/.eslintignore +0 -9
  215. package/lib/reducers/clockSkewAdjustment.js +0 -44
  216. package/lib/sagas/effects/callUntil.js +0 -48
  217. package/lib/selectors/clockSkewAdjustment.js +0 -14
  218. package/lib/utils/mime-wrapper.js +0 -49
  219. package/src/actions/deleteActivity.js +0 -8
  220. package/src/actions/incomingActivity.js +0 -10
  221. package/src/actions/markActivity.js +0 -14
  222. package/src/actions/postActivity.js +0 -14
  223. package/src/reducers/activities.js +0 -116
  224. package/src/reducers/clockSkewAdjustment.js +0 -29
  225. package/src/sagas/effects/callUntil.js +0 -13
  226. package/src/selectors/activities.js +0 -8
  227. package/src/selectors/clockSkewAdjustment.js +0 -1
  228. package/src/selectors/combineSelectors.js +0 -8
  229. package/src/selectors/dictateState.js +0 -1
  230. package/src/selectors/language.js +0 -1
  231. package/src/selectors/notifications.js +0 -3
  232. package/src/selectors/sendBoxValue.js +0 -1
  233. package/src/selectors/sendTimeout.js +0 -1
  234. package/src/selectors/sendTypingIndicator.js +0 -1
  235. package/src/selectors/shouldSpeakIncomingActivity.js +0 -1
  236. package/src/utils/deleteKey.js +0 -11
  237. package/src/utils/mime-wrapper.js +0 -40
  238. package/src/utils/uniqueID.js +0 -12
@@ -0,0 +1,172 @@
1
+ /* eslint no-magic-numbers: ["error", { "ignore": [0, 1, -1] }] */
2
+
3
+ import updateIn from 'simple-update-in';
4
+
5
+ import { DELETE_ACTIVITY } from '../actions/deleteActivity';
6
+ import { INCOMING_ACTIVITY } from '../actions/incomingActivity';
7
+ import { MARK_ACTIVITY } from '../actions/markActivity';
8
+ import { POST_ACTIVITY_FULFILLED, POST_ACTIVITY_PENDING, POST_ACTIVITY_REJECTED } from '../actions/postActivity';
9
+ import { SEND_FAILED, SENDING, SENT } from '../constants/ActivityClientState';
10
+ import type { DeleteActivityAction } from '../actions/deleteActivity';
11
+ import type { IncomingActivityAction } from '../actions/incomingActivity';
12
+ import type { MarkActivityAction } from '../actions/markActivity';
13
+ import type {
14
+ PostActivityFulfilledAction,
15
+ PostActivityPendingAction,
16
+ PostActivityRejectedAction
17
+ } from '../actions/postActivity';
18
+ import type { WebChatActivity } from '../types/WebChatActivity';
19
+
20
+ type ActivitiesAction =
21
+ | DeleteActivityAction
22
+ | IncomingActivityAction
23
+ | MarkActivityAction
24
+ | PostActivityFulfilledAction
25
+ | PostActivityPendingAction
26
+ | PostActivityRejectedAction;
27
+
28
+ type ActivitiesStateType = WebChatActivity[];
29
+
30
+ const DEFAULT_STATE: ActivitiesStateType = [];
31
+ const DIRECT_LINE_PLACEHOLDER_URL =
32
+ 'https://docs.botframework.com/static/devportal/client/images/bot-framework-default-placeholder.png';
33
+
34
+ function getClientActivityID(activity: WebChatActivity): string | undefined {
35
+ return activity.channelData?.clientActivityID;
36
+ }
37
+
38
+ function findByClientActivityID(clientActivityID: string): (activity: WebChatActivity) => boolean {
39
+ return (activity: WebChatActivity) => getClientActivityID(activity) === clientActivityID;
40
+ }
41
+
42
+ function patchActivity(activity: WebChatActivity, lastActivity: WebChatActivity): WebChatActivity {
43
+ // Direct Line channel will return a placeholder image for the user-uploaded image.
44
+ // As observed, the URL for the placeholder image is https://docs.botframework.com/static/devportal/client/images/bot-framework-default-placeholder.png.
45
+ // To make our code simpler, we are removing the value if "contentUrl" is pointing to a placeholder image.
46
+
47
+ // TODO: [P2] #2869 This "contentURL" removal code should be moved to DirectLineJS adapter.
48
+
49
+ // Also, if the "contentURL" starts with "blob:", this means the user is uploading a file (the URL is constructed by URL.createObjectURL)
50
+ // Although the copy/reference of the file is temporary in-memory, to make the UX consistent across page refresh, we do not allow the user to re-download the file either.
51
+
52
+ activity = updateIn(activity, ['attachments', () => true, 'contentUrl'], (contentUrl: string) => {
53
+ if (contentUrl !== DIRECT_LINE_PLACEHOLDER_URL && !/^blob:/iu.test(contentUrl)) {
54
+ return contentUrl;
55
+ }
56
+ });
57
+
58
+ // If the message does not have sequence ID, use these fallback values:
59
+ // 1. "timestamp" field
60
+ // - outgoing activity will not have "timestamp" field
61
+ // 2. last activity sequence ID (or 0) + 0.001
62
+ // - best effort to put this message the last one in the chat history
63
+ activity = updateIn(activity, ['channelData', 'webchat:sequence-id'], (sequenceId?: number) =>
64
+ typeof sequenceId === 'number'
65
+ ? sequenceId
66
+ : typeof activity.timestamp !== 'undefined'
67
+ ? +new Date(activity.timestamp)
68
+ : // We assume there will be no more than 1,000 messages sent before receiving server response.
69
+ // If there are more than 1,000 messages, some messages will get reordered and appear jumpy after receiving server response.
70
+ // eslint-disable-next-line no-magic-numbers
71
+ (lastActivity?.channelData?.['webchat:sequence-id'] || 0) + 0.001
72
+ );
73
+
74
+ // TODO: [P1] #3953 We should move this patching logic to a DLJS wrapper for simplicity.
75
+ activity = updateIn(activity, ['channelData', 'webchat:sequence-id'], (sequenceId: number) =>
76
+ typeof sequenceId === 'number' ? sequenceId : +new Date(activity.timestamp || 0) || 0
77
+ );
78
+
79
+ return activity;
80
+ }
81
+
82
+ function upsertActivityWithSort(activities: WebChatActivity[], nextActivity: WebChatActivity): WebChatActivity[] {
83
+ nextActivity = patchActivity(nextActivity, activities[activities.length - 1]);
84
+
85
+ const { channelData: { clientActivityID: nextClientActivityID, 'webchat:sequence-id': nextSequenceId } = {} } =
86
+ nextActivity;
87
+
88
+ const nextActivities = activities.filter(
89
+ ({ channelData: { clientActivityID } = {}, id }) =>
90
+ // We will remove all "sending messages" activities and activities with same ID
91
+ // "clientActivityID" is unique and used to track if the message has been sent and echoed back from the server
92
+ !(nextClientActivityID && clientActivityID === nextClientActivityID) && !(id && id === nextActivity.id)
93
+ );
94
+
95
+ // Then, find the right (sorted) place to insert the new activity at, based on timestamp
96
+ // Since clockskew might happen, we will ignore timestamp on messages that are sending
97
+
98
+ const indexToInsert = nextActivities.findIndex(
99
+ ({ channelData: { state, 'webchat:sequence-id': sequenceId } = {} }) =>
100
+ (sequenceId || 0) > (nextSequenceId || 0) && state !== SENDING && state !== SEND_FAILED
101
+ );
102
+
103
+ // If no right place are found, append it
104
+ nextActivities.splice(~indexToInsert ? indexToInsert : nextActivities.length, 0, nextActivity);
105
+
106
+ return nextActivities;
107
+ }
108
+
109
+ export default function activities(
110
+ state: ActivitiesStateType = DEFAULT_STATE,
111
+ action: ActivitiesAction
112
+ ): ActivitiesStateType {
113
+ switch (action.type) {
114
+ case DELETE_ACTIVITY:
115
+ state = updateIn(state, [({ id }: WebChatActivity) => id === action.payload.activityID]);
116
+ break;
117
+
118
+ case MARK_ACTIVITY:
119
+ {
120
+ const { payload } = action;
121
+
122
+ state = updateIn(
123
+ state,
124
+ [({ id }: WebChatActivity) => id === payload.activityID, 'channelData', payload.name],
125
+ () => payload.value
126
+ );
127
+ }
128
+
129
+ break;
130
+
131
+ case POST_ACTIVITY_PENDING:
132
+ {
133
+ let {
134
+ payload: { activity }
135
+ } = action;
136
+
137
+ activity = updateIn(activity, ['channelData', 'state'], () => SENDING);
138
+
139
+ state = upsertActivityWithSort(state, activity);
140
+ }
141
+
142
+ break;
143
+
144
+ case POST_ACTIVITY_REJECTED:
145
+ state = updateIn(
146
+ state,
147
+ [findByClientActivityID(action.meta.clientActivityID), 'channelData', 'state'],
148
+ () => SEND_FAILED
149
+ );
150
+
151
+ break;
152
+
153
+ case POST_ACTIVITY_FULFILLED:
154
+ state = updateIn(state, [findByClientActivityID(action.meta.clientActivityID)], () =>
155
+ // We will replace the activity with the version from the server
156
+ updateIn(patchActivity(action.payload.activity, state[state.length - 1]), ['channelData', 'state'], () => SENT)
157
+ );
158
+
159
+ break;
160
+
161
+ case INCOMING_ACTIVITY:
162
+ // TODO: [P4] #2100 Move "typing" into Constants.ActivityType
163
+ state = upsertActivityWithSort(state, action.payload.activity);
164
+
165
+ break;
166
+
167
+ default:
168
+ break;
169
+ }
170
+
171
+ return state;
172
+ }
@@ -3,6 +3,7 @@ import updateIn from 'simple-update-in';
3
3
  import { DISMISS_NOTIFICATION } from '../actions/dismissNotification';
4
4
  import { SAGA_ERROR } from '../actions/sagaError';
5
5
  import { SET_NOTIFICATION } from '../actions/setNotification';
6
+ import isForbiddenPropertyName from '../utils/isForbiddenPropertyName';
6
7
 
7
8
  const DEFAULT_STATE = {};
8
9
 
@@ -15,23 +16,28 @@ export default function notifications(state = DEFAULT_STATE, { payload, type })
15
16
  state = updateIn(state, ['connectivitystatus', 'message'], () => 'javascripterror');
16
17
  } else if (type === SET_NOTIFICATION) {
17
18
  const { alt, data, id, level, message } = payload;
18
- const notification = state[id];
19
19
 
20
- if (
21
- !notification ||
22
- alt !== notification.alt ||
23
- !Object.is(data, notification.data) ||
24
- level !== notification.level ||
25
- message !== notification.message
26
- ) {
27
- state = updateIn(state, [id], () => ({
28
- alt,
29
- data,
30
- id,
31
- level,
32
- message,
33
- timestamp: now
34
- }));
20
+ if (!isForbiddenPropertyName(id)) {
21
+ // Mitigated through denylisting.
22
+ // eslint-disable-next-line security/detect-object-injection
23
+ const notification = state[id];
24
+
25
+ if (
26
+ !notification ||
27
+ alt !== notification.alt ||
28
+ !Object.is(data, notification.data) ||
29
+ level !== notification.level ||
30
+ message !== notification.message
31
+ ) {
32
+ state = updateIn(state, [id], () => ({
33
+ alt,
34
+ data,
35
+ id,
36
+ level,
37
+ message,
38
+ timestamp: now
39
+ }));
40
+ }
35
41
  }
36
42
  }
37
43
 
@@ -18,11 +18,12 @@ export default function lastTyping(state = DEFAULT_STATE, { payload, type }) {
18
18
  } = payload;
19
19
 
20
20
  if (activityType === 'typing') {
21
- state = updateIn(state, [id], () => ({
22
- at: Date.now(),
23
- name,
24
- role
25
- }));
21
+ const now = Date.now();
22
+
23
+ state = updateIn(state, [id, 'at'], at => at || now);
24
+ state = updateIn(state, [id, 'last'], () => now);
25
+ state = updateIn(state, [id, 'name'], () => name);
26
+ state = updateIn(state, [id, 'role'], () => role);
26
27
  } else if (activityType === 'message') {
27
28
  state = updateIn(state, [id]);
28
29
  }
@@ -78,6 +78,6 @@ function* connectionStatusToNotification({ payload: { directLine } }) {
78
78
  }
79
79
  }
80
80
 
81
- export default function*() {
81
+ export default function* () {
82
82
  yield takeLatest(CONNECT, connectionStatusToNotification);
83
83
  }
@@ -1,18 +1,20 @@
1
1
  import { call } from 'redux-saga/effects';
2
2
 
3
- export default function observeOnceEffect(observable) {
3
+ import { Observable, Observer, Subscription } from '../../types/external/Observable';
4
+
5
+ export default function observeOnceEffect<T>(observable: Observable<T>) {
4
6
  return call(function* observeOnce() {
5
- let subscription;
7
+ let subscription: Subscription;
6
8
 
7
9
  try {
8
10
  return yield call(
9
11
  () =>
10
- new Promise((resolve, reject) => {
12
+ new Promise<T>((resolve, reject) => {
11
13
  subscription = observable.subscribe({
12
14
  complete: resolve,
13
15
  error: reject,
14
16
  next: resolve
15
- });
17
+ } as Observer<T>);
16
18
  })
17
19
  );
18
20
  } finally {
@@ -4,13 +4,32 @@ import { CONNECT_FULFILLING } from '../../actions/connect';
4
4
  import { DISCONNECT_PENDING } from '../../actions/disconnect';
5
5
  import { RECONNECT_PENDING, RECONNECT_FULFILLING } from '../../actions/reconnect';
6
6
 
7
- export default function whileConnectedEffect(fn) {
7
+ import type { DirectLineJSBotConnection } from '../../types/external/DirectLineJSBotConnection';
8
+
9
+ export default function whileConnectedEffect(
10
+ fn: ({
11
+ directLine,
12
+ userID,
13
+ username
14
+ }: {
15
+ directLine: DirectLineJSBotConnection;
16
+ userID: string;
17
+ username: string;
18
+ }) => void
19
+ ) {
8
20
  return call(function* whileConnected() {
9
21
  for (;;) {
10
22
  const {
11
23
  meta: { userID, username },
12
24
  payload: { directLine }
25
+ }: {
26
+ meta: {
27
+ userID: string;
28
+ username: string;
29
+ };
30
+ payload: { directLine: DirectLineJSBotConnection };
13
31
  } = yield take([CONNECT_FULFILLING, RECONNECT_FULFILLING]);
32
+
14
33
  const task = yield fork(fn, { directLine, userID, username });
15
34
 
16
35
  // When we receive DISCONNECT_PENDING or RECONNECT_PENDING, the Direct Line connection is currently busy and should not be used.
@@ -4,10 +4,13 @@ import updateIn from 'simple-update-in';
4
4
  import observeEach from './effects/observeEach';
5
5
  import queueIncomingActivity from '../actions/queueIncomingActivity';
6
6
  import whileConnected from './effects/whileConnected';
7
+ import type { DirectLineActivity } from '../types/external/DirectLineActivity';
8
+ import type { DirectLineJSBotConnection } from '../types/external/DirectLineJSBotConnection';
9
+ import type { WebChatActivity } from '../types/WebChatActivity';
7
10
 
8
- const PASSTHRU_FN = value => value;
11
+ const PASSTHRU_FN = (value: unknown) => value;
9
12
 
10
- function patchActivityWithFromRole(activity, userID) {
13
+ function patchActivityWithFromRole(activity: DirectLineActivity, userID?: string): DirectLineActivity {
11
14
  // Some activities, such as "ConversationUpdate", does not have "from" defined.
12
15
  // And although "role" is defined in Direct Line spec, it was not sent over the wire.
13
16
  // We normalize the activity here to simplify null-check and logic later.
@@ -28,7 +31,7 @@ function patchActivityWithFromRole(activity, userID) {
28
31
  return activity;
29
32
  }
30
33
 
31
- function patchNullAsUndefined(activity) {
34
+ function patchNullAsUndefined(activity: DirectLineActivity): DirectLineActivity {
32
35
  // These fields are known used in Web Chat and in any cases, they should not be null, but undefined.
33
36
  // The only field omitted is "value", as it could be null purposefully.
34
37
 
@@ -56,12 +59,28 @@ function patchNullAsUndefined(activity) {
56
59
  }, activity);
57
60
  }
58
61
 
59
- function* observeActivity({ directLine, userID }) {
60
- yield observeEach(directLine.activity$, function* observeActivity(activity) {
62
+ // Patching the `from.name` to be a human readable name.
63
+ // We use the `from.name` for typing indicator, such that it read "John is typing...".
64
+ function patchFromName(activity: DirectLineActivity) {
65
+ return updateIn(activity, ['from', 'name'], (name: string | undefined): string => {
66
+ const { channelId, from = {} } = activity;
67
+
68
+ if ((channelId === 'directline' || channelId === 'webchat') && from.id === from.name && from.role === 'bot') {
69
+ return 'Bot';
70
+ }
71
+
72
+ return name;
73
+ });
74
+ }
75
+
76
+ function* observeActivity({ directLine, userID }: { directLine: DirectLineJSBotConnection; userID?: string }) {
77
+ yield observeEach(directLine.activity$, function* observeActivity(activity: DirectLineActivity) {
78
+ // TODO: [P2] #3953 Move the patching logic to a DirectLineJS wrapper, instead of too close to inners of Web Chat.
61
79
  activity = patchNullAsUndefined(activity);
62
80
  activity = patchActivityWithFromRole(activity, userID);
81
+ activity = patchFromName(activity);
63
82
 
64
- yield put(queueIncomingActivity(activity));
83
+ yield put(queueIncomingActivity(activity as WebChatActivity));
65
84
  });
66
85
  }
67
86
 
@@ -1,41 +1,44 @@
1
1
  import { all, call, cancelled, put, race, select, take, takeEvery } from 'redux-saga/effects';
2
2
 
3
- import observeOnce from './effects/observeOnce';
4
- import whileConnected from './effects/whileConnected';
5
-
6
- import clockSkewAdjustmentSelector from '../selectors/clockSkewAdjustment';
7
- import combineSelectors from '../selectors/combineSelectors';
8
- import dateToLocaleISOString from '../utils/dateToLocaleISOString';
9
- import languageSelector from '../selectors/language';
10
- import sendTimeoutSelector from '../selectors/sendTimeout';
11
-
12
- import deleteKey from '../utils/deleteKey';
13
- import sleep from '../utils/sleep';
14
- import uniqueID from '../utils/uniqueID';
15
-
3
+ import { INCOMING_ACTIVITY } from '../actions/incomingActivity';
16
4
  import {
17
5
  POST_ACTIVITY,
18
6
  POST_ACTIVITY_FULFILLED,
19
7
  POST_ACTIVITY_PENDING,
20
8
  POST_ACTIVITY_REJECTED
21
9
  } from '../actions/postActivity';
22
-
23
- import { INCOMING_ACTIVITY } from '../actions/incomingActivity';
24
-
25
- function getTimestamp(date, clockSkewAdjustment = 0) {
26
- // "+date" will return epoch time in milliseconds, same as Date.getTime().
27
- return new Date(+date + clockSkewAdjustment).toISOString();
28
- }
29
-
30
- function* postActivity(directLine, userID, username, numActivitiesPosted, { meta: { method }, payload: { activity } }) {
31
- const { clockSkewAdjustment, locale } = yield select(
32
- combineSelectors({ clockSkewAdjustment: clockSkewAdjustmentSelector, locale: languageSelector })
33
- );
34
- const { attachments } = activity;
10
+ import dateToLocaleISOString from '../utils/dateToLocaleISOString';
11
+ import deleteKey from '../utils/deleteKey';
12
+ import languageSelector from '../selectors/language';
13
+ import observeOnce from './effects/observeOnce';
14
+ import sendTimeoutSelector from '../selectors/sendTimeout';
15
+ import sleep from '../utils/sleep';
16
+ import uniqueID from '../utils/uniqueID';
17
+ import whileConnected from './effects/whileConnected';
18
+ import type { DirectLineActivity } from '../types/external/DirectLineActivity';
19
+ import type { DirectLineJSBotConnection } from '../types/external/DirectLineJSBotConnection';
20
+ import type { IncomingActivityAction } from '../actions/incomingActivity';
21
+ import type {
22
+ PostActivityAction,
23
+ PostActivityFulfilledAction,
24
+ PostActivityPendingAction,
25
+ PostActivityRejectedAction
26
+ } from '../actions/postActivity';
27
+ import type { WebChatActivity } from '../types/WebChatActivity';
28
+
29
+ function* postActivity(
30
+ directLine: DirectLineJSBotConnection,
31
+ userID: string,
32
+ username: string,
33
+ numActivitiesPosted: number,
34
+ { meta: { method }, payload: { activity } }: PostActivityAction
35
+ ) {
36
+ const attachments = (activity.type === 'message' && activity.attachments) || [];
35
37
  const clientActivityID = uniqueID();
36
- const now = new Date();
38
+ const locale = yield select(languageSelector);
37
39
  const localTimeZone =
38
40
  typeof window.Intl === 'undefined' ? undefined : new Intl.DateTimeFormat().resolvedOptions().timeZone;
41
+ const now = new Date();
39
42
 
40
43
  activity = {
41
44
  ...deleteKey(activity, 'id'),
@@ -49,9 +52,7 @@ function* postActivity(directLine, userID, username, numActivitiesPosted, { meta
49
52
  })),
50
53
  channelData: {
51
54
  ...deleteKey(activity.channelData, 'state'),
52
- clientActivityID,
53
- // This is unskewed local timestamp for estimating clock skew.
54
- clientTimestamp: getTimestamp(now)
55
+ clientActivityID
55
56
  },
56
57
  channelId: 'webchat',
57
58
  from: {
@@ -61,10 +62,7 @@ function* postActivity(directLine, userID, username, numActivitiesPosted, { meta
61
62
  },
62
63
  locale,
63
64
  localTimestamp: dateToLocaleISOString(now),
64
- localTimezone: localTimeZone,
65
- // This timestamp will be replaced by Direct Line Channel in echoback.
66
- // We are temporarily adding this timestamp for sorting.
67
- timestamp: getTimestamp(now, clockSkewAdjustment)
65
+ localTimezone: localTimeZone
68
66
  };
69
67
 
70
68
  if (!numActivitiesPosted) {
@@ -81,9 +79,9 @@ function* postActivity(directLine, userID, username, numActivitiesPosted, { meta
81
79
  ];
82
80
  }
83
81
 
84
- const meta = { clientActivityID, method };
82
+ const meta: { clientActivityID: string; method: string } = { clientActivityID, method };
85
83
 
86
- yield put({ type: POST_ACTIVITY_PENDING, meta, payload: { activity } });
84
+ yield put({ type: POST_ACTIVITY_PENDING, meta, payload: { activity } } as PostActivityPendingAction);
87
85
 
88
86
  try {
89
87
  // Quirks: We might receive INCOMING_ACTIVITY before the postActivity call completed
@@ -93,10 +91,8 @@ function* postActivity(directLine, userID, username, numActivitiesPosted, { meta
93
91
  for (;;) {
94
92
  const {
95
93
  payload: { activity }
96
- } = yield take(INCOMING_ACTIVITY);
97
- const { channelData = {}, id } = activity;
98
-
99
- if (channelData.clientActivityID === clientActivityID && id) {
94
+ }: IncomingActivityAction = yield take(INCOMING_ACTIVITY);
95
+ if (activity.channelData?.clientActivityID === clientActivityID && activity.id) {
100
96
  return activity;
101
97
  }
102
98
  }
@@ -107,35 +103,48 @@ function* postActivity(directLine, userID, username, numActivitiesPosted, { meta
107
103
  // - Direct Line service only respond on HTTP after bot respond to Direct Line
108
104
  // - Activity may take too long time to echo back
109
105
 
110
- const sendTimeout = yield select(sendTimeoutSelector);
106
+ const sendTimeout: number = yield select(sendTimeoutSelector);
111
107
 
112
108
  const {
113
109
  send: { echoBack }
114
- } = yield race({
110
+ }: { send: { echoBack: WebChatActivity } } = yield race({
115
111
  send: all({
116
112
  echoBack: echoBackCall,
117
- postActivity: observeOnce(directLine.postActivity(activity))
113
+ postActivity: observeOnce(directLine.postActivity(activity as DirectLineActivity))
118
114
  }),
119
115
  timeout: call(() => sleep(sendTimeout).then(() => Promise.reject(new Error('timeout'))))
120
116
  });
121
117
 
122
- yield put({ type: POST_ACTIVITY_FULFILLED, meta, payload: { activity: echoBack } });
118
+ yield put({ type: POST_ACTIVITY_FULFILLED, meta, payload: { activity: echoBack } } as PostActivityFulfilledAction);
123
119
  } catch (err) {
124
120
  console.error('botframework-webchat: Failed to post activity to chat adapter.', err);
125
121
 
126
- yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: err });
122
+ yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: err } as PostActivityRejectedAction);
127
123
  } finally {
128
124
  if (yield cancelled()) {
129
- yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: new Error('cancelled') });
125
+ yield put({
126
+ type: POST_ACTIVITY_REJECTED,
127
+ error: true,
128
+ meta,
129
+ payload: new Error('cancelled')
130
+ } as PostActivityRejectedAction);
130
131
  }
131
132
  }
132
133
  }
133
134
 
134
135
  export default function* postActivitySaga() {
135
- yield whileConnected(function* postActivityWhileConnected({ directLine, userID, username }) {
136
+ yield whileConnected(function* postActivityWhileConnected({
137
+ directLine,
138
+ userID,
139
+ username
140
+ }: {
141
+ directLine: DirectLineJSBotConnection;
142
+ userID: string;
143
+ username: string;
144
+ }) {
136
145
  let numActivitiesPosted = 0;
137
146
 
138
- yield takeEvery(POST_ACTIVITY, function* postActivityWrapper(action) {
147
+ yield takeEvery(POST_ACTIVITY, function* postActivityWrapper(action: PostActivityAction) {
139
148
  yield* postActivity(directLine, userID, username, numActivitiesPosted++, action);
140
149
  });
141
150
  });
@@ -50,51 +50,52 @@ function* waitForActivityId(replyToId, initialActivities) {
50
50
  }
51
51
 
52
52
  function* queueIncomingActivity({ userID }) {
53
- yield takeEveryAndSelect(QUEUE_INCOMING_ACTIVITY, activitiesSelector, function* queueIncomingActivity(
54
- { payload: { activity } },
55
- initialActivities
56
- ) {
57
- // This is for resolving an accessibility issue.
58
- // If the incoming activity has "replyToId" field, hold on it until the activity replied to is in the transcript, then release this one.
59
- const { replyToId } = activity;
60
- const initialBotActivities = initialActivities.filter(({ from: { role } }) => role === 'bot');
61
-
62
- // To speed up the first activity render time, we do not delay the first activity from the bot.
63
- // Even if it is the first activity from the bot, the bot might be "replying" to the "conversationUpdate" event.
64
- // Thus, the "replyToId" will always be there even it is the first activity in the conversation.
65
- if (replyToId && initialBotActivities.length) {
66
- // Either the activity replied to is in the transcript or after timeout.
67
- const result = yield race({
68
- _: waitForActivityId(replyToId, initialActivities),
69
- timeout: call(sleep, REPLY_TIMEOUT)
70
- });
71
-
72
- if ('timeout' in result) {
73
- console.warn(
74
- `botframework-webchat: Timed out while waiting for activity "${replyToId}" which activity "${activity.id}" is replying to.`,
75
- {
76
- activity,
77
- replyToId
78
- }
79
- );
53
+ yield takeEveryAndSelect(
54
+ QUEUE_INCOMING_ACTIVITY,
55
+ activitiesSelector,
56
+ function* queueIncomingActivity({ payload: { activity } }, initialActivities) {
57
+ // This is for resolving an accessibility issue.
58
+ // If the incoming activity has "replyToId" field, hold on it until the activity replied to is in the transcript, then release this one.
59
+ const { replyToId } = activity;
60
+ const initialBotActivities = initialActivities.filter(({ from: { role } }) => role === 'bot');
61
+
62
+ // To speed up the first activity render time, we do not delay the first activity from the bot.
63
+ // Even if it is the first activity from the bot, the bot might be "replying" to the "conversationUpdate" event.
64
+ // Thus, the "replyToId" will always be there even it is the first activity in the conversation.
65
+ if (replyToId && initialBotActivities.length) {
66
+ // Either the activity replied to is in the transcript or after timeout.
67
+ const result = yield race({
68
+ _: waitForActivityId(replyToId, initialActivities),
69
+ timeout: call(sleep, REPLY_TIMEOUT)
70
+ });
71
+
72
+ if ('timeout' in result) {
73
+ console.warn(
74
+ `botframework-webchat: Timed out while waiting for activity "${replyToId}" which activity "${activity.id}" is replying to.`,
75
+ {
76
+ activity,
77
+ replyToId
78
+ }
79
+ );
80
+ }
80
81
  }
81
- }
82
82
 
83
- yield put(incomingActivity(activity));
83
+ yield put(incomingActivity(activity));
84
84
 
85
- // Update suggested actions
86
- // TODO: [P3] We could put this logic inside reducer to minimize number of actions dispatched.
87
- const messageActivities = yield select(activitiesOfType('message'));
88
- const lastMessageActivity = messageActivities[messageActivities.length - 1];
85
+ // Update suggested actions
86
+ // TODO: [P3] We could put this logic inside reducer to minimize number of actions dispatched.
87
+ const messageActivities = yield select(activitiesOfType('message'));
88
+ const lastMessageActivity = messageActivities[messageActivities.length - 1];
89
89
 
90
- if (activityFromBot(lastMessageActivity)) {
91
- const { suggestedActions: { actions, to } = {} } = lastMessageActivity;
90
+ if (activityFromBot(lastMessageActivity)) {
91
+ const { suggestedActions: { actions, to } = {} } = lastMessageActivity;
92
92
 
93
- // If suggested actions is not destined to anyone, or is destined to the user, show it.
94
- // In other words, if suggested actions is destined to someone else, don't show it.
95
- yield put(setSuggestedActions(to && to.length && !to.includes(userID) ? null : actions));
93
+ // If suggested actions is not destined to anyone, or is destined to the user, show it.
94
+ // In other words, if suggested actions is destined to someone else, don't show it.
95
+ yield put(setSuggestedActions(to && to.length && !to.includes(userID) ? null : actions));
96
+ }
96
97
  }
97
- });
98
+ );
98
99
  }
99
100
 
100
101
  export default function* queueIncomingActivitySaga() {
@@ -1,7 +1,7 @@
1
1
  import { put, takeEvery } from 'redux-saga/effects';
2
+ import mime from 'mime';
2
3
 
3
4
  import { SEND_FILES } from '../actions/sendFiles';
4
- import mime from '../utils/mime-wrapper';
5
5
  import postActivity from '../actions/postActivity';
6
6
  import whileConnected from './effects/whileConnected';
7
7
 
@@ -3,7 +3,6 @@ import postActivity from '../actions/postActivity';
3
3
  import { SEND_MESSAGE_BACK } from '../actions/sendMessageBack';
4
4
  import whileConnected from './effects/whileConnected';
5
5
 
6
-
7
6
  // https://github.com/microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#message-back
8
7
  function* postActivityWithMessageBack({ payload: { displayText, text, value } }) {
9
8
  yield put(