@seamly/web-ui 20.3.1 → 20.6.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.
- package/CHANGELOG.md +769 -0
- package/build/dist/lib/index.debug.js +66 -99
- package/build/dist/lib/index.debug.min.js +1 -1
- package/build/dist/lib/index.debug.min.js.LICENSE.txt +8 -20
- package/build/dist/lib/index.js +748 -4372
- package/build/dist/lib/index.min.js +1 -1
- package/build/dist/lib/index.min.js.LICENSE.txt +0 -5
- package/build/dist/lib/standalone.js +606 -4449
- package/build/dist/lib/standalone.min.js +1 -1
- package/build/dist/lib/style-guide.js +373 -178
- package/build/dist/lib/style-guide.min.js +1 -1
- package/build/dist/lib/styles.css +1 -1
- package/package.json +2 -4
- package/src/javascripts/api/index.js +17 -1
- package/src/javascripts/domains/config/reducer.js +2 -0
- package/src/javascripts/domains/forms/provider.js +14 -6
- package/src/javascripts/domains/store/index.js +11 -11
- package/src/javascripts/domains/store/state-reducer.js +1 -0
- package/src/javascripts/domains/translations/middleware.js +6 -5
- package/src/javascripts/domains/visibility/actions.js +2 -0
- package/src/javascripts/domains/visibility/hooks.js +60 -1
- package/src/javascripts/domains/visibility/reducer.js +5 -0
- package/src/javascripts/domains/visibility/selectors.js +5 -0
- package/src/javascripts/domains/visibility/utils.js +5 -1
- package/src/javascripts/style-guide/components/app.js +2 -0
- package/src/javascripts/style-guide/states.js +86 -51
- package/src/javascripts/ui/components/conversation/conversation.js +40 -36
- package/src/javascripts/ui/components/conversation/event/card-component.js +1 -2
- package/src/javascripts/ui/components/conversation/event/conversation-suggestions.js +19 -9
- package/src/javascripts/ui/components/conversation/event/cta.js +1 -2
- package/src/javascripts/ui/components/conversation/event/image.js +11 -3
- package/src/javascripts/ui/components/conversation/event/participant.js +2 -11
- package/src/javascripts/ui/components/conversation/event/splash.js +1 -3
- package/src/javascripts/ui/components/conversation/event/text.js +9 -9
- package/src/javascripts/ui/components/entry/text-entry/text-entry-form.js +6 -0
- package/src/javascripts/ui/components/layout/chat.js +52 -48
- package/src/javascripts/ui/components/suggestions/suggestions-list.js +12 -14
- package/src/javascripts/ui/components/view/deprecated-view.js +16 -11
- package/src/javascripts/ui/components/view/inline-view.js +13 -8
- package/src/javascripts/ui/hooks/seamly-entry-hooks.js +3 -2
- package/src/javascripts/ui/hooks/seamly-state-hooks.js +7 -3
- package/src/javascripts/ui/hooks/use-seamly-chat.js +41 -29
- package/src/javascripts/ui/hooks/use-seamly-commands.js +16 -4
- package/src/javascripts/ui/utils/seamly-utils.js +27 -7
- package/src/stylesheets/5-components/_message-count.scss +5 -2
- package/webpack/parts/dev-server.js +10 -1
- package/src/.DS_Store +0 -0
- package/src/javascripts/lib/parse-body.js +0 -10
- package/src/javascripts/ui/components/conversation/event/hooks/use-text-rendering.js +0 -35
|
@@ -64,6 +64,11 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
|
|
|
64
64
|
}
|
|
65
65
|
}, [setBlockAutoEntrySwitch, hasValue])
|
|
66
66
|
|
|
67
|
+
const handlePointerDown = (event) => {
|
|
68
|
+
// When a message is submitted, the keyboard should be prevented from closing on mobile devices
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
}
|
|
71
|
+
|
|
67
72
|
return (
|
|
68
73
|
<Form
|
|
69
74
|
className={className('entry-form')}
|
|
@@ -99,6 +104,7 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
|
|
|
99
104
|
<button
|
|
100
105
|
className={className('button', 'input__submit')}
|
|
101
106
|
type="submit"
|
|
107
|
+
onPointerDown={handlePointerDown}
|
|
102
108
|
aria-disabled={!hasValue || reachedCharacterLimit ? 'true' : null}
|
|
103
109
|
>
|
|
104
110
|
<Icon name="send" size="32" alt={t('input.sendMessage')} />
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { forwardRef } from 'preact/compat'
|
|
1
2
|
import { className } from '../../../lib/css'
|
|
2
3
|
import {
|
|
3
4
|
useSeamlyAppContainerClassNames,
|
|
@@ -9,54 +10,57 @@ import { useI18n } from '../../../domains/i18n'
|
|
|
9
10
|
import { useVisibility, visibilityStates } from '../../../domains/visibility'
|
|
10
11
|
import Suggestions from '../suggestions'
|
|
11
12
|
|
|
12
|
-
const Chat = (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
13
|
+
const Chat = forwardRef(
|
|
14
|
+
({ children, className: givenClassName = '' }, forwardedRef) => {
|
|
15
|
+
const { isOpen, isVisible, setVisibility } = useVisibility()
|
|
16
|
+
const { namespace, layoutMode } = useConfig()
|
|
17
|
+
const { isInline } = useSeamlyLayoutMode()
|
|
18
|
+
const appContainerClassNames = useSeamlyAppContainerClassNames()
|
|
19
|
+
const userResponded = useUserHasResponded()
|
|
20
|
+
const { t } = useI18n()
|
|
21
|
+
|
|
22
|
+
const defaultClassNames = [
|
|
23
|
+
'chat',
|
|
24
|
+
`chat--layout-${layoutMode}`,
|
|
25
|
+
`namespace--${namespace}`,
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const classNames = [
|
|
29
|
+
...defaultClassNames,
|
|
30
|
+
...appContainerClassNames,
|
|
31
|
+
givenClassName,
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
if (!isOpen && layoutMode !== 'app') {
|
|
35
|
+
classNames.push('chat--collapsed')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (userResponded) {
|
|
39
|
+
classNames.push('chat--user-responded')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const onKeyDownHandler = (e) => {
|
|
43
|
+
if ((e.code && e.code === 'Escape') || e.keyCode === 27)
|
|
44
|
+
if (!isInline && isOpen) {
|
|
45
|
+
setVisibility(visibilityStates.minimized)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
isVisible && (
|
|
51
|
+
<section
|
|
52
|
+
className={className(classNames)}
|
|
53
|
+
onKeyDown={onKeyDownHandler}
|
|
54
|
+
tabIndex="-1"
|
|
55
|
+
ref={forwardedRef}
|
|
56
|
+
aria-label={t('chat.srLabel')}
|
|
57
|
+
>
|
|
58
|
+
<div className={className('chat-wrapper')}>{children}</div>
|
|
59
|
+
{layoutMode === 'inline' && isOpen && <Suggestions isAside={true} />}
|
|
60
|
+
</section>
|
|
61
|
+
)
|
|
58
62
|
)
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
},
|
|
64
|
+
)
|
|
61
65
|
|
|
62
66
|
export default Chat
|
|
@@ -6,19 +6,17 @@ const SuggestionsList = ({
|
|
|
6
6
|
suggestions = [],
|
|
7
7
|
onClickSuggestion,
|
|
8
8
|
hasIcon = true,
|
|
9
|
-
}) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 '
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
88
|
+
const activeEntryOptions =
|
|
89
|
+
entryOptionsOverride[activeEntry] || entryOptions[activeEntry] || {}
|
|
89
90
|
|
|
90
91
|
const setBlockAutoEntrySwitch = useCallback(
|
|
91
92
|
(value) => {
|
|
@@ -71,6 +71,9 @@ export const useSeamlyHeaderData = () => useSeamlyStateContext().headerTitles
|
|
|
71
71
|
|
|
72
72
|
export const useSeamlyUnreadCount = () => useSeamlyStateContext().unreadEvents
|
|
73
73
|
|
|
74
|
+
export const useLoadedImageEventIds = () =>
|
|
75
|
+
useSeamlyStateContext().loadedImageEventIds
|
|
76
|
+
|
|
74
77
|
export const useSkiplink = () => useSeamlyStateContext().skiplinkTargetId
|
|
75
78
|
|
|
76
79
|
export const useSeamlyParticipant = (participantId) =>
|
|
@@ -100,14 +103,15 @@ export const useEntryTextLimit = () => {
|
|
|
100
103
|
const {
|
|
101
104
|
entryMeta: {
|
|
102
105
|
options: { text },
|
|
106
|
+
optionsOverride: { text: overrideText },
|
|
103
107
|
},
|
|
104
108
|
} = useSeamlyStateContext()
|
|
105
109
|
|
|
106
|
-
const { limit } = text || {}
|
|
110
|
+
const { limit } = overrideText || text || { limit: null }
|
|
107
111
|
|
|
108
112
|
return {
|
|
109
|
-
hasLimit: limit
|
|
110
|
-
limit: limit
|
|
113
|
+
hasLimit: limit !== null,
|
|
114
|
+
limit: limit !== null ? limit : null,
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
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 {
|
|
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 {
|
|
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 (!
|
|
82
|
-
|
|
83
|
+
if (!apiConfigReady || !apiConnected) {
|
|
84
|
+
startCalled.current = false
|
|
83
85
|
}
|
|
84
|
-
}, [
|
|
86
|
+
}, [apiConfigReady, apiConnected])
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
// We don't connect
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
!
|
|
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
|
-
|
|
110
|
-
start()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
connectCalled.current = true
|
|
124
|
+
connectAndStart()
|
|
114
125
|
}, [
|
|
115
|
-
isOpen,
|
|
116
|
-
hasConversation,
|
|
117
126
|
apiConfigReady,
|
|
118
|
-
|
|
119
|
-
connect,
|
|
127
|
+
connectAndStart,
|
|
120
128
|
dispatch,
|
|
121
|
-
|
|
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: {
|
|
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
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomId } from 'lib/id'
|
|
1
2
|
import { getTimeFromSeconds } from './general-utils'
|
|
2
3
|
|
|
3
4
|
export const eventTypes = {
|
|
@@ -73,6 +74,7 @@ export const seamlyActions = {
|
|
|
73
74
|
CLEAR_EVENTS: 'CLEAR_EVENTS',
|
|
74
75
|
SET_HISTORY: 'SET_HISTORY',
|
|
75
76
|
SET_EVENTS_READ: 'SET_EVENTS_READ',
|
|
77
|
+
SET_LOADED_IMAGE_EVENT_IDS: 'SET_LOADED_IMAGE_EVENT_IDS',
|
|
76
78
|
ACK_EVENT: 'ACK_EVENT',
|
|
77
79
|
SET_IS_LOADING: 'SET_IS_LOADING',
|
|
78
80
|
CLEAR_PARTICIPANTS: 'CLEAR_PARTICIPANTS',
|
|
@@ -121,6 +123,7 @@ const {
|
|
|
121
123
|
CLEAR_EVENTS,
|
|
122
124
|
SET_HISTORY,
|
|
123
125
|
SET_EVENTS_READ,
|
|
126
|
+
SET_LOADED_IMAGE_EVENT_IDS,
|
|
124
127
|
ACK_EVENT,
|
|
125
128
|
SET_IS_LOADING,
|
|
126
129
|
CLEAR_PARTICIPANTS,
|
|
@@ -171,13 +174,18 @@ const orderHistory = (events) => {
|
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
export const mergeHistory = (stateEvents, historyEvents) => {
|
|
177
|
+
const newStateEvents = stateEvents.filter(
|
|
178
|
+
(stateEvent) =>
|
|
179
|
+
// Deduplicate the event streams, giving events in historyEvents
|
|
180
|
+
// precedence so the server is able to push changes to events.
|
|
181
|
+
!historyEvents.some(
|
|
182
|
+
(historyEvent) => historyEvent.payload.id === stateEvent.payload.id,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
|
|
174
186
|
const newHistoryEvents = historyEvents
|
|
175
187
|
.filter(
|
|
176
188
|
(historyEvent) =>
|
|
177
|
-
// Deduplicate the event streams
|
|
178
|
-
!stateEvents.find(
|
|
179
|
-
(stateEvent) => stateEvent.payload.id === historyEvent.payload.id,
|
|
180
|
-
) &&
|
|
181
189
|
// Remove all non displayable participant messages
|
|
182
190
|
!(
|
|
183
191
|
historyEvent.type === 'participant' &&
|
|
@@ -190,7 +198,7 @@ export const mergeHistory = (stateEvents, historyEvents) => {
|
|
|
190
198
|
// the normal merging logic there is no added effect.
|
|
191
199
|
.reverse()
|
|
192
200
|
|
|
193
|
-
return orderHistory([...newHistoryEvents, ...
|
|
201
|
+
return orderHistory([...newHistoryEvents, ...newStateEvents])
|
|
194
202
|
}
|
|
195
203
|
|
|
196
204
|
const participantReducer = (state, action) => {
|
|
@@ -348,6 +356,8 @@ export const seamlyStateReducer = (state, action) => {
|
|
|
348
356
|
? readStates.read
|
|
349
357
|
: readStates.received,
|
|
350
358
|
}),
|
|
359
|
+
// We add a randomid to use as key for mapping of Events to avoid rerendering
|
|
360
|
+
key: randomId(),
|
|
351
361
|
...payload,
|
|
352
362
|
},
|
|
353
363
|
},
|
|
@@ -384,7 +394,7 @@ export const seamlyStateReducer = (state, action) => {
|
|
|
384
394
|
: state
|
|
385
395
|
|
|
386
396
|
case CLEAR_EVENTS:
|
|
387
|
-
return { ...state, unreadEvents: 0, events: [] }
|
|
397
|
+
return { ...state, unreadEvents: 0, loadedImageEventIds: [], events: [] }
|
|
388
398
|
case SET_EVENTS_READ:
|
|
389
399
|
return {
|
|
390
400
|
...state,
|
|
@@ -404,6 +414,11 @@ export const seamlyStateReducer = (state, action) => {
|
|
|
404
414
|
return event
|
|
405
415
|
}),
|
|
406
416
|
}
|
|
417
|
+
case SET_LOADED_IMAGE_EVENT_IDS:
|
|
418
|
+
return {
|
|
419
|
+
...state,
|
|
420
|
+
loadedImageEventIds: [...state.loadedImageEventIds, action.eventId],
|
|
421
|
+
}
|
|
407
422
|
case SET_HISTORY:
|
|
408
423
|
const {
|
|
409
424
|
events: history,
|
|
@@ -606,7 +621,12 @@ export const seamlyStateReducer = (state, action) => {
|
|
|
606
621
|
headerTitles: headerTitlesReducer(state.headerTitles, action),
|
|
607
622
|
}
|
|
608
623
|
case SET_INITIAL_STATE:
|
|
609
|
-
|
|
624
|
+
const { initialState } = action
|
|
625
|
+
return {
|
|
626
|
+
...state,
|
|
627
|
+
initialState,
|
|
628
|
+
unreadEvents: initialState.unreadMessageCount,
|
|
629
|
+
}
|
|
610
630
|
case SET_SERVICE_DATA_ITEM:
|
|
611
631
|
return {
|
|
612
632
|
...state,
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
height: $messagecountsize;
|
|
11
11
|
transform: scale(1);
|
|
12
12
|
transform-origin: 50% 50%;
|
|
13
|
-
transition: transform 0.3s 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
|
13
|
+
transition: transform 0.3s 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275),
|
|
14
|
+
opacity $transition;
|
|
14
15
|
border-radius: 50%;
|
|
16
|
+
opacity: 1;
|
|
15
17
|
background-color: $negative;
|
|
16
18
|
color: $white;
|
|
17
19
|
font-size: $fontsize-small;
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
|
|
21
23
|
&:empty {
|
|
22
24
|
transform: scale(0);
|
|
23
|
-
transition: transform $transition;
|
|
25
|
+
transition: transform $transition, opacity $transition;
|
|
26
|
+
opacity: 0;
|
|
24
27
|
}
|
|
25
28
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
const path = require('path')
|
|
2
|
+
|
|
3
|
+
module.exports = (env, argv, { PUBLIC_PATH, SRC_ROOT }) => ({
|
|
2
4
|
devtool: 'eval-source-map',
|
|
3
5
|
devServer: {
|
|
4
6
|
headers: { 'Access-Control-Allow-Origin': '*' },
|
|
@@ -9,4 +11,11 @@ module.exports = (env, argv, { PUBLIC_PATH }) => ({
|
|
|
9
11
|
allowedHosts: 'all',
|
|
10
12
|
hot: true,
|
|
11
13
|
},
|
|
14
|
+
resolve: {
|
|
15
|
+
alias: {
|
|
16
|
+
api: path.resolve(SRC_ROOT, 'javascripts/api'),
|
|
17
|
+
domains: path.resolve(SRC_ROOT, 'javascripts/domains'),
|
|
18
|
+
ui: path.resolve(SRC_ROOT, 'javascripts/ui'),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
12
21
|
})
|
package/src/.DS_Store
DELETED
|
Binary file
|