@webitel/ui-chats 26.8.3 → 26.8.4
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/package.json +9 -2
- package/src/locale/en/en.ts +3 -0
- package/src/locale/es/es.ts +3 -0
- package/src/locale/kz/kz.ts +3 -0
- package/src/locale/pl/pl.ts +3 -0
- package/src/locale/ro/ro.ts +3 -0
- package/src/locale/ru/ru.ts +3 -0
- package/src/locale/uk/uk.ts +3 -0
- package/src/locale/uz/uz.ts +3 -0
- package/src/locale/vi/vi.ts +3 -0
- package/src/shared/composables/__tests__/useChatScroll.spec.ts +164 -0
- package/src/shared/composables/__tests__/useObserveHeightUntilStable.spec.ts +48 -0
- package/src/{ui/messaging → shared}/composables/useChatScroll.ts +37 -9
- package/src/{ui/messaging → shared}/composables/useObserveHeightUntilStable.ts +2 -0
- package/src/ui/index.ts +2 -2
- package/src/ui/messaging/components/the-messages-container.vue +2 -2
- package/src/v2/index.ts +4 -0
- package/src/v2/locale/en.ts +29 -0
- package/src/v2/locale/es.ts +28 -0
- package/src/v2/locale/kz.ts +28 -0
- package/src/v2/locale/pl.ts +28 -0
- package/src/v2/locale/ro.ts +28 -0
- package/src/v2/locale/ru.ts +27 -0
- package/src/v2/locale/uk.ts +27 -0
- package/src/v2/locale/useChatsV2I18n.ts +14 -0
- package/src/v2/locale/uz.ts +28 -0
- package/src/v2/locale/vi.ts +28 -0
- package/src/v2/scripts/__tests__/attachments.spec.ts +168 -0
- package/src/v2/scripts/__tests__/fixtures.ts +56 -0
- package/src/v2/scripts/__tests__/formatDate.spec.ts +96 -0
- package/src/v2/scripts/__tests__/getMessageStatus.spec.ts +214 -0
- package/src/v2/scripts/__tests__/isContactCentreSide.spec.ts +21 -0
- package/src/v2/scripts/__tests__/resolveSystemNotice.spec.ts +157 -0
- package/src/v2/scripts/__tests__/toHistoryItems.spec.ts +100 -0
- package/src/v2/scripts/attachments.ts +144 -0
- package/src/v2/scripts/formatDate.ts +74 -0
- package/src/v2/scripts/getMessageStatus.ts +58 -0
- package/src/v2/scripts/isContactCentreSide.ts +14 -0
- package/src/v2/scripts/resolveSystemNotice.ts +119 -0
- package/src/v2/scripts/toHistoryItems.ts +63 -0
- package/src/v2/scripts/toTimestamp.ts +21 -0
- package/src/v2/types/index.ts +74 -0
- package/src/v2/ui/__tests__/mountWithChats.ts +38 -0
- package/src/v2/ui/chat-composer/__tests__/chat-composer.spec.ts +221 -0
- package/src/v2/ui/chat-composer/chat-composer.vue +212 -0
- package/src/v2/ui/chat-history/__tests__/chat-history.spec.ts +235 -0
- package/src/v2/ui/chat-history/chat-history.vue +262 -0
- package/src/v2/ui/chat-history/components/chat-date-divider.vue +36 -0
- package/src/v2/ui/chat-history/components/chat-message.vue +148 -0
- package/src/v2/ui/chat-history/components/chat-system-notice.vue +100 -0
- package/src/v2/ui/chat-history/components/message-attachments.vue +162 -0
- package/src/v2/ui/chat-history/components/message-status.vue +31 -0
- package/src/v2/ui/chat-history/components/message-text.vue +37 -0
- package/src/v2/ui/chat-history/components/scroll-to-bottom-btn.vue +55 -0
- package/src/v2/ui/chat-history/gallery.ts +8 -0
- package/src/v2/ui/chat-thread/__tests__/chat-thread.spec.ts +122 -0
- package/src/v2/ui/chat-thread/chat-thread.vue +111 -0
- package/types/.tsbuildinfo +1 -1
- package/types/locale/en/en.d.ts +25 -0
- package/types/locale/es/es.d.ts +25 -0
- package/types/locale/index.d.ts +225 -0
- package/types/locale/kz/kz.d.ts +25 -0
- package/types/locale/pl/pl.d.ts +25 -0
- package/types/locale/ro/ro.d.ts +25 -0
- package/types/locale/ru/ru.d.ts +25 -0
- package/types/locale/uk/uk.d.ts +25 -0
- package/types/locale/uz/uz.d.ts +25 -0
- package/types/locale/vi/vi.d.ts +25 -0
- package/types/{ui/messaging → shared}/composables/useChatScroll.d.ts +14 -4
- package/types/ui/index.d.ts +2 -2
- package/types/v2/index.d.ts +4 -0
- package/types/v2/locale/en.d.ts +27 -0
- package/types/v2/locale/es.d.ts +26 -0
- package/types/v2/locale/kz.d.ts +26 -0
- package/types/v2/locale/pl.d.ts +26 -0
- package/types/v2/locale/ro.d.ts +26 -0
- package/types/v2/locale/ru.d.ts +26 -0
- package/types/v2/locale/uk.d.ts +26 -0
- package/types/v2/locale/useChatsV2I18n.d.ts +5 -0
- package/types/v2/locale/uz.d.ts +26 -0
- package/types/v2/locale/vi.d.ts +26 -0
- package/types/v2/scripts/__tests__/fixtures.d.ts +7 -0
- package/types/v2/scripts/attachments.d.ts +30 -0
- package/types/v2/scripts/formatDate.d.ts +17 -0
- package/types/v2/scripts/getMessageStatus.d.ts +8 -0
- package/types/v2/scripts/isContactCentreSide.d.ts +8 -0
- package/types/v2/scripts/resolveSystemNotice.d.ts +23 -0
- package/types/v2/scripts/toHistoryItems.d.ts +19 -0
- package/types/v2/scripts/toTimestamp.d.ts +7 -0
- package/types/v2/types/index.d.ts +54 -0
- package/types/v2/ui/__tests__/mountWithChats.d.ts +8 -0
- package/types/v2/ui/chat-composer/chat-composer.vue.d.ts +31 -0
- package/types/v2/ui/chat-history/chat-history.vue.d.ts +11 -0
- package/types/v2/ui/chat-history/components/chat-date-divider.vue.d.ts +6 -0
- package/types/v2/ui/chat-history/components/chat-message.vue.d.ts +10 -0
- package/types/v2/ui/chat-history/components/chat-system-notice.vue.d.ts +9 -0
- package/types/v2/ui/chat-history/components/message-attachments.vue.d.ts +7 -0
- package/types/v2/ui/chat-history/components/message-status.vue.d.ts +7 -0
- package/types/v2/ui/chat-history/components/message-text.vue.d.ts +6 -0
- package/types/v2/ui/chat-history/components/scroll-to-bottom-btn.vue.d.ts +12 -0
- package/types/v2/ui/chat-history/gallery.d.ts +6 -0
- package/types/v2/ui/chat-thread/chat-thread.vue.d.ts +26 -0
- /package/src/{ui/messaging → shared}/composables/useScrollToBottomBtn.ts +0 -0
- /package/types/{ui/messaging → shared}/composables/useObserveHeightUntilStable.d.ts +0 -0
- /package/types/{ui/messaging → shared}/composables/useScrollToBottomBtn.d.ts +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { flushPromises } from '@vue/test-utils';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
client,
|
|
6
|
+
message,
|
|
7
|
+
operator,
|
|
8
|
+
thread,
|
|
9
|
+
} from '../../../scripts/__tests__/fixtures';
|
|
10
|
+
import { ChatThreadMode } from '../../../types';
|
|
11
|
+
import { deferred, mountWithChats } from '../../__tests__/mountWithChats';
|
|
12
|
+
import ChatHistory from '../chat-history.vue';
|
|
13
|
+
|
|
14
|
+
vi.mock('@webitel/ui-sdk/components', async (importOriginal) => ({
|
|
15
|
+
...(await importOriginal<typeof import('@webitel/ui-sdk/components')>()),
|
|
16
|
+
WtChatEmoji: {
|
|
17
|
+
name: 'WtChatEmoji',
|
|
18
|
+
emits: [
|
|
19
|
+
'insert-emoji',
|
|
20
|
+
],
|
|
21
|
+
template:
|
|
22
|
+
'<button class="emoji-stub" @click="$emit(\'insert-emoji\', \'🙂\')" />',
|
|
23
|
+
},
|
|
24
|
+
WtGalleria: {
|
|
25
|
+
name: 'WtGalleria',
|
|
26
|
+
props: [
|
|
27
|
+
'visible',
|
|
28
|
+
'activeIndex',
|
|
29
|
+
'value',
|
|
30
|
+
],
|
|
31
|
+
template: '<div class="galleria-stub" />',
|
|
32
|
+
},
|
|
33
|
+
WtVidstackPlayer: {
|
|
34
|
+
name: 'WtVidstackPlayer',
|
|
35
|
+
template: '<div class="player-stub" />',
|
|
36
|
+
},
|
|
37
|
+
WtPlayer: {
|
|
38
|
+
name: 'WtPlayer',
|
|
39
|
+
template: '<div class="player-stub" />',
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const setup = (props = {}) =>
|
|
44
|
+
mountWithChats(ChatHistory, {
|
|
45
|
+
props: {
|
|
46
|
+
thread: thread(),
|
|
47
|
+
messages: [],
|
|
48
|
+
selfMemberId: operator.id,
|
|
49
|
+
...props,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('ChatHistory', () => {
|
|
54
|
+
it('renders dividers, messages and system notices', () => {
|
|
55
|
+
const wrapper = setup({
|
|
56
|
+
messages: [
|
|
57
|
+
message({
|
|
58
|
+
sender: client,
|
|
59
|
+
}),
|
|
60
|
+
message({
|
|
61
|
+
sender: operator,
|
|
62
|
+
system: {
|
|
63
|
+
type: 'transferred',
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
expect(wrapper.findAll('.chat-date-divider')).toHaveLength(1);
|
|
69
|
+
expect(wrapper.findAll('.chat-message')).toHaveLength(1);
|
|
70
|
+
expect(wrapper.findAll('.chat-system-notice')).toHaveLength(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('puts contact-centre messages on the outgoing side', () => {
|
|
74
|
+
const wrapper = setup({
|
|
75
|
+
messages: [
|
|
76
|
+
message({
|
|
77
|
+
sender: client,
|
|
78
|
+
}),
|
|
79
|
+
message({
|
|
80
|
+
sender: operator,
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
const rows = wrapper.findAll('.chat-message');
|
|
85
|
+
expect(rows[0].classes()).not.toContain('chat-message--outgoing');
|
|
86
|
+
expect(rows[1].classes()).toContain('chat-message--outgoing');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('shows ticks only on the operator’s own messages', () => {
|
|
90
|
+
const wrapper = setup({
|
|
91
|
+
messages: [
|
|
92
|
+
message({
|
|
93
|
+
sender: client,
|
|
94
|
+
}),
|
|
95
|
+
message({
|
|
96
|
+
sender: operator,
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
const rows = wrapper.findAll('.chat-message');
|
|
101
|
+
expect(rows[0].find('.message-status').exists()).toBe(false);
|
|
102
|
+
expect(rows[1].find('.message-status').exists()).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('shows a tombstone for deleted messages', () => {
|
|
106
|
+
const wrapper = setup({
|
|
107
|
+
messages: [
|
|
108
|
+
message({
|
|
109
|
+
deleted: true,
|
|
110
|
+
body: undefined,
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
expect(wrapper.text()).toContain('Message deleted');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Review Focus #2
|
|
118
|
+
it('does not render an empty bubble for content it cannot draw', () => {
|
|
119
|
+
const wrapper = setup({
|
|
120
|
+
messages: [
|
|
121
|
+
message({
|
|
122
|
+
body: undefined,
|
|
123
|
+
location: {
|
|
124
|
+
latitude: 1,
|
|
125
|
+
longitude: 2,
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
expect(wrapper.text()).toContain('Unsupported message');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('mounts the gallery only once an image is opened', async () => {
|
|
134
|
+
const wrapper = setup({
|
|
135
|
+
messages: [
|
|
136
|
+
message({
|
|
137
|
+
body: undefined,
|
|
138
|
+
images: [
|
|
139
|
+
{
|
|
140
|
+
id: 'i1',
|
|
141
|
+
url: 'https://x/1.png',
|
|
142
|
+
width: 4,
|
|
143
|
+
height: 3,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
// the gallery carries its own hidden confirm popup; keep it out of the page until needed
|
|
150
|
+
expect(wrapper.find('.galleria-stub').exists()).toBe(false);
|
|
151
|
+
|
|
152
|
+
await wrapper.find('.message-attachments__image').trigger('click');
|
|
153
|
+
expect(wrapper.find('.galleria-stub').exists()).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// the topmost entry anchors the scroll position while older items are prepended;
|
|
157
|
+
// a divider must not, as a same-day page keeps its element in place
|
|
158
|
+
it('marks only message and notice rows as scroll anchors', () => {
|
|
159
|
+
const wrapper = setup({
|
|
160
|
+
messages: [
|
|
161
|
+
message({
|
|
162
|
+
sender: client,
|
|
163
|
+
}),
|
|
164
|
+
message({
|
|
165
|
+
sender: operator,
|
|
166
|
+
system: {
|
|
167
|
+
type: 'transferred',
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
const entries = wrapper.findAll('.chat-history__entry');
|
|
173
|
+
expect(entries).toHaveLength(2);
|
|
174
|
+
expect(entries[0].find('.chat-message').exists()).toBe(true);
|
|
175
|
+
expect(
|
|
176
|
+
wrapper.find('.chat-history__entry .chat-date-divider').exists(),
|
|
177
|
+
).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('shows the waiting line only in awaiting mode', async () => {
|
|
181
|
+
const wrapper = setup({
|
|
182
|
+
messages: [
|
|
183
|
+
message(),
|
|
184
|
+
],
|
|
185
|
+
mode: ChatThreadMode.Awaiting,
|
|
186
|
+
});
|
|
187
|
+
expect(wrapper.find('.chat-history__awaiting').exists()).toBe(true);
|
|
188
|
+
await wrapper.setProps({
|
|
189
|
+
mode: ChatThreadMode.Active,
|
|
190
|
+
});
|
|
191
|
+
expect(wrapper.find('.chat-history__awaiting').exists()).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('shows the top sentinel only while there is more history', async () => {
|
|
195
|
+
const wrapper = setup({
|
|
196
|
+
messages: [
|
|
197
|
+
message(),
|
|
198
|
+
],
|
|
199
|
+
hasMore: false,
|
|
200
|
+
});
|
|
201
|
+
expect(wrapper.find('.chat-history__sentinel').exists()).toBe(false);
|
|
202
|
+
await wrapper.setProps({
|
|
203
|
+
hasMore: true,
|
|
204
|
+
});
|
|
205
|
+
expect(wrapper.find('.chat-history__sentinel').exists()).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('shows one spinner while onLoadMore is pending and stops when it settles, even on failure', async () => {
|
|
209
|
+
const pending = deferred();
|
|
210
|
+
const onLoadMore = vi.fn(() => pending.promise);
|
|
211
|
+
const wrapper = setup({
|
|
212
|
+
messages: [
|
|
213
|
+
message(),
|
|
214
|
+
],
|
|
215
|
+
hasMore: true,
|
|
216
|
+
onLoadMore,
|
|
217
|
+
});
|
|
218
|
+
wrapper
|
|
219
|
+
.findComponent({
|
|
220
|
+
name: 'WtIntersectionObserver',
|
|
221
|
+
})
|
|
222
|
+
.vm.$emit('next');
|
|
223
|
+
await flushPromises();
|
|
224
|
+
expect(onLoadMore).toHaveBeenCalledTimes(1);
|
|
225
|
+
expect(
|
|
226
|
+
wrapper.find('.chat-history__sentinel').findAll('.wt-loader'),
|
|
227
|
+
).toHaveLength(1);
|
|
228
|
+
|
|
229
|
+
pending.reject(new Error('offline'));
|
|
230
|
+
await flushPromises();
|
|
231
|
+
expect(
|
|
232
|
+
wrapper.find('.chat-history__sentinel').findAll('.wt-loader'),
|
|
233
|
+
).toHaveLength(0);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="chat-history">
|
|
3
|
+
<div
|
|
4
|
+
ref="scroll-container"
|
|
5
|
+
class="chat-history__scroll wt-scrollbar"
|
|
6
|
+
@scroll="handleChatScroll"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
ref="scroll-content"
|
|
10
|
+
class="chat-history__content"
|
|
11
|
+
>
|
|
12
|
+
<!-- the observer draws its own loader while loading -->
|
|
13
|
+
<div
|
|
14
|
+
v-if="props.hasMore"
|
|
15
|
+
class="chat-history__sentinel"
|
|
16
|
+
>
|
|
17
|
+
<wt-intersection-observer
|
|
18
|
+
:can-load-more="props.hasMore && !isLoadingMore"
|
|
19
|
+
:loading="isLoadingMore"
|
|
20
|
+
@next="handleLoadMore"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div
|
|
25
|
+
v-for="item of items"
|
|
26
|
+
:key="item.key"
|
|
27
|
+
class="chat-history__item"
|
|
28
|
+
:class="{ 'chat-history__entry': item.kind !== 'divider' }"
|
|
29
|
+
>
|
|
30
|
+
<chat-date-divider
|
|
31
|
+
v-if="item.kind === 'divider'"
|
|
32
|
+
:date="item.date"
|
|
33
|
+
/>
|
|
34
|
+
<chat-system-notice
|
|
35
|
+
v-else-if="item.kind === 'system'"
|
|
36
|
+
:message="item.message"
|
|
37
|
+
:thread="props.thread"
|
|
38
|
+
:resolve-avatar-url="props.resolveAvatarUrl"
|
|
39
|
+
/>
|
|
40
|
+
<chat-message
|
|
41
|
+
v-else
|
|
42
|
+
:message="item.message"
|
|
43
|
+
:thread="props.thread"
|
|
44
|
+
:self-member-id="props.selfMemberId"
|
|
45
|
+
:resolve-avatar-url="props.resolveAvatarUrl"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
v-if="props.mode === ChatThreadMode.Awaiting"
|
|
51
|
+
class="chat-history__awaiting typo-caption"
|
|
52
|
+
>
|
|
53
|
+
{{ t('history.waitingForOperator') }}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<scroll-to-bottom-btn
|
|
59
|
+
v-if="showScrollToBottomBtn"
|
|
60
|
+
:new-message-count="newUnseenMessagesCount"
|
|
61
|
+
@scroll="scrollToBottom('smooth')"
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
<!-- mounted only while open: the gallery carries a hidden confirm popup and
|
|
65
|
+
is an async component, neither belongs on every chat page -->
|
|
66
|
+
<wt-galleria
|
|
67
|
+
v-if="isGalleryOpen"
|
|
68
|
+
v-model:visible="isGalleryOpen"
|
|
69
|
+
v-model:active-index="galleryIndex"
|
|
70
|
+
:value="galleryItems"
|
|
71
|
+
delete-disabled
|
|
72
|
+
@download="downloadGalleryImage"
|
|
73
|
+
/>
|
|
74
|
+
</section>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<script setup lang="ts">
|
|
78
|
+
import { WtGalleria, WtIntersectionObserver } from '@webitel/ui-sdk/components';
|
|
79
|
+
import { computed, provide, ref, useTemplateRef, watch } from 'vue';
|
|
80
|
+
|
|
81
|
+
import { useChatScroll } from '../../../shared/composables/useChatScroll';
|
|
82
|
+
import { useObserveHeightUntilStable } from '../../../shared/composables/useObserveHeightUntilStable';
|
|
83
|
+
import { useChatsV2I18n } from '../../locale/useChatsV2I18n';
|
|
84
|
+
import { collectGalleryImages } from '../../scripts/attachments';
|
|
85
|
+
import { toHistoryItems } from '../../scripts/toHistoryItems';
|
|
86
|
+
import {
|
|
87
|
+
type ChatHistoryProps,
|
|
88
|
+
ChatThreadMode,
|
|
89
|
+
type MessageModel,
|
|
90
|
+
} from '../../types';
|
|
91
|
+
import ChatDateDivider from './components/chat-date-divider.vue';
|
|
92
|
+
import ChatMessage from './components/chat-message.vue';
|
|
93
|
+
import ChatSystemNotice from './components/chat-system-notice.vue';
|
|
94
|
+
import ScrollToBottomBtn from './components/scroll-to-bottom-btn.vue';
|
|
95
|
+
import { ChatGalleryKey } from './gallery';
|
|
96
|
+
|
|
97
|
+
/** how long after (re)opening a thread late layout shifts still pin the bottom */
|
|
98
|
+
const SETTLE_TIMEOUT_MS = 2000;
|
|
99
|
+
|
|
100
|
+
const props = withDefaults(defineProps<ChatHistoryProps>(), {
|
|
101
|
+
mode: ChatThreadMode.Active,
|
|
102
|
+
hasMore: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const emit = defineEmits<{
|
|
106
|
+
seen: [
|
|
107
|
+
message: MessageModel,
|
|
108
|
+
];
|
|
109
|
+
}>();
|
|
110
|
+
|
|
111
|
+
const { t } = useChatsV2I18n();
|
|
112
|
+
|
|
113
|
+
const scrollContainer = useTemplateRef<HTMLElement>('scroll-container');
|
|
114
|
+
const scrollContent = useTemplateRef<HTMLElement>('scroll-content');
|
|
115
|
+
|
|
116
|
+
const items = computed(() => toHistoryItems(props.messages));
|
|
117
|
+
|
|
118
|
+
const isLoadingMore = ref(false);
|
|
119
|
+
|
|
120
|
+
let lastSeenId: string | null = null;
|
|
121
|
+
|
|
122
|
+
function handleSeen() {
|
|
123
|
+
const newest = props.messages.at(-1);
|
|
124
|
+
if (!newest || newest.id === lastSeenId) return;
|
|
125
|
+
lastSeenId = newest.id;
|
|
126
|
+
emit('seen', newest);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const {
|
|
130
|
+
showScrollToBottomBtn,
|
|
131
|
+
newUnseenMessagesCount,
|
|
132
|
+
scrollToBottom,
|
|
133
|
+
loadNextMessages,
|
|
134
|
+
handleChatScroll,
|
|
135
|
+
} = useChatScroll<MessageModel>({
|
|
136
|
+
chatContainer: scrollContainer,
|
|
137
|
+
chatContent: scrollContent,
|
|
138
|
+
messages: computed(() => props.messages),
|
|
139
|
+
isSelf: (message) =>
|
|
140
|
+
!!message.sender?.id && message.sender.id === props.selfMemberId,
|
|
141
|
+
// a divider must not anchor: a same-day older page keeps its element in place
|
|
142
|
+
itemClass: 'chat-history__entry',
|
|
143
|
+
chatId: computed(() => props.thread.id),
|
|
144
|
+
isChatClosed: computed(() => false),
|
|
145
|
+
isLoading: isLoadingMore,
|
|
146
|
+
onBeforeStart: ({ scrollToBottom }) => {
|
|
147
|
+
scrollToBottom();
|
|
148
|
+
startObserve();
|
|
149
|
+
},
|
|
150
|
+
onSeen: handleSeen,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const { startObserve } = useObserveHeightUntilStable(
|
|
154
|
+
scrollContainer,
|
|
155
|
+
() => scrollToBottom('instant'),
|
|
156
|
+
SETTLE_TIMEOUT_MS,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
function handleLoadMore() {
|
|
160
|
+
const onLoadMore = props.onLoadMore;
|
|
161
|
+
if (!onLoadMore || isLoadingMore.value) return;
|
|
162
|
+
|
|
163
|
+
loadNextMessages(props.hasMore, async () => {
|
|
164
|
+
isLoadingMore.value = true;
|
|
165
|
+
try {
|
|
166
|
+
await onLoadMore();
|
|
167
|
+
} catch {
|
|
168
|
+
// the app owns error reporting; the sentinel retries on next sight
|
|
169
|
+
} finally {
|
|
170
|
+
isLoadingMore.value = false;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const galleryImages = computed(() => collectGalleryImages(props.messages));
|
|
176
|
+
|
|
177
|
+
const galleryItems = computed(() =>
|
|
178
|
+
galleryImages.value.map(({ src }) => ({
|
|
179
|
+
src,
|
|
180
|
+
thumbnailSrc: src,
|
|
181
|
+
title: '',
|
|
182
|
+
})),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const isGalleryOpen = ref(false);
|
|
186
|
+
const galleryIndex = ref(0);
|
|
187
|
+
|
|
188
|
+
provide(ChatGalleryKey, {
|
|
189
|
+
open: (imageKey) => {
|
|
190
|
+
const index = galleryImages.value.findIndex(
|
|
191
|
+
(image) => image.key === imageKey,
|
|
192
|
+
);
|
|
193
|
+
if (index < 0) return;
|
|
194
|
+
galleryIndex.value = index;
|
|
195
|
+
isGalleryOpen.value = true;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
function downloadGalleryImage(index: number) {
|
|
200
|
+
const image = galleryImages.value[index];
|
|
201
|
+
if (image) window.open(image.src, '_blank', 'noopener');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
watch(
|
|
205
|
+
() => props.thread.id,
|
|
206
|
+
() => {
|
|
207
|
+
lastSeenId = null;
|
|
208
|
+
isGalleryOpen.value = false;
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<style scoped>
|
|
214
|
+
.chat-history {
|
|
215
|
+
position: relative;
|
|
216
|
+
display: flex;
|
|
217
|
+
min-height: 0;
|
|
218
|
+
overflow: hidden;
|
|
219
|
+
background: var(--wt-ws-chat-window-colors-root-background);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.chat-history__scroll {
|
|
223
|
+
box-sizing: border-box;
|
|
224
|
+
width: 100%;
|
|
225
|
+
overflow-x: hidden;
|
|
226
|
+
overflow-y: auto;
|
|
227
|
+
padding-right: var(--scrollbar-width);
|
|
228
|
+
scrollbar-gutter: stable both-edges;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.chat-history__content {
|
|
232
|
+
display: flex;
|
|
233
|
+
flex-direction: column;
|
|
234
|
+
gap: var(--wt-ws-chat-window-sizes-root-gap);
|
|
235
|
+
padding: var(--wt-ws-chat-window-sizes-root-padding-y) var(--wt-ws-chat-window-sizes-root-padding-x);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.chat-history__sentinel {
|
|
239
|
+
/* reserve the loader's height so its appearance does not shift the rows (WTEL-5366) */
|
|
240
|
+
display: flex;
|
|
241
|
+
flex-direction: column;
|
|
242
|
+
align-items: center;
|
|
243
|
+
justify-content: flex-end;
|
|
244
|
+
min-height: calc(var(--spacing-lg) * 2 + var(--icon-md-size));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.chat-history__awaiting {
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: var(--spacing-xs);
|
|
251
|
+
color: var(--text-disabled-color);
|
|
252
|
+
font-style: italic;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.chat-history__awaiting::before,
|
|
256
|
+
.chat-history__awaiting::after {
|
|
257
|
+
content: '';
|
|
258
|
+
flex: 1;
|
|
259
|
+
height: 1px;
|
|
260
|
+
background: var(--p-divider-border-color);
|
|
261
|
+
}
|
|
262
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="chat-date-divider">
|
|
3
|
+
<wt-chip :color="ChipColor.SECONDARY">
|
|
4
|
+
{{ label }}
|
|
5
|
+
</wt-chip>
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { WtChip } from '@webitel/ui-sdk/components';
|
|
11
|
+
import { ChipColor } from '@webitel/ui-sdk/enums';
|
|
12
|
+
import { computed } from 'vue';
|
|
13
|
+
|
|
14
|
+
import { useChatsV2I18n } from '../../../locale/useChatsV2I18n';
|
|
15
|
+
import { formatDividerDate } from '../../../scripts/formatDate';
|
|
16
|
+
|
|
17
|
+
const props = defineProps<{
|
|
18
|
+
date: number;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const { t, locale } = useChatsV2I18n();
|
|
22
|
+
|
|
23
|
+
const label = computed(() =>
|
|
24
|
+
formatDividerDate(props.date, {
|
|
25
|
+
locale: locale.value,
|
|
26
|
+
todayLabel: t('history.today'),
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
.chat-date-divider {
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="chat-message"
|
|
4
|
+
:class="{ 'chat-message--outgoing': isOutgoing }"
|
|
5
|
+
>
|
|
6
|
+
<wt-avatar
|
|
7
|
+
class="chat-message__avatar"
|
|
8
|
+
size="sm"
|
|
9
|
+
:src="avatarUrl"
|
|
10
|
+
:username="authorName"
|
|
11
|
+
/>
|
|
12
|
+
<div class="chat-message__bubble">
|
|
13
|
+
<p
|
|
14
|
+
v-if="props.message.deleted"
|
|
15
|
+
class="chat-message__placeholder typo-body-1"
|
|
16
|
+
>
|
|
17
|
+
{{ t('history.deletedMessage') }}
|
|
18
|
+
</p>
|
|
19
|
+
<template v-else>
|
|
20
|
+
<message-attachments
|
|
21
|
+
v-if="withAttachments"
|
|
22
|
+
:message="props.message"
|
|
23
|
+
/>
|
|
24
|
+
<message-text
|
|
25
|
+
v-if="props.message.body"
|
|
26
|
+
:text="props.message.body"
|
|
27
|
+
/>
|
|
28
|
+
<p
|
|
29
|
+
v-else-if="!withAttachments"
|
|
30
|
+
class="chat-message__placeholder typo-body-1"
|
|
31
|
+
>
|
|
32
|
+
{{ t('history.unsupportedMessage') }}
|
|
33
|
+
</p>
|
|
34
|
+
</template>
|
|
35
|
+
<div class="chat-message__meta typo-caption">
|
|
36
|
+
<span v-if="time">{{ time }}</span>
|
|
37
|
+
<message-status
|
|
38
|
+
v-if="status"
|
|
39
|
+
:status="status"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import { WtAvatar } from '@webitel/ui-sdk/components';
|
|
48
|
+
import { computed } from 'vue';
|
|
49
|
+
|
|
50
|
+
import { useChatsV2I18n } from '../../../locale/useChatsV2I18n';
|
|
51
|
+
import { hasAttachments } from '../../../scripts/attachments';
|
|
52
|
+
import { formatMessageTime } from '../../../scripts/formatDate';
|
|
53
|
+
import { getMessageStatus } from '../../../scripts/getMessageStatus';
|
|
54
|
+
import { isContactCentreSide } from '../../../scripts/isContactCentreSide';
|
|
55
|
+
import { participantName } from '../../../scripts/resolveSystemNotice';
|
|
56
|
+
import { toTimestamp } from '../../../scripts/toTimestamp';
|
|
57
|
+
import type {
|
|
58
|
+
MessageModel,
|
|
59
|
+
ResolveAvatarUrl,
|
|
60
|
+
ThreadModel,
|
|
61
|
+
} from '../../../types';
|
|
62
|
+
import MessageAttachments from './message-attachments.vue';
|
|
63
|
+
import MessageStatus from './message-status.vue';
|
|
64
|
+
import MessageText from './message-text.vue';
|
|
65
|
+
|
|
66
|
+
const props = defineProps<{
|
|
67
|
+
message: MessageModel;
|
|
68
|
+
thread: ThreadModel;
|
|
69
|
+
selfMemberId: string;
|
|
70
|
+
resolveAvatarUrl?: ResolveAvatarUrl;
|
|
71
|
+
}>();
|
|
72
|
+
|
|
73
|
+
const { t } = useChatsV2I18n();
|
|
74
|
+
|
|
75
|
+
const isOutgoing = computed(() => isContactCentreSide(props.message.sender));
|
|
76
|
+
|
|
77
|
+
const authorName = computed(() => participantName(props.message.sender));
|
|
78
|
+
|
|
79
|
+
const avatarUrl = computed(() =>
|
|
80
|
+
props.message.sender
|
|
81
|
+
? props.resolveAvatarUrl?.(props.message.sender)
|
|
82
|
+
: undefined,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const withAttachments = computed(() => hasAttachments(props.message));
|
|
86
|
+
|
|
87
|
+
const time = computed(() => {
|
|
88
|
+
const timestamp = toTimestamp(props.message.createdAt);
|
|
89
|
+
return timestamp === null ? '' : formatMessageTime(timestamp);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const status = computed(() =>
|
|
93
|
+
getMessageStatus(props.message, props.thread, props.selfMemberId),
|
|
94
|
+
);
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<style scoped>
|
|
98
|
+
.chat-message {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: flex-start;
|
|
101
|
+
gap: var(--wt-ws-chat-window-sizes-message-item-gap);
|
|
102
|
+
padding: var(--wt-ws-chat-window-sizes-message-item-padding-y) var(--wt-ws-chat-window-sizes-message-item-padding-x);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.chat-message--outgoing {
|
|
106
|
+
flex-direction: row-reverse;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.chat-message__avatar {
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.chat-message__bubble {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
gap: var(--wt-ws-chat-window-sizes-message-item-chat-message-gap);
|
|
117
|
+
max-width: 70%;
|
|
118
|
+
min-width: 0;
|
|
119
|
+
padding: var(--wt-ws-chat-window-sizes-message-item-chat-message-padding-y) var(--wt-ws-chat-window-sizes-message-item-chat-message-padding-x);
|
|
120
|
+
border-radius: var(--wt-ws-chat-window-sizes-message-item-chat-message-border-radius);
|
|
121
|
+
background: var(--wt-ws-chat-window-colors-message-item-chat-message-client-background);
|
|
122
|
+
color: var(--wt-ws-chat-window-colors-message-item-chat-message-client-color);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* the corner next to the avatar is square, as in DES-730 */
|
|
126
|
+
.chat-message:not(.chat-message--outgoing) .chat-message__bubble {
|
|
127
|
+
border-top-left-radius: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.chat-message--outgoing .chat-message__bubble {
|
|
131
|
+
border-top-right-radius: 0;
|
|
132
|
+
background: var(--wt-ws-chat-window-colors-message-item-chat-message-agent-background);
|
|
133
|
+
color: var(--wt-ws-chat-window-colors-message-item-chat-message-agent-color);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.chat-message__placeholder {
|
|
137
|
+
margin: 0;
|
|
138
|
+
font-style: italic;
|
|
139
|
+
opacity: 0.7;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.chat-message__meta {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
justify-content: flex-end;
|
|
146
|
+
gap: var(--spacing-2xs);
|
|
147
|
+
}
|
|
148
|
+
</style>
|