@webitel/ui-chats 26.8.3 → 26.8.5

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 (107) 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/index.ts +2 -2
  16. package/src/ui/messaging/components/the-messages-container.vue +2 -2
  17. package/src/v2/index.ts +4 -0
  18. package/src/v2/locale/en.ts +29 -0
  19. package/src/v2/locale/es.ts +28 -0
  20. package/src/v2/locale/kz.ts +28 -0
  21. package/src/v2/locale/pl.ts +28 -0
  22. package/src/v2/locale/ro.ts +28 -0
  23. package/src/v2/locale/ru.ts +27 -0
  24. package/src/v2/locale/uk.ts +27 -0
  25. package/src/v2/locale/useChatsV2I18n.ts +14 -0
  26. package/src/v2/locale/uz.ts +28 -0
  27. package/src/v2/locale/vi.ts +28 -0
  28. package/src/v2/scripts/__tests__/attachments.spec.ts +168 -0
  29. package/src/v2/scripts/__tests__/fixtures.ts +56 -0
  30. package/src/v2/scripts/__tests__/formatDate.spec.ts +96 -0
  31. package/src/v2/scripts/__tests__/getMessageStatus.spec.ts +304 -0
  32. package/src/v2/scripts/__tests__/isContactCentreSide.spec.ts +21 -0
  33. package/src/v2/scripts/__tests__/resolveSystemNotice.spec.ts +157 -0
  34. package/src/v2/scripts/__tests__/toHistoryItems.spec.ts +100 -0
  35. package/src/v2/scripts/attachments.ts +144 -0
  36. package/src/v2/scripts/formatDate.ts +74 -0
  37. package/src/v2/scripts/getMessageStatus.ts +59 -0
  38. package/src/v2/scripts/isContactCentreSide.ts +14 -0
  39. package/src/v2/scripts/isSelfMessage.ts +29 -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 +334 -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 +156 -0
  51. package/src/v2/ui/chat-history/components/chat-system-notice.vue +111 -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/isSelfMessage.d.ts +10 -0
  89. package/types/v2/scripts/resolveSystemNotice.d.ts +23 -0
  90. package/types/v2/scripts/toHistoryItems.d.ts +19 -0
  91. package/types/v2/scripts/toTimestamp.d.ts +7 -0
  92. package/types/v2/types/index.d.ts +54 -0
  93. package/types/v2/ui/__tests__/mountWithChats.d.ts +8 -0
  94. package/types/v2/ui/chat-composer/chat-composer.vue.d.ts +31 -0
  95. package/types/v2/ui/chat-history/chat-history.vue.d.ts +11 -0
  96. package/types/v2/ui/chat-history/components/chat-date-divider.vue.d.ts +6 -0
  97. package/types/v2/ui/chat-history/components/chat-message.vue.d.ts +10 -0
  98. package/types/v2/ui/chat-history/components/chat-system-notice.vue.d.ts +9 -0
  99. package/types/v2/ui/chat-history/components/message-attachments.vue.d.ts +7 -0
  100. package/types/v2/ui/chat-history/components/message-status.vue.d.ts +7 -0
  101. package/types/v2/ui/chat-history/components/message-text.vue.d.ts +6 -0
  102. package/types/v2/ui/chat-history/components/scroll-to-bottom-btn.vue.d.ts +12 -0
  103. package/types/v2/ui/chat-history/gallery.d.ts +6 -0
  104. package/types/v2/ui/chat-thread/chat-thread.vue.d.ts +26 -0
  105. /package/src/{ui/messaging → shared}/composables/useScrollToBottomBtn.ts +0 -0
  106. /package/types/{ui/messaging → shared}/composables/useObserveHeightUntilStable.d.ts +0 -0
  107. /package/types/{ui/messaging → shared}/composables/useScrollToBottomBtn.d.ts +0 -0
@@ -0,0 +1,156 @@
1
+ <template>
2
+ <div
3
+ class="chat-message"
4
+ :class="{ 'chat-message--outgoing': isOutgoing }"
5
+ >
6
+ <wt-tooltip
7
+ class="chat-message__avatar"
8
+ placement="top"
9
+ :disabled="!authorName"
10
+ >
11
+ <template #activator>
12
+ <wt-avatar
13
+ size="sm"
14
+ :src="avatarUrl"
15
+ :username="authorName"
16
+ />
17
+ </template>
18
+ {{ authorName }}
19
+ </wt-tooltip>
20
+ <div class="chat-message__bubble">
21
+ <p
22
+ v-if="props.message.deleted"
23
+ class="chat-message__placeholder typo-body-1"
24
+ >
25
+ {{ t('history.deletedMessage') }}
26
+ </p>
27
+ <template v-else>
28
+ <message-attachments
29
+ v-if="withAttachments"
30
+ :message="props.message"
31
+ />
32
+ <message-text
33
+ v-if="props.message.body"
34
+ :text="props.message.body"
35
+ />
36
+ <p
37
+ v-else-if="!withAttachments"
38
+ class="chat-message__placeholder typo-body-1"
39
+ >
40
+ {{ t('history.unsupportedMessage') }}
41
+ </p>
42
+ </template>
43
+ <div class="chat-message__meta typo-caption">
44
+ <span v-if="time">{{ time }}</span>
45
+ <message-status
46
+ v-if="status"
47
+ :status="status"
48
+ />
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </template>
53
+
54
+ <script setup lang="ts">
55
+ import { WtAvatar, WtTooltip } from '@webitel/ui-sdk/components';
56
+ import { computed } from 'vue';
57
+
58
+ import { useChatsV2I18n } from '../../../locale/useChatsV2I18n';
59
+ import { hasAttachments } from '../../../scripts/attachments';
60
+ import { formatMessageTime } from '../../../scripts/formatDate';
61
+ import { getMessageStatus } from '../../../scripts/getMessageStatus';
62
+ import { isContactCentreSide } from '../../../scripts/isContactCentreSide';
63
+ import { participantName } from '../../../scripts/resolveSystemNotice';
64
+ import { toTimestamp } from '../../../scripts/toTimestamp';
65
+ import type {
66
+ MessageModel,
67
+ ResolveAvatarUrl,
68
+ ThreadModel,
69
+ } from '../../../types';
70
+ import MessageAttachments from './message-attachments.vue';
71
+ import MessageStatus from './message-status.vue';
72
+ import MessageText from './message-text.vue';
73
+
74
+ const props = defineProps<{
75
+ message: MessageModel;
76
+ thread: ThreadModel;
77
+ selfMemberId: string;
78
+ resolveAvatarUrl?: ResolveAvatarUrl;
79
+ }>();
80
+
81
+ const { t } = useChatsV2I18n();
82
+
83
+ const isOutgoing = computed(() => isContactCentreSide(props.message.sender));
84
+
85
+ const authorName = computed(() => participantName(props.message.sender));
86
+
87
+ const avatarUrl = computed(() =>
88
+ props.message.sender
89
+ ? props.resolveAvatarUrl?.(props.message.sender)
90
+ : undefined,
91
+ );
92
+
93
+ const withAttachments = computed(() => hasAttachments(props.message));
94
+
95
+ const time = computed(() => {
96
+ const timestamp = toTimestamp(props.message.createdAt);
97
+ return timestamp === null ? '' : formatMessageTime(timestamp);
98
+ });
99
+
100
+ const status = computed(() =>
101
+ getMessageStatus(props.message, props.thread, props.selfMemberId),
102
+ );
103
+ </script>
104
+
105
+ <style scoped>
106
+ .chat-message {
107
+ display: flex;
108
+ align-items: flex-start;
109
+ gap: var(--wt-ws-chat-window-sizes-message-item-gap);
110
+ padding: var(--wt-ws-chat-window-sizes-message-item-padding-y) var(--wt-ws-chat-window-sizes-message-item-padding-x);
111
+ }
112
+
113
+ .chat-message--outgoing {
114
+ flex-direction: row-reverse;
115
+ }
116
+
117
+ .chat-message__avatar {
118
+ flex-shrink: 0;
119
+ }
120
+
121
+ .chat-message__bubble {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: var(--wt-ws-chat-window-sizes-message-item-chat-message-gap);
125
+ max-width: 70%;
126
+ min-width: 0;
127
+ 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);
128
+ border-radius: var(--wt-ws-chat-window-sizes-message-item-chat-message-border-radius);
129
+ background: var(--wt-ws-chat-window-colors-message-item-chat-message-client-background);
130
+ color: var(--wt-ws-chat-window-colors-message-item-chat-message-client-color);
131
+ }
132
+
133
+ /* the corner next to the avatar is square, as in DES-730 */
134
+ .chat-message:not(.chat-message--outgoing) .chat-message__bubble {
135
+ border-top-left-radius: 0;
136
+ }
137
+
138
+ .chat-message--outgoing .chat-message__bubble {
139
+ border-top-right-radius: 0;
140
+ background: var(--wt-ws-chat-window-colors-message-item-chat-message-agent-background);
141
+ color: var(--wt-ws-chat-window-colors-message-item-chat-message-agent-color);
142
+ }
143
+
144
+ .chat-message__placeholder {
145
+ margin: 0;
146
+ font-style: italic;
147
+ opacity: 0.7;
148
+ }
149
+
150
+ .chat-message__meta {
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: flex-end;
154
+ gap: var(--spacing-2xs);
155
+ }
156
+ </style>
@@ -0,0 +1,111 @@
1
+ <template>
2
+ <div
3
+ class="chat-system-notice"
4
+ :class="`chat-system-notice--${notice.tone}`"
5
+ >
6
+ <span class="chat-system-notice__line" />
7
+ <span class="chat-system-notice__content typo-caption">
8
+ <wt-tooltip
9
+ v-if="notice.actor"
10
+ class="chat-system-notice__avatar"
11
+ placement="top"
12
+ :disabled="!actorRealName"
13
+ >
14
+ <template #activator>
15
+ <wt-avatar
16
+ size="2xs"
17
+ :src="props.resolveAvatarUrl?.(notice.actor)"
18
+ :username="actorName"
19
+ />
20
+ </template>
21
+ {{ actorRealName }}
22
+ </wt-tooltip>
23
+ <span class="chat-system-notice__text">{{ text }}</span>
24
+ <span
25
+ v-if="time"
26
+ class="chat-system-notice__time"
27
+ >• {{ time }}</span>
28
+ </span>
29
+ <span class="chat-system-notice__line" />
30
+ </div>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { WtAvatar, WtTooltip } from '@webitel/ui-sdk/components';
35
+ import { computed } from 'vue';
36
+
37
+ import { useChatsV2I18n } from '../../../locale/useChatsV2I18n';
38
+ import { formatMessageTime } from '../../../scripts/formatDate';
39
+ import {
40
+ participantName,
41
+ resolveSystemNotice,
42
+ } from '../../../scripts/resolveSystemNotice';
43
+ import { toTimestamp } from '../../../scripts/toTimestamp';
44
+ import type {
45
+ MessageModel,
46
+ ResolveAvatarUrl,
47
+ ThreadModel,
48
+ } from '../../../types';
49
+
50
+ const props = defineProps<{
51
+ message: MessageModel;
52
+ thread: ThreadModel;
53
+ resolveAvatarUrl?: ResolveAvatarUrl;
54
+ }>();
55
+
56
+ const { t } = useChatsV2I18n();
57
+
58
+ const notice = computed(() => resolveSystemNotice(props.message, props.thread));
59
+
60
+ // the actor's own name; the tooltip stays off for the generic "system" fallback
61
+ const actorRealName = computed(() => participantName(notice.value.actor));
62
+
63
+ const actorName = computed(
64
+ () => actorRealName.value || t('systemNotice.system'),
65
+ );
66
+
67
+ const text = computed(() =>
68
+ notice.value.textKey
69
+ ? t(notice.value.textKey, {
70
+ actor: actorName.value,
71
+ })
72
+ : (notice.value.fallbackText ?? ''),
73
+ );
74
+
75
+ const time = computed(() => {
76
+ const timestamp = toTimestamp(props.message.createdAt);
77
+ return timestamp === null ? '' : formatMessageTime(timestamp);
78
+ });
79
+ </script>
80
+
81
+ <style scoped>
82
+ .chat-system-notice {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: var(--spacing-xs);
86
+ color: var(--text-disabled-color);
87
+ font-style: italic;
88
+ }
89
+
90
+ .chat-system-notice__line {
91
+ flex: 1;
92
+ height: 1px;
93
+ background: var(--p-divider-border-color);
94
+ }
95
+
96
+ .chat-system-notice__content {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: var(--spacing-2xs);
100
+ max-width: 70%;
101
+ text-align: center;
102
+ }
103
+
104
+ .chat-system-notice--positive {
105
+ color: var(--text-success-color);
106
+ }
107
+
108
+ .chat-system-notice--negative {
109
+ color: var(--text-error-color);
110
+ }
111
+ </style>
@@ -0,0 +1,162 @@
1
+ <template>
2
+ <div class="message-attachments">
3
+ <div
4
+ v-if="attachments.images.length"
5
+ class="message-attachments__images"
6
+ :class="{ 'message-attachments__images--single': attachments.images.length === 1 }"
7
+ >
8
+ <button
9
+ v-for="{ key, image } of attachments.images"
10
+ :key="key"
11
+ type="button"
12
+ class="message-attachments__image"
13
+ :style="aspectRatio(image)"
14
+ @click="gallery?.open(key)"
15
+ >
16
+ <img
17
+ :src="image.url"
18
+ alt=""
19
+ loading="lazy"
20
+ >
21
+ </button>
22
+ </div>
23
+
24
+ <template
25
+ v-for="{ key, document, kind } of attachments.media"
26
+ :key="key"
27
+ >
28
+ <wt-vidstack-player
29
+ v-if="kind === 'video'"
30
+ class="message-attachments__media"
31
+ :src="document.url"
32
+ :mime="document.mime"
33
+ :title="document.name"
34
+ :size="ComponentSize.SM"
35
+ countdown-time-mode
36
+ static
37
+ hide-expand
38
+ stretch
39
+ />
40
+ <wt-player
41
+ v-else
42
+ class="message-attachments__media"
43
+ :src="document.url"
44
+ :autoplay="false"
45
+ :closable="false"
46
+ countdown-time-mode
47
+ hide-volume-slider
48
+ hide-mute-button
49
+ />
50
+ </template>
51
+
52
+ <a
53
+ v-for="file of attachments.files"
54
+ :key="file.key"
55
+ class="message-attachments__file"
56
+ :href="file.url"
57
+ target="_blank"
58
+ rel="noopener"
59
+ download
60
+ :title="t('history.download')"
61
+ >
62
+ <wt-icon icon="docs" />
63
+ <span class="message-attachments__file-name typo-body-2">{{ file.name }}</span>
64
+ <span
65
+ v-if="file.size"
66
+ class="message-attachments__file-size typo-caption"
67
+ >{{ prettifyFileSize(file.size) }}</span>
68
+ <wt-icon icon="download" />
69
+ </a>
70
+ </div>
71
+ </template>
72
+
73
+ <script setup lang="ts">
74
+ import { WtIcon, WtPlayer, WtVidstackPlayer } from '@webitel/ui-sdk/components';
75
+ import { ComponentSize } from '@webitel/ui-sdk/enums';
76
+ import { prettifyFileSize } from '@webitel/ui-sdk/scripts';
77
+ import { computed, inject } from 'vue';
78
+
79
+ import { useChatsV2I18n } from '../../../locale/useChatsV2I18n';
80
+ import { classifyAttachments } from '../../../scripts/attachments';
81
+ import type { ChatImage, MessageModel } from '../../../types';
82
+ import { ChatGalleryKey } from '../gallery';
83
+
84
+ const props = defineProps<{
85
+ message: MessageModel;
86
+ }>();
87
+
88
+ const { t } = useChatsV2I18n();
89
+ const gallery = inject(ChatGalleryKey, null);
90
+
91
+ const attachments = computed(() => classifyAttachments(props.message));
92
+
93
+ // Reserve the box before the image loads, so a late load cannot shift the
94
+ // scroll position.
95
+ const aspectRatio = (image: ChatImage) => ({
96
+ aspectRatio:
97
+ image.width && image.height ? `${image.width} / ${image.height}` : '1 / 1',
98
+ });
99
+ </script>
100
+
101
+ <style scoped>
102
+ .message-attachments {
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: var(--spacing-xs);
106
+ }
107
+
108
+ .message-attachments__images {
109
+ display: grid;
110
+ grid-template-columns: repeat(2, minmax(0, 1fr));
111
+ gap: var(--spacing-3xs);
112
+ }
113
+
114
+ .message-attachments__images--single {
115
+ grid-template-columns: minmax(0, 1fr);
116
+ }
117
+
118
+ .message-attachments__image {
119
+ display: block;
120
+ width: 100%;
121
+ max-height: 320px;
122
+ padding: 0;
123
+ border: 0;
124
+ border-radius: var(--border-radius);
125
+ overflow: hidden;
126
+ cursor: zoom-in;
127
+ background: none;
128
+ }
129
+
130
+ .message-attachments__image img {
131
+ display: block;
132
+ width: 100%;
133
+ height: 100%;
134
+ object-fit: cover;
135
+ }
136
+
137
+ .message-attachments__media {
138
+ width: 100%;
139
+ }
140
+
141
+ .message-attachments__file {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: var(--spacing-xs);
145
+ padding: var(--spacing-xs);
146
+ border-radius: var(--border-radius);
147
+ color: inherit;
148
+ text-decoration: none;
149
+ }
150
+
151
+ .message-attachments__file-name {
152
+ flex: 1;
153
+ min-width: 0;
154
+ overflow: hidden;
155
+ text-overflow: ellipsis;
156
+ white-space: nowrap;
157
+ }
158
+
159
+ .message-attachments__file-size {
160
+ color: var(--text-disabled-color);
161
+ }
162
+ </style>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <wt-icon
3
+ class="message-status"
4
+ :class="`message-status--${props.status}`"
5
+ :icon="props.status === 'sent' ? 'tick' : 'chat-message-status-read'"
6
+ size="sm"
7
+ :aria-label="props.status"
8
+ />
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import { WtIcon } from '@webitel/ui-sdk/components';
13
+
14
+ import type { ChatMessageStatus } from '../../../types';
15
+
16
+ const props = defineProps<{
17
+ status: ChatMessageStatus;
18
+ }>();
19
+ </script>
20
+
21
+ <style scoped>
22
+ /* wt-icon paints with fill: var(--icon-color) */
23
+ .message-status {
24
+ flex-shrink: 0;
25
+ --icon-color: currentColor;
26
+ }
27
+
28
+ .message-status--read {
29
+ --icon-color: var(--info-color);
30
+ }
31
+ </style>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <p
3
+ class="message-text typo-body-1"
4
+ v-html="linkedText"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import Autolinker from 'autolinker';
10
+ import { computed } from 'vue';
11
+
12
+ const props = defineProps<{
13
+ text: string;
14
+ }>();
15
+
16
+ // Autolinker keeps literal "<" signs (DEV-2848) and sanitizes the rest
17
+ const linkedText = computed(() =>
18
+ Autolinker.link(props.text, {
19
+ newWindow: true,
20
+ sanitizeHtml: true,
21
+ className: 'message-text__link',
22
+ }),
23
+ );
24
+ </script>
25
+
26
+ <style scoped>
27
+ .message-text {
28
+ margin: 0;
29
+ white-space: pre-wrap;
30
+ overflow-wrap: anywhere;
31
+ }
32
+
33
+ .message-text :deep(.message-text__link) {
34
+ color: var(--text-link-color);
35
+ text-decoration: underline;
36
+ }
37
+ </style>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div class="scroll-to-bottom-btn">
3
+ <div
4
+ v-show="props.newMessageCount"
5
+ class="scroll-to-bottom-btn__counter typo-body-2"
6
+ >
7
+ {{ props.newMessageCount }}
8
+ </div>
9
+ <wt-rounded-action
10
+ icon="arrow-down"
11
+ rounded
12
+ @click="emit('scroll')"
13
+ />
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { WtRoundedAction } from '@webitel/ui-sdk/components';
19
+
20
+ const props = withDefaults(
21
+ defineProps<{
22
+ newMessageCount?: number;
23
+ }>(),
24
+ {
25
+ newMessageCount: 0,
26
+ },
27
+ );
28
+
29
+ const emit = defineEmits<{
30
+ scroll: [];
31
+ }>();
32
+ </script>
33
+
34
+ <style scoped>
35
+ .scroll-to-bottom-btn {
36
+ position: absolute;
37
+ right: calc(var(--scrollbar-width) + var(--spacing-xs));
38
+ bottom: var(--spacing-xs);
39
+ display: flex;
40
+ flex-direction: column;
41
+ align-items: center;
42
+ gap: var(--spacing-2xs);
43
+ }
44
+
45
+ .scroll-to-bottom-btn__counter {
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ width: 24px;
50
+ height: 24px;
51
+ border-radius: 50%;
52
+ background: var(--success-color);
53
+ color: var(--text-on-brand-color);
54
+ }
55
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { InjectionKey } from 'vue';
2
+
3
+ export interface ChatGallery {
4
+ /** opens the history-wide gallery at the image with this key */
5
+ open: (imageKey: string) => void;
6
+ }
7
+
8
+ export const ChatGalleryKey: InjectionKey<ChatGallery> = Symbol('ChatGallery');
@@ -0,0 +1,122 @@
1
+ import { flushPromises } from '@vue/test-utils';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { message, operator, thread } from '../../../scripts/__tests__/fixtures';
5
+ import { ChatThreadMode } from '../../../types';
6
+ import { deferred, mountWithChats } from '../../__tests__/mountWithChats';
7
+ import ChatThread from '../chat-thread.vue';
8
+
9
+ vi.mock('@webitel/ui-sdk/components', async (importOriginal) => ({
10
+ ...(await importOriginal<typeof import('@webitel/ui-sdk/components')>()),
11
+ WtChatEmoji: {
12
+ name: 'WtChatEmoji',
13
+ emits: [
14
+ 'insert-emoji',
15
+ ],
16
+ template:
17
+ '<button class="emoji-stub" @click="$emit(\'insert-emoji\', \'🙂\')" />',
18
+ },
19
+ WtGalleria: {
20
+ name: 'WtGalleria',
21
+ props: [
22
+ 'visible',
23
+ 'activeIndex',
24
+ 'value',
25
+ ],
26
+ template: '<div class="galleria-stub" />',
27
+ },
28
+ WtVidstackPlayer: {
29
+ name: 'WtVidstackPlayer',
30
+ template: '<div class="player-stub" />',
31
+ },
32
+ WtPlayer: {
33
+ name: 'WtPlayer',
34
+ template: '<div class="player-stub" />',
35
+ },
36
+ }));
37
+
38
+ const setup = (props = {}) =>
39
+ mountWithChats(ChatThread, {
40
+ attachTo: document.body,
41
+ props: {
42
+ thread: thread(),
43
+ messages: [
44
+ message(),
45
+ ],
46
+ selfMemberId: operator.id,
47
+ ...props,
48
+ },
49
+ });
50
+
51
+ describe('ChatThread', () => {
52
+ it('shows the composer only in active mode', async () => {
53
+ const wrapper = setup({
54
+ mode: ChatThreadMode.Active,
55
+ });
56
+ expect(wrapper.find('.chat-composer').exists()).toBe(true);
57
+ await wrapper.setProps({
58
+ mode: ChatThreadMode.Readonly,
59
+ });
60
+ expect(wrapper.find('.chat-composer').exists()).toBe(false);
61
+ await wrapper.setProps({
62
+ mode: ChatThreadMode.Awaiting,
63
+ });
64
+ expect(wrapper.find('.chat-composer').exists()).toBe(false);
65
+ });
66
+
67
+ // Review Focus #3
68
+ it('drops the draft when the thread changes', async () => {
69
+ const wrapper = setup();
70
+ await wrapper.find('textarea').setValue('half-written');
71
+ await wrapper.setProps({
72
+ thread: thread({
73
+ id: 'thread-2',
74
+ }),
75
+ });
76
+ expect(wrapper.find('textarea').element.value).toBe('');
77
+ });
78
+
79
+ // Review Focus #3
80
+ it('does not let a late send from the previous thread clear the new draft', async () => {
81
+ const pending = deferred();
82
+ const wrapper = setup({
83
+ onSend: vi.fn(() => pending.promise),
84
+ });
85
+ await wrapper.find('textarea').setValue('first');
86
+ await wrapper.find('.chat-composer__send').trigger('click');
87
+ await wrapper.setProps({
88
+ thread: thread({
89
+ id: 'thread-2',
90
+ }),
91
+ });
92
+ await wrapper.find('textarea').setValue('second');
93
+ pending.resolve();
94
+ await flushPromises();
95
+ expect(wrapper.find('textarea').element.value).toBe('second');
96
+ });
97
+
98
+ // spec §4: per-thread drafts via v-model:draft="drafts[thread.id]", undefined until typed
99
+ it('reports draft changes to a v-model:draft bound to an undefined value', async () => {
100
+ const onUpdateDraft = vi.fn();
101
+ const wrapper = setup({
102
+ draft: undefined,
103
+ 'onUpdate:draft': onUpdateDraft,
104
+ });
105
+ await wrapper.find('textarea').setValue('per-thread');
106
+ expect(onUpdateDraft).toHaveBeenLastCalledWith('per-thread');
107
+ });
108
+
109
+ it('forwards the actions slot to the composer', () => {
110
+ const wrapper = mountWithChats(ChatThread, {
111
+ props: {
112
+ thread: thread(),
113
+ messages: [],
114
+ selfMemberId: operator.id,
115
+ },
116
+ slots: {
117
+ actions: '<button class="quick-reply" />',
118
+ },
119
+ });
120
+ expect(wrapper.find('.chat-composer .quick-reply').exists()).toBe(true);
121
+ });
122
+ });