@webitel/ui-chats 26.8.2 → 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.
Files changed (106) hide show
  1. package/package.json +9 -2
  2. package/src/locale/en/en.ts +3 -0
  3. package/src/locale/es/es.ts +3 -0
  4. package/src/locale/kz/kz.ts +3 -0
  5. package/src/locale/pl/pl.ts +3 -0
  6. package/src/locale/ro/ro.ts +3 -0
  7. package/src/locale/ru/ru.ts +3 -0
  8. package/src/locale/uk/uk.ts +3 -0
  9. package/src/locale/uz/uz.ts +3 -0
  10. package/src/locale/vi/vi.ts +3 -0
  11. package/src/shared/composables/__tests__/useChatScroll.spec.ts +164 -0
  12. package/src/shared/composables/__tests__/useObserveHeightUntilStable.spec.ts +48 -0
  13. package/src/{ui/messaging → shared}/composables/useChatScroll.ts +37 -9
  14. package/src/{ui/messaging → shared}/composables/useObserveHeightUntilStable.ts +2 -0
  15. package/src/ui/chat-footer/modules/user-input/components/actions/send-message-action.vue +1 -0
  16. package/src/ui/index.ts +2 -2
  17. package/src/ui/messaging/components/the-messages-container.vue +2 -2
  18. package/src/v2/index.ts +4 -0
  19. package/src/v2/locale/en.ts +29 -0
  20. package/src/v2/locale/es.ts +28 -0
  21. package/src/v2/locale/kz.ts +28 -0
  22. package/src/v2/locale/pl.ts +28 -0
  23. package/src/v2/locale/ro.ts +28 -0
  24. package/src/v2/locale/ru.ts +27 -0
  25. package/src/v2/locale/uk.ts +27 -0
  26. package/src/v2/locale/useChatsV2I18n.ts +14 -0
  27. package/src/v2/locale/uz.ts +28 -0
  28. package/src/v2/locale/vi.ts +28 -0
  29. package/src/v2/scripts/__tests__/attachments.spec.ts +168 -0
  30. package/src/v2/scripts/__tests__/fixtures.ts +56 -0
  31. package/src/v2/scripts/__tests__/formatDate.spec.ts +96 -0
  32. package/src/v2/scripts/__tests__/getMessageStatus.spec.ts +214 -0
  33. package/src/v2/scripts/__tests__/isContactCentreSide.spec.ts +21 -0
  34. package/src/v2/scripts/__tests__/resolveSystemNotice.spec.ts +157 -0
  35. package/src/v2/scripts/__tests__/toHistoryItems.spec.ts +100 -0
  36. package/src/v2/scripts/attachments.ts +144 -0
  37. package/src/v2/scripts/formatDate.ts +74 -0
  38. package/src/v2/scripts/getMessageStatus.ts +58 -0
  39. package/src/v2/scripts/isContactCentreSide.ts +14 -0
  40. package/src/v2/scripts/resolveSystemNotice.ts +119 -0
  41. package/src/v2/scripts/toHistoryItems.ts +63 -0
  42. package/src/v2/scripts/toTimestamp.ts +21 -0
  43. package/src/v2/types/index.ts +74 -0
  44. package/src/v2/ui/__tests__/mountWithChats.ts +38 -0
  45. package/src/v2/ui/chat-composer/__tests__/chat-composer.spec.ts +221 -0
  46. package/src/v2/ui/chat-composer/chat-composer.vue +212 -0
  47. package/src/v2/ui/chat-history/__tests__/chat-history.spec.ts +235 -0
  48. package/src/v2/ui/chat-history/chat-history.vue +262 -0
  49. package/src/v2/ui/chat-history/components/chat-date-divider.vue +36 -0
  50. package/src/v2/ui/chat-history/components/chat-message.vue +148 -0
  51. package/src/v2/ui/chat-history/components/chat-system-notice.vue +100 -0
  52. package/src/v2/ui/chat-history/components/message-attachments.vue +162 -0
  53. package/src/v2/ui/chat-history/components/message-status.vue +31 -0
  54. package/src/v2/ui/chat-history/components/message-text.vue +37 -0
  55. package/src/v2/ui/chat-history/components/scroll-to-bottom-btn.vue +55 -0
  56. package/src/v2/ui/chat-history/gallery.ts +8 -0
  57. package/src/v2/ui/chat-thread/__tests__/chat-thread.spec.ts +122 -0
  58. package/src/v2/ui/chat-thread/chat-thread.vue +111 -0
  59. package/types/.tsbuildinfo +1 -1
  60. package/types/locale/en/en.d.ts +25 -0
  61. package/types/locale/es/es.d.ts +25 -0
  62. package/types/locale/index.d.ts +225 -0
  63. package/types/locale/kz/kz.d.ts +25 -0
  64. package/types/locale/pl/pl.d.ts +25 -0
  65. package/types/locale/ro/ro.d.ts +25 -0
  66. package/types/locale/ru/ru.d.ts +25 -0
  67. package/types/locale/uk/uk.d.ts +25 -0
  68. package/types/locale/uz/uz.d.ts +25 -0
  69. package/types/locale/vi/vi.d.ts +25 -0
  70. package/types/{ui/messaging → shared}/composables/useChatScroll.d.ts +14 -4
  71. package/types/ui/index.d.ts +2 -2
  72. package/types/v2/index.d.ts +4 -0
  73. package/types/v2/locale/en.d.ts +27 -0
  74. package/types/v2/locale/es.d.ts +26 -0
  75. package/types/v2/locale/kz.d.ts +26 -0
  76. package/types/v2/locale/pl.d.ts +26 -0
  77. package/types/v2/locale/ro.d.ts +26 -0
  78. package/types/v2/locale/ru.d.ts +26 -0
  79. package/types/v2/locale/uk.d.ts +26 -0
  80. package/types/v2/locale/useChatsV2I18n.d.ts +5 -0
  81. package/types/v2/locale/uz.d.ts +26 -0
  82. package/types/v2/locale/vi.d.ts +26 -0
  83. package/types/v2/scripts/__tests__/fixtures.d.ts +7 -0
  84. package/types/v2/scripts/attachments.d.ts +30 -0
  85. package/types/v2/scripts/formatDate.d.ts +17 -0
  86. package/types/v2/scripts/getMessageStatus.d.ts +8 -0
  87. package/types/v2/scripts/isContactCentreSide.d.ts +8 -0
  88. package/types/v2/scripts/resolveSystemNotice.d.ts +23 -0
  89. package/types/v2/scripts/toHistoryItems.d.ts +19 -0
  90. package/types/v2/scripts/toTimestamp.d.ts +7 -0
  91. package/types/v2/types/index.d.ts +54 -0
  92. package/types/v2/ui/__tests__/mountWithChats.d.ts +8 -0
  93. package/types/v2/ui/chat-composer/chat-composer.vue.d.ts +31 -0
  94. package/types/v2/ui/chat-history/chat-history.vue.d.ts +11 -0
  95. package/types/v2/ui/chat-history/components/chat-date-divider.vue.d.ts +6 -0
  96. package/types/v2/ui/chat-history/components/chat-message.vue.d.ts +10 -0
  97. package/types/v2/ui/chat-history/components/chat-system-notice.vue.d.ts +9 -0
  98. package/types/v2/ui/chat-history/components/message-attachments.vue.d.ts +7 -0
  99. package/types/v2/ui/chat-history/components/message-status.vue.d.ts +7 -0
  100. package/types/v2/ui/chat-history/components/message-text.vue.d.ts +6 -0
  101. package/types/v2/ui/chat-history/components/scroll-to-bottom-btn.vue.d.ts +12 -0
  102. package/types/v2/ui/chat-history/gallery.d.ts +6 -0
  103. package/types/v2/ui/chat-thread/chat-thread.vue.d.ts +26 -0
  104. /package/src/{ui/messaging → shared}/composables/useScrollToBottomBtn.ts +0 -0
  105. /package/types/{ui/messaging → shared}/composables/useObserveHeightUntilStable.d.ts +0 -0
  106. /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>