@seamly/web-ui 20.4.0 → 20.5.0

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 (38) hide show
  1. package/build/dist/lib/index.debug.js +35 -68
  2. package/build/dist/lib/index.debug.min.js +1 -1
  3. package/build/dist/lib/index.debug.min.js.LICENSE.txt +4 -16
  4. package/build/dist/lib/index.js +374 -3850
  5. package/build/dist/lib/index.min.js +1 -1
  6. package/build/dist/lib/index.min.js.LICENSE.txt +0 -5
  7. package/build/dist/lib/standalone.js +394 -3862
  8. package/build/dist/lib/standalone.min.js +1 -1
  9. package/build/dist/lib/style-guide.js +200 -113
  10. package/build/dist/lib/style-guide.min.js +1 -1
  11. package/package.json +2 -4
  12. package/src/javascripts/api/index.js +17 -1
  13. package/src/javascripts/domains/config/reducer.js +2 -0
  14. package/src/javascripts/domains/forms/provider.js +14 -6
  15. package/src/javascripts/domains/visibility/actions.js +2 -0
  16. package/src/javascripts/domains/visibility/hooks.js +60 -1
  17. package/src/javascripts/domains/visibility/reducer.js +5 -0
  18. package/src/javascripts/domains/visibility/selectors.js +5 -0
  19. package/src/javascripts/domains/visibility/utils.js +5 -1
  20. package/src/javascripts/style-guide/components/app.js +2 -0
  21. package/src/javascripts/style-guide/states.js +30 -50
  22. package/src/javascripts/ui/components/conversation/event/card-component.js +1 -2
  23. package/src/javascripts/ui/components/conversation/event/conversation-suggestions.js +19 -9
  24. package/src/javascripts/ui/components/conversation/event/cta.js +1 -2
  25. package/src/javascripts/ui/components/conversation/event/participant.js +2 -11
  26. package/src/javascripts/ui/components/conversation/event/splash.js +1 -3
  27. package/src/javascripts/ui/components/conversation/event/text.js +9 -9
  28. package/src/javascripts/ui/components/layout/chat.js +52 -48
  29. package/src/javascripts/ui/components/suggestions/suggestions-list.js +12 -14
  30. package/src/javascripts/ui/components/view/deprecated-view.js +16 -11
  31. package/src/javascripts/ui/components/view/inline-view.js +13 -8
  32. package/src/javascripts/ui/hooks/seamly-entry-hooks.js +3 -2
  33. package/src/javascripts/ui/hooks/seamly-state-hooks.js +4 -3
  34. package/src/javascripts/ui/hooks/use-seamly-chat.js +41 -29
  35. package/src/javascripts/ui/hooks/use-seamly-commands.js +16 -4
  36. package/src/javascripts/ui/utils/seamly-utils.js +16 -6
  37. package/src/javascripts/lib/parse-body.js +0 -10
  38. package/src/javascripts/ui/components/conversation/event/hooks/use-text-rendering.js +0 -35
@@ -6,19 +6,17 @@ const SuggestionsList = ({
6
6
  suggestions = [],
7
7
  onClickSuggestion,
8
8
  hasIcon = true,
9
- }) => {
10
- return (
11
- <ul className={className('suggestions__list', givenClassName)}>
12
- {suggestions.map((suggestion) => (
13
- <SuggestionsItem
14
- hasIcon={hasIcon}
15
- key={suggestion.id}
16
- onClick={onClickSuggestion}
17
- {...suggestion}
18
- />
19
- ))}
20
- </ul>
21
- )
22
- }
9
+ }) => (
10
+ <ul className={className('suggestions__list', givenClassName)}>
11
+ {suggestions.map((suggestion) => (
12
+ <SuggestionsItem
13
+ hasIcon={hasIcon}
14
+ key={suggestion.id}
15
+ onClick={onClickSuggestion}
16
+ {...suggestion}
17
+ />
18
+ ))}
19
+ </ul>
20
+ )
23
21
 
24
22
  export default SuggestionsList
@@ -1,3 +1,4 @@
1
+ import { useVisibility, useShowInlineView } from 'domains/visibility'
1
2
  import DeprecatedAppFrame from '../layout/deprecated-app-frame'
2
3
  import ChatFrame from '../layout/chat-frame'
3
4
  import AgentInfo from '../layout/agent-info'
@@ -6,25 +7,29 @@ import Conversation from '../conversation/conversation'
6
7
  import EntryContainer from '../entry/entry-container'
7
8
  import Interrupt from '../layout/interrupt'
8
9
  import { useSeamlyChat } from '../../hooks/seamly-hooks'
9
- import { useVisibility } from '../../../domains/visibility'
10
10
  import DeprecatedToggleButton from '../entry/deprecated-toggle-button'
11
11
 
12
12
  const DeprecatedView = () => {
13
13
  const { isVisible } = useVisibility()
14
14
  const { openChat, closeChat } = useSeamlyChat()
15
+ const { showInlineView, containerRef } = useShowInlineView()
15
16
 
16
17
  return (
17
18
  isVisible && (
18
- <DeprecatedAppFrame>
19
- <DeprecatedToggleButton onOpenChat={openChat} />
20
- <Header onCloseChat={closeChat}>
21
- <AgentInfo />
22
- </Header>
23
- <ChatFrame interruptComponent={Interrupt}>
24
- <Conversation />
25
- <EntryContainer />
26
- </ChatFrame>
27
- </DeprecatedAppFrame>
19
+ <div ref={containerRef}>
20
+ {showInlineView && (
21
+ <DeprecatedAppFrame>
22
+ <DeprecatedToggleButton onOpenChat={openChat} />
23
+ <Header onCloseChat={closeChat}>
24
+ <AgentInfo />
25
+ </Header>
26
+ <ChatFrame interruptComponent={Interrupt}>
27
+ <Conversation />
28
+ <EntryContainer />
29
+ </ChatFrame>
30
+ </DeprecatedAppFrame>
31
+ )}
32
+ </div>
28
33
  )
29
34
  )
30
35
  }
@@ -1,20 +1,22 @@
1
- import { className } from '../../../lib/css'
1
+ import { className } from 'lib/css'
2
+ import { useVisibility, useShowInlineView } from 'domains/visibility'
3
+ import { useInterrupt } from 'domains/interrupt'
2
4
  import Chat from '../layout/chat'
3
5
  import Interrupt from '../layout/interrupt'
4
6
  import Conversation from '../conversation/conversation'
5
7
  import EntryContainer from '../entry/entry-container'
6
8
  import ChatFrame from '../layout/chat-frame'
7
9
  import useSeamlyChat from '../../hooks/use-seamly-chat'
8
- import { useVisibility } from '../../../domains/visibility'
9
10
  import PreChatMessages from '../layout/pre-chat-messages'
10
11
  import Suggestions from '../suggestions'
11
12
  import InOutTransition, {
12
13
  transitionStartStates,
13
14
  } from '../widgets/in-out-transition'
14
- import { useInterrupt } from '../../../domains/interrupt'
15
15
 
16
16
  const InlineView = () => {
17
17
  useSeamlyChat()
18
+ const { showInlineView, containerRef } = useShowInlineView()
19
+
18
20
  const { isOpen } = useVisibility()
19
21
  const { hasInterrupt, meta } = useInterrupt()
20
22
 
@@ -29,6 +31,7 @@ const InlineView = () => {
29
31
  transitionStartState={transitionStartStates.rendered}
30
32
  >
31
33
  <div
34
+ ref={containerRef}
32
35
  className={className(
33
36
  'unstarted-wrapper',
34
37
  'unstarted-wrapper--inline',
@@ -42,11 +45,13 @@ const InlineView = () => {
42
45
  isActive={isOpen}
43
46
  transitionStartState={transitionStartStates.rendered}
44
47
  >
45
- <Chat>
46
- <ChatFrame interruptComponent={Interrupt}>
47
- {isOpen && <Conversation />}
48
- <EntryContainer />
49
- </ChatFrame>
48
+ <Chat ref={containerRef}>
49
+ {showInlineView && (
50
+ <ChatFrame interruptComponent={Interrupt}>
51
+ {isOpen && <Conversation />}
52
+ <EntryContainer />
53
+ </ChatFrame>
54
+ )}
50
55
  </Chat>
51
56
  </InOutTransition>
52
57
  </>
@@ -80,12 +80,13 @@ export const useSeamlyEntry = () => {
80
80
  active,
81
81
  userSelected,
82
82
  options: entryOptions,
83
+ optionsOverride: entryOptionsOverride,
83
84
  } = useSeamlyStateContext().entryMeta
84
85
  const dispatch = useSeamlyDispatchContext()
85
86
 
86
87
  const activeEntry = userSelected || active || defaultEntry
87
-
88
- const activeEntryOptions = entryOptions[activeEntry] || {}
88
+ const activeEntryOptions =
89
+ entryOptionsOverride[activeEntry] || entryOptions[activeEntry] || {}
89
90
 
90
91
  const setBlockAutoEntrySwitch = useCallback(
91
92
  (value) => {
@@ -103,14 +103,15 @@ export const useEntryTextLimit = () => {
103
103
  const {
104
104
  entryMeta: {
105
105
  options: { text },
106
+ optionsOverride: { text: overrideText },
106
107
  },
107
108
  } = useSeamlyStateContext()
108
109
 
109
- const { limit } = text || {}
110
+ const { limit } = overrideText || text || { limit: null }
110
111
 
111
112
  return {
112
- hasLimit: limit != null,
113
- limit: limit != null ? limit : null,
113
+ hasLimit: limit !== null,
114
+ limit: limit !== null ? limit : null,
114
115
  }
115
116
  }
116
117
 
@@ -1,29 +1,31 @@
1
- import { useEffect, useRef } from 'preact/hooks'
1
+ import { useCallback, useEffect, useRef } from 'preact/hooks'
2
2
  import { useI18n } from 'domains/i18n'
3
3
  import { seamlyActions } from 'ui/utils/seamly-utils'
4
4
  import { useVisibility, visibilityStates } from 'domains/visibility'
5
5
  import useSeamlyDispatchContext from './use-seamly-dispatch'
6
- import { useEvents } from './seamly-state-hooks'
6
+ import { useEvents, useSeamlyLayoutMode } from './seamly-state-hooks'
7
7
  import useSeamlyCommands from './use-seamly-commands'
8
8
  import { useSeamlyHasConversation } from './seamly-api-hooks'
9
9
  import { useLiveRegion } from './live-region-hooks'
10
- import { useConfig } from '../../domains/config'
10
+ import { useSelector } from '../../domains/redux/hooks'
11
+ import { selectShowInlineView } from '../../domains/visibility/selectors'
11
12
 
12
13
  const { SET_IS_LOADING } = seamlyActions
13
14
 
14
15
  const useSeamlyChat = () => {
15
16
  const { t } = useI18n()
16
- const { layoutMode } = useConfig()
17
+ const { isInline, isWindow } = useSeamlyLayoutMode()
17
18
  const { isOpen, isVisible, setVisibility } = useVisibility()
19
+ const showInlineView = useSelector(selectShowInlineView)
18
20
  const dispatch = useSeamlyDispatchContext()
19
21
  const events = useEvents()
20
22
  const spinnerTimeout = useRef(null)
21
- const { start, connect, apiConfigReady } = useSeamlyCommands()
23
+ const { start, connect, apiConfigReady, apiConnected } = useSeamlyCommands()
22
24
  const hasConversation = useSeamlyHasConversation()
23
25
  const prevIsOpen = useRef(null)
24
26
  const prevIsVisible = useRef(null)
27
+ const startCalled = useRef(false)
25
28
  const { sendAssertive } = useLiveRegion()
26
- const connectCalled = useRef(false)
27
29
 
28
30
  const hasEvents = events.length > 0
29
31
 
@@ -76,28 +78,41 @@ const useSeamlyChat = () => {
76
78
  }, [hasEvents, dispatch])
77
79
 
78
80
  useEffect(() => {
79
- // This is needed to reset the ref to allow connect to happen again.
81
+ // This is needed to reset the ref to allow connect and start to happen again.
80
82
  // Mostly due to Interrupt situations and a reset being called.
81
- if (!hasConversation || !apiConfigReady) {
82
- connectCalled.current = false
83
+ if (!apiConfigReady || !apiConnected) {
84
+ startCalled.current = false
83
85
  }
84
- }, [hasConversation, apiConfigReady])
86
+ }, [apiConfigReady, apiConnected])
85
87
 
86
- useEffect(() => {
87
- // We don't connect minimised or hidden window interfaces unless
88
- // they had been connected before.
89
- // We also keep track of whether connect was called before to avoid
90
- // multiple in-flight connection processes.
88
+ const connectAndStart = useCallback(async () => {
89
+ // We don't connect if we are already connected to the api to avoid multiple in-flight connection processes.
90
+ if (!apiConnected) {
91
+ await connect()
92
+ }
93
+
94
+ // We only start a conversation when the chat interface is either 'open' or if using the inline view if it's 'open' or 'minimized'.
95
+ if (isOpen || (isVisible && isInline)) {
96
+ start()
97
+ startCalled.current = true
98
+ }
99
+ }, [apiConnected, connect, isInline, isOpen, isVisible, start])
91
100
 
101
+ useEffect(() => {
102
+ // We dont't connect or start when the apiConfig is not ready yet.
103
+ // We also keep track of whether start has been called to avoid multiple in-flight connection processes.
104
+ // We check if the window view is not open and no conversation is started yet.
105
+ // Lastly we check if the inline view is not scrolled in to view.
92
106
  if (
93
- (layoutMode === 'window' && !isOpen && !hasConversation) ||
94
- connectCalled.current ||
95
- !apiConfigReady
107
+ !apiConfigReady ||
108
+ startCalled.current ||
109
+ (isWindow && !isOpen && !hasConversation) ||
110
+ (isInline && !showInlineView)
96
111
  ) {
97
112
  return
98
113
  }
99
114
 
100
- if (hasConversation) {
115
+ if (hasConversation && isOpen) {
101
116
  // We deactivate the extra startup loading spinner when a conversation is available
102
117
  // We also stop setting the loading indicator in the first place to avoid a flash.
103
118
  clearTimeout(spinnerTimeout.current)
@@ -106,19 +121,16 @@ const useSeamlyChat = () => {
106
121
  isLoading: false,
107
122
  })
108
123
  }
109
- connect().then(() => {
110
- start()
111
- })
112
-
113
- connectCalled.current = true
124
+ connectAndStart()
114
125
  }, [
115
- isOpen,
116
- hasConversation,
117
126
  apiConfigReady,
118
- start,
119
- connect,
127
+ connectAndStart,
120
128
  dispatch,
121
- layoutMode,
129
+ hasConversation,
130
+ isInline,
131
+ isOpen,
132
+ isWindow,
133
+ showInlineView,
122
134
  ])
123
135
 
124
136
  const openChat = () => {
@@ -7,7 +7,7 @@ import { Actions as InterruptActions } from 'domains/interrupt'
7
7
  import { useConfig } from 'domains/config'
8
8
  import * as AppActions from 'domains/app/actions'
9
9
  import { useUserHasResponded } from 'domains/app/hooks'
10
- import { useVisibility } from 'domains/visibility'
10
+ import { useVisibility, visibilityStates } from 'domains/visibility'
11
11
  import { useStableCallback } from './utility-hooks'
12
12
  import useSeamlyDispatchContext from './use-seamly-dispatch'
13
13
  import { useSeamlyUnreadCount } from './seamly-state-hooks'
@@ -26,7 +26,7 @@ const useSeamlyCommands = () => {
26
26
 
27
27
  const hasResponded = useUserHasResponded()
28
28
  const hasConversation = useSeamlyHasConversation()
29
- const { visible: visibility } = useVisibility()
29
+ const { visible: visibility, setVisibility } = useVisibility()
30
30
  const unreadMessageCount = useSeamlyUnreadCount()
31
31
 
32
32
  const emitEvent = useCallback(
@@ -44,6 +44,7 @@ const useSeamlyCommands = () => {
44
44
  hasResponded,
45
45
  unreadMessageCount,
46
46
  })
47
+
47
48
  api.send('start')
48
49
  emitEvent('ui.start', {
49
50
  visibility,
@@ -107,7 +108,13 @@ const useSeamlyCommands = () => {
107
108
  emitEvent('message', message)
108
109
  dispatch({
109
110
  type: ADD_EVENT,
110
- event: { type: 'message', payload: message },
111
+ event: {
112
+ type: 'message',
113
+ payload: {
114
+ ...message,
115
+ optimisticallyInjected: true,
116
+ },
117
+ },
111
118
  })
112
119
  },
113
120
  [api, dispatch, emitEvent, getTextMessageBase],
@@ -213,12 +220,16 @@ const useSeamlyCommands = () => {
213
220
  .then((initialState) => {
214
221
  if (initialState) {
215
222
  dispatch({ type: SET_INITIAL_STATE, initialState })
223
+ if (initialState.userResponded) {
224
+ dispatch(AppActions.setHasResponded(initialState.userResponded))
225
+ setVisibility(visibilityStates.open)
226
+ }
216
227
  }
217
228
  })
218
229
  .catch((error) => {
219
230
  dispatch(InterruptActions.set(error))
220
231
  })
221
- }, [api, dispatch])
232
+ }, [api, dispatch, setVisibility])
222
233
 
223
234
  return {
224
235
  connect,
@@ -232,6 +243,7 @@ const useSeamlyCommands = () => {
232
243
  addMessageBubble,
233
244
  addUploadBubble,
234
245
  addDivider,
246
+ apiConnected: api.connected,
235
247
  apiConfigReady: api.configReady,
236
248
  }
237
249
  }
@@ -173,13 +173,18 @@ const orderHistory = (events) => {
173
173
  }
174
174
 
175
175
  export const mergeHistory = (stateEvents, historyEvents) => {
176
+ const newStateEvents = stateEvents.filter(
177
+ (stateEvent) =>
178
+ // Deduplicate the event streams, giving events in historyEvents
179
+ // precedence so the server is able to push changes to events.
180
+ !historyEvents.some(
181
+ (historyEvent) => historyEvent.payload.id === stateEvent.payload.id,
182
+ ),
183
+ )
184
+
176
185
  const newHistoryEvents = historyEvents
177
186
  .filter(
178
187
  (historyEvent) =>
179
- // Deduplicate the event streams
180
- !stateEvents.find(
181
- (stateEvent) => stateEvent.payload.id === historyEvent.payload.id,
182
- ) &&
183
188
  // Remove all non displayable participant messages
184
189
  !(
185
190
  historyEvent.type === 'participant' &&
@@ -192,7 +197,7 @@ export const mergeHistory = (stateEvents, historyEvents) => {
192
197
  // the normal merging logic there is no added effect.
193
198
  .reverse()
194
199
 
195
- return orderHistory([...newHistoryEvents, ...stateEvents])
200
+ return orderHistory([...newHistoryEvents, ...newStateEvents])
196
201
  }
197
202
 
198
203
  const participantReducer = (state, action) => {
@@ -613,7 +618,12 @@ export const seamlyStateReducer = (state, action) => {
613
618
  headerTitles: headerTitlesReducer(state.headerTitles, action),
614
619
  }
615
620
  case SET_INITIAL_STATE:
616
- return { ...state, initialState: action.initialState }
621
+ const { initialState } = action
622
+ return {
623
+ ...state,
624
+ initialState,
625
+ unreadEvents: initialState.unreadMessageCount,
626
+ }
617
627
  case SET_SERVICE_DATA_ITEM:
618
628
  return {
619
629
  ...state,
@@ -1,10 +0,0 @@
1
- import { marked } from 'marked'
2
-
3
- export default (body) => {
4
- try {
5
- return marked(body)
6
- } catch (e) {
7
- console.log('Could not parse message', body, e)
8
- return ''
9
- }
10
- }
@@ -1,35 +0,0 @@
1
- import Mustache from 'mustache'
2
-
3
- Mustache.escape = function (escapeText) {
4
- return escapeText
5
- }
6
-
7
- const parseLinkVariable = (variable) => {
8
- return `<a href='${variable.url}' data-link-id='${variable.id}' ${
9
- variable.newTab ? 'target="_blank"' : ''
10
- }>${variable.name}</a>`
11
- }
12
-
13
- export function parseRichText(text, variables = {}) {
14
- const view = {}
15
- Object.entries(variables).forEach(([key, variable]) => {
16
- switch (variable.type) {
17
- case 'link':
18
- view[key] = parseLinkVariable(variable)
19
- break
20
- case 'text':
21
- view[key] = variable.value
22
- break
23
- }
24
- }, {})
25
-
26
- // Disable escaping as we'll be generating HTML
27
- const oldEscape = Mustache.escape
28
- Mustache.escape = function (escapeText) {
29
- return escapeText
30
- }
31
- const output = Mustache.render(text, view)
32
- Mustache.escape = oldEscape
33
-
34
- return output
35
- }