@webitel/ui-chats 0.1.39 → 0.1.41

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 (50) hide show
  1. package/package.json +17 -1
  2. package/src/adapters/chat-web-sdk/toChatMessage.ts +111 -0
  3. package/src/adapters/index.ts +12 -0
  4. package/src/types/index.ts +15 -0
  5. package/src/ui/index.ts +2 -0
  6. package/src/ui/messaging/components/chat-date-divider.vue +1 -1
  7. package/src/ui/messaging/components/scroll-to-bottom-btn.vue +0 -2
  8. package/src/ui/messaging/components/the-messages-container.vue +58 -46
  9. package/src/ui/messaging/composables/useChatScroll.ts +161 -81
  10. package/src/ui/messaging/composables/useObserveHeightUntilStable.ts +49 -0
  11. package/src/ui/messaging/composables/useScrollToBottomBtn.ts +51 -0
  12. package/src/ui/messaging/modules/message/components/chat-message.vue +1 -2
  13. package/src/ui/messaging/modules/message/components/details/chat-message-document.vue +1 -2
  14. package/src/ui/messaging/modules/message/components/details/chat-message-image.vue +0 -2
  15. package/src/ui/messaging/modules/message/components/details/chat-message-player.vue +1 -2
  16. package/src/ui/messaging/modules/message/components/details/chat-message-size-exceeded-error.vue +1 -1
  17. package/src/ui/messaging/modules/message/components/details/chat-message-time.vue +1 -1
  18. package/src/ui/messaging/types/ChatMessage.types.ts +3 -2
  19. package/src/ui/the-chat-container.vue +10 -1
  20. package/src/ui/utils/ResultCallbacks.types.ts +1 -1
  21. package/src/ui/utils/emitter.ts +1 -1
  22. package/types/adapters/chat-web-sdk/toChatMessage.d.ts +23 -0
  23. package/types/adapters/index.d.ts +8 -0
  24. package/types/types/index.d.ts +8 -0
  25. package/types/ui/chat-footer/modules/user-input/enums/ChatAction.enum.d.ts +6 -5
  26. package/types/ui/index.d.ts +7 -3
  27. package/types/ui/messaging/components/the-messages-container.vue.d.ts +7 -1
  28. package/types/ui/messaging/composables/useChatScroll.d.ts +13 -3
  29. package/types/ui/messaging/composables/useObserveHeightUntilStable.d.ts +11 -0
  30. package/types/ui/messaging/composables/useScrollToBottomBtn.d.ts +9 -0
  31. package/types/ui/messaging/modules/message/composables/useChatMessageFile.d.ts +2 -0
  32. package/types/ui/messaging/types/ChatMessage.types.d.ts +33 -32
  33. package/types/ui/the-chat-container.vue.d.ts +39 -49
  34. package/types/ui/utils/ResultCallbacks.types.d.ts +1 -1
  35. package/types/ui/utils/emitter.d.ts +1 -1
  36. package/types/ui/chat-container.vue.d.ts +0 -76
  37. package/types/ui/chat-footer/modules/user-input/types/ChatAction.types.d.ts +0 -10
  38. package/types/ui/chat-input/components/actions/attach-files-action.vue.d.ts +0 -29
  39. package/types/ui/chat-input/components/actions/emoji-picker-action.vue.d.ts +0 -24
  40. package/types/ui/chat-input/components/actions/send-message-action.vue.d.ts +0 -29
  41. package/types/ui/chat-input/components/chat-input-actions-bar.vue.d.ts +0 -43
  42. package/types/ui/chat-input/components/chat-input-actions-wrapper.vue.d.ts +0 -34
  43. package/types/ui/chat-input/components/chat-input.vue.d.ts +0 -42
  44. package/types/ui/chat-input/components/chat-text-field.vue.d.ts +0 -32
  45. package/types/ui/chat-input/enums/ChatAction.enum.d.ts +0 -6
  46. package/types/ui/media-viewer/media-viewer.vue.d.ts +0 -11
  47. package/types/ui/messaging/components/chat-messages-container.vue.d.ts +0 -11
  48. package/types/ui/messaging/components/the-chat-messages-container.vue.d.ts +0 -40
  49. package/types/ui/messaging/composebles/useChatScroll.d.ts +0 -12
  50. package/types/ui/messaging/modules/message/components/details/chat-message-avatar.vue.d.ts +0 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/ui-chats",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Reusable Webitel Frontend Code for Chats UI",
5
5
  "workspaces": [
6
6
  "../../",
@@ -10,6 +10,7 @@
10
10
  "scripts": {
11
11
  "dev": "vite",
12
12
  "build:types": "vue-tsc -p ./tsconfig.build.json",
13
+ "typecheck": "vue-tsc --noEmit -p ./tsconfig.typecheck.json",
13
14
  "lint:fix": "npx biome check --write ./src",
14
15
  "biome:ci:gh": "biome ci ./src --reporter=github",
15
16
  "test:unit": "node -e \"process.exit(0)\"",
@@ -26,6 +27,7 @@
26
27
  "devDependencies": {
27
28
  "@biomejs/biome": "2.4.16",
28
29
  "@types/node": "^24.10.0",
30
+ "@webitel/chat-web-sdk": "^0.0.19",
29
31
  "@vitejs/plugin-vue": "^6.0.1",
30
32
  "@vue/tsconfig": "^0.8.1",
31
33
  "mitt": "^3",
@@ -37,11 +39,17 @@
37
39
  },
38
40
  "peerDependencies": {
39
41
  "@webitel/api-services": "^0.1.0",
42
+ "@webitel/chat-web-sdk": "^0.0.19",
40
43
  "@webitel/styleguide": "~26.2",
41
44
  "@webitel/ui-sdk": "~26.6",
42
45
  "mitt": "^3",
43
46
  "vue": "^3.5"
44
47
  },
48
+ "peerDependenciesMeta": {
49
+ "@webitel/chat-web-sdk": {
50
+ "optional": true
51
+ }
52
+ },
45
53
  "type": "module",
46
54
  "main": "index.ts",
47
55
  "types": "index.d.ts",
@@ -59,6 +67,14 @@
59
67
  "./ui": {
60
68
  "types": "./types/ui/index.d.ts",
61
69
  "import": "./src/ui/index.ts"
70
+ },
71
+ "./types": {
72
+ "types": "./types/types/index.d.ts",
73
+ "import": "./src/types/index.ts"
74
+ },
75
+ "./adapters": {
76
+ "types": "./types/adapters/index.d.ts",
77
+ "import": "./src/adapters/index.ts"
62
78
  }
63
79
  },
64
80
  "author": "webitel",
@@ -0,0 +1,111 @@
1
+ import type { IMessage } from '@webitel/chat-web-sdk';
2
+
3
+ import type { ChatMember, ChatMessageFile, ChatMessageType } from '../../types';
4
+
5
+ /**
6
+ * Sub-shapes of a chat-web-sdk message, derived via indexed access so this
7
+ * adapter depends on a single package (`@webitel/chat-web-sdk`) and stays in
8
+ * sync automatically when the SDK reshapes them.
9
+ */
10
+ type SdkMember = NonNullable<IMessage['sender']>;
11
+ type SdkImage = NonNullable<IMessage['images']>[number];
12
+ type SdkDocument = NonNullable<IMessage['documents']>[number];
13
+
14
+ export interface ToChatMessageOptions {
15
+ /**
16
+ * Decide whether a message is authored by the current agent / "self" side.
17
+ * Result is written to `ChatMember.self`, which drives outgoing-bubble
18
+ * alignment in the UI. The message alone cannot know who "self" is — that
19
+ * is presentation context the caller owns (e.g. compare `sender.contact`
20
+ * against the logged-in account).
21
+ */
22
+ isSelf?: (message: IMessage) => boolean;
23
+ }
24
+
25
+ /**
26
+ * Parse a chat-web-sdk Unix-ms timestamp (`string`) into the numeric ms the
27
+ * UI contract expects. Returns `0` for missing / non-numeric input.
28
+ */
29
+ const toTimestamp = (value: string | undefined): number => {
30
+ if (value == null) return 0;
31
+ const parsed = Number(value);
32
+ return Number.isFinite(parsed) ? parsed : 0;
33
+ };
34
+
35
+ const mapImage = (image: SdkImage): ChatMessageFile => ({
36
+ id: image.id,
37
+ mime: image.mime,
38
+ url: image.url,
39
+ });
40
+
41
+ const mapDocument = (document: SdkDocument): ChatMessageFile => ({
42
+ id: document.id,
43
+ name: document.name,
44
+ size: document.size,
45
+ mime: document.mime,
46
+ url: document.url,
47
+ });
48
+
49
+ /**
50
+ * A UI message carries at most one `file`, while the SDK exposes separate
51
+ * `images[]` / `documents[]` arrays. Pick the first available attachment,
52
+ * preferring images.
53
+ */
54
+ const mapAttachment = (message: IMessage): ChatMessageFile | undefined => {
55
+ const image = message.images?.[0];
56
+ if (image) return mapImage(image);
57
+
58
+ const document = message.documents?.[0];
59
+ if (document) return mapDocument(document);
60
+
61
+ return undefined;
62
+ };
63
+
64
+ const mapMember = (
65
+ sender: SdkMember | undefined,
66
+ self: boolean | undefined,
67
+ ): ChatMember => {
68
+ const contact = sender?.contact;
69
+
70
+ return {
71
+ id: sender?.id ?? '',
72
+ name: contact?.name ?? '',
73
+ // UI derives bot rendering from `type === 'bot'`; otherwise keep the
74
+ // SDK channel type ('telegram', 'webchat', …).
75
+ type: contact?.isBot ? 'bot' : (contact?.type ?? ''),
76
+ externalId: contact?.sub ?? contact?.iss,
77
+ self,
78
+ };
79
+ };
80
+
81
+ /**
82
+ * Map a single `@webitel/chat-web-sdk` message (`IMessage`) into the
83
+ * `@webitel/ui-chats` presentation contract (`ChatMessageType`).
84
+ *
85
+ * Anti-corruption layer: keeps `ui-chats` free of any backend/SDK coupling.
86
+ */
87
+ export const mapMessageToChatMessage = (
88
+ message: IMessage,
89
+ options: ToChatMessageOptions = {},
90
+ ): ChatMessageType => {
91
+ const createdAt = toTimestamp(message.createdAt);
92
+
93
+ return {
94
+ id: message.id,
95
+ text: message.body,
96
+ createdAt,
97
+ date: createdAt,
98
+ updatedAt: message.editedAt ? toTimestamp(message.editedAt) : undefined,
99
+ file: mapAttachment(message),
100
+ member: mapMember(message.sender, options.isSelf?.(message)),
101
+ };
102
+ };
103
+
104
+ /**
105
+ * Batch variant of {@link mapMessageToChatMessage} for message history pages.
106
+ */
107
+ export const mapMessagesToChatMessages = (
108
+ messages: readonly IMessage[],
109
+ options?: ToChatMessageOptions,
110
+ ): ChatMessageType[] =>
111
+ messages.map((message) => mapMessageToChatMessage(message, options));
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Adapters mapping external data sources into the `@webitel/ui-chats`
3
+ * presentation contract (`ChatMessageType`, …).
4
+ *
5
+ * Consuming this entry pulls `@webitel/chat-web-sdk` (declared as an optional
6
+ * peerDependency). The core `@webitel/ui-chats/ui` entry stays free of it.
7
+ */
8
+ export {
9
+ mapMessagesToChatMessages,
10
+ mapMessageToChatMessage,
11
+ type ToChatMessageOptions,
12
+ } from './chat-web-sdk/toChatMessage';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Public data contract of `@webitel/ui-chats`.
3
+ *
4
+ * These are the plain, transport-agnostic shapes the UI components accept.
5
+ * External data sources (e.g. `@webitel/chat-web-sdk`) must be mapped into
6
+ * them — see `@webitel/ui-chats/adapters`.
7
+ */
8
+ export type {
9
+ ChatMember,
10
+ ChatMessageChatInfo,
11
+ ChatMessageFile,
12
+ ChatMessageType,
13
+ ChatVia,
14
+ ContactInfo,
15
+ } from '../ui/messaging/types/ChatMessage.types';
package/src/ui/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { ChatAction } from './chat-footer/modules/user-input/enums/ChatAction.enum';
2
+ export { useChatScroll } from './messaging/composables/useChatScroll';
3
+ export { useObserveHeightUntilStable } from './messaging/composables/useObserveHeightUntilStable';
2
4
  export { useChatMessageFile } from './messaging/modules/message/composables/useChatMessageFile';
3
5
  export { MessageAction } from './messaging/modules/message/enums/MessageAction.enum';
4
6
  export type {
@@ -19,7 +19,7 @@ const props = defineProps<{
19
19
 
20
20
  const { t } = useI18n();
21
21
 
22
- const formattedDate = computed<Date>(() => {
22
+ const formattedDate = computed<string>(() => {
23
23
  const chatDate = formatDate(+props.date, FormatDateMode.DATE);
24
24
  const today = formatDate(Date.now(), FormatDateMode.DATE);
25
25
 
@@ -17,8 +17,6 @@
17
17
  </template>
18
18
 
19
19
  <script setup lang="ts">
20
- import { defineEmits } from 'vue';
21
-
22
20
  const props = withDefaults(
23
21
  defineProps<{
24
22
  newMessageCount?: number;
@@ -7,30 +7,35 @@
7
7
  ref="messages-container"
8
8
  class="the-chat-messages-container__wrapper wt-scrollbar"
9
9
  @scroll="handleChatScroll"
10
- @resize="handleChatResize"
11
10
  >
12
- <chat-observer
13
- v-if="props.next"
14
- :next="props.next"
15
- :loading="props.isLoading"
16
- @[ChatAction.LoadNextMessages]="handleLoadNextMessages"
17
- />
18
- <chat-message
19
- v-for="(message, index) of props.messages"
20
- :key="message.id"
21
- :message="message"
22
- :agent-name="props.agentName"
23
- :show-avatar="showAvatar(index)"
24
- :without-avatars="props.withoutAvatars"
25
- @[MessageAction.ClickOnImage]="clickOnImage(message)"
11
+ <div
12
+ ref="chat-content"
13
+ class="the-chat-messages-container__content"
26
14
  >
27
- <template #before-message>
28
- <chat-date-divider
29
- v-if="showChatDate(index)"
30
- :date="message.createdAt"
31
- />
32
- </template>
33
- </chat-message>
15
+ <chat-observer
16
+ v-if="props.next"
17
+ :next="props.next"
18
+ :loading="props.isLoading"
19
+ @[ChatAction.LoadNextMessages]="handleLoadNextMessages"
20
+ />
21
+ <chat-message
22
+ v-for="(message, index) of props.messages"
23
+ :key="message.id"
24
+ :message="message"
25
+ :agent-name="props.agentName"
26
+ :show-avatar="showAvatar(index)"
27
+ :without-avatars="props.withoutAvatars"
28
+ :username="props.contact?.name?.commonName"
29
+ @[MessageAction.ClickOnImage]="clickOnImage(message)"
30
+ >
31
+ <template #before-message>
32
+ <chat-date-divider
33
+ v-if="showChatDate(index)"
34
+ :date="message.createdAt"
35
+ />
36
+ </template>
37
+ </chat-message>
38
+ </div>
34
39
  </div>
35
40
  <scroll-to-bottom-btn
36
41
  v-if="showScrollToBottomBtn"
@@ -44,19 +49,13 @@
44
49
  setup
45
50
  lang="ts"
46
51
  >
52
+ import { WebitelContactsContact } from '@webitel/api-services/gen/models';
47
53
  import type { Emitter } from 'mitt';
48
- import {
49
- computed,
50
- defineProps,
51
- inject,
52
- nextTick,
53
- onMounted,
54
- useTemplateRef,
55
- } from 'vue';
56
-
54
+ import { computed, inject, useTemplateRef } from 'vue';
57
55
  import { ChatAction } from '../../chat-footer/modules/user-input/enums/ChatAction.enum';
58
56
  import type { UiChatsEmitterEvents } from '../../utils/emitter';
59
57
  import { useChatScroll } from '../composables/useChatScroll';
58
+ import { useObserveHeightUntilStable } from '../composables/useObserveHeightUntilStable';
60
59
  import ChatMessage from '../modules/message/components/chat-message.vue';
61
60
  import { useChatMessages } from '../modules/message/composables/useChatMessage';
62
61
  import { MessageAction } from '../modules/message/enums/MessageAction.enum';
@@ -74,17 +73,23 @@ const props = withDefaults(
74
73
  isLoading?: boolean;
75
74
  withoutAvatars?: boolean;
76
75
  agentName?: string;
76
+ contact?: WebitelContactsContact;
77
+ chatId?: string;
78
+ isChatClosed?: boolean;
77
79
  }>(),
78
80
  {
79
81
  next: false,
80
82
  isLoading: false,
81
83
  withoutAvatars: false,
84
+ chatId: '',
85
+ isChatClosed: false,
82
86
  },
83
87
  );
84
88
 
85
89
  const emit = defineEmits<(e: typeof ChatAction.LoadNextMessages) => void>();
86
90
 
87
- const messagesContainer = useTemplateRef('messages-container');
91
+ const chatContainer = useTemplateRef('messages-container');
92
+ const chatContent = useTemplateRef('chat-content');
88
93
 
89
94
  const { showAvatar, showChatDate } = useChatMessages(
90
95
  computed(() => props.messages),
@@ -96,11 +101,21 @@ const {
96
101
  scrollToBottom,
97
102
  loadNextMessages,
98
103
  handleChatScroll,
99
- handleChatResize,
100
- } = useChatScroll(
101
- messagesContainer,
102
- computed(() => props.messages), // props values reactivity https://stackoverflow.com/questions/72408463/use-props-in-composables-vue3 @author ye.pohranichna
103
- computed(() => props.isLoading),
104
+ } = useChatScroll({
105
+ chatContainer,
106
+ chatContent,
107
+ messages: computed(() => props.messages), // props values reactivity https://stackoverflow.com/questions/72408463/use-props-in-composables-vue3 @author ye.pohranichna
108
+ chatId: computed(() => props.chatId),
109
+ isChatClosed: computed(() => props.isChatClosed),
110
+ isLoading: computed(() => props.isLoading),
111
+ onBeforeStart: ({ scrollToBottom }) => {
112
+ scrollToBottom();
113
+ startObserve();
114
+ },
115
+ });
116
+
117
+ const { startObserve } = useObserveHeightUntilStable(chatContainer, () =>
118
+ scrollToBottom('instant'),
104
119
  );
105
120
 
106
121
  function handleLoadNextMessages() {
@@ -115,13 +130,7 @@ function clickOnImage(message: ChatMessageType) {
115
130
  uiChatsEmitter?.emit('clickChatMessageImage', message);
116
131
  }
117
132
 
118
- onMounted(async () => {
119
- /* TODO: add loader on all chat(in the-chat-container?) and remove timeout after that */
120
- await nextTick();
121
- setTimeout(() => {
122
- scrollToBottom();
123
- }, 500);
124
- });
133
+ // TODO: add loader for all chats(in the-chat-container or in calling component?)
125
134
  </script>
126
135
 
127
136
  <style
@@ -136,14 +145,17 @@ onMounted(async () => {
136
145
  }
137
146
 
138
147
  .the-chat-messages-container__wrapper {
139
- display: flex;
140
- flex-direction: column;
141
148
  box-sizing: border-box;
142
149
  overflow-x: hidden;
143
150
  overflow-y: auto;
144
151
  width: 100%;
145
152
  padding-right: var(--scrollbar-width); // scrollbar offset
146
153
  scrollbar-gutter: stable both-edges;
154
+ }
155
+
156
+ .the-chat-messages-container__content {
157
+ display: flex;
158
+ flex-direction: column;
147
159
  gap: var(--spacing-xs);
148
160
  }
149
161
  </style>