@webitel/ui-chats 0.1.49 → 0.1.51

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/declaration.d.ts +7 -0
  3. package/src/ui/chat-footer/modules/user-input/components/chat-input-actions-bar.vue +2 -2
  4. package/src/ui/chat-footer/modules/user-input/components/chat-text-field.vue +4 -4
  5. package/src/ui/chat-footer/modules/user-input/enums/ChatAction.enum.ts +5 -0
  6. package/src/ui/messaging/composables/useChatScroll.ts +34 -3
  7. package/src/ui/messaging/modules/message/components/chat-message.vue +1 -1
  8. package/src/ui/messaging/modules/message/components/details/chat-message-player.vue +5 -4
  9. package/src/ui/the-chat-container.vue +2 -2
  10. package/types/.tsbuildinfo +1 -1
  11. package/types/ui/chat-footer/modules/user-input/components/actions/attach-files-action.vue.d.ts +1 -1
  12. package/types/ui/chat-footer/modules/user-input/components/actions/send-message-action.vue.d.ts +1 -1
  13. package/types/ui/chat-footer/modules/user-input/components/chat-input-actions-bar.vue.d.ts +4 -4
  14. package/types/ui/chat-footer/modules/user-input/components/chat-text-field.vue.d.ts +4 -5
  15. package/types/ui/chat-footer/modules/user-input/enums/ChatAction.enum.d.ts +4 -0
  16. package/types/ui/image-viewer/image-viewer.vue.d.ts +1 -1
  17. package/types/ui/messaging/components/chat-observer.vue.d.ts +2 -2
  18. package/types/ui/messaging/components/scroll-to-bottom-btn.vue.d.ts +2 -2
  19. package/types/ui/messaging/components/the-messages-container.vue.d.ts +1 -1
  20. package/types/ui/messaging/composables/useChatScroll.d.ts +1 -0
  21. package/types/ui/messaging/modules/message/components/chat-message.vue.d.ts +2 -2
  22. package/types/ui/messaging/modules/message/components/details/chat-message-image.vue.d.ts +1 -1
  23. package/types/ui/messaging/modules/message/components/details/chat-message-player.vue.d.ts +2 -2
  24. package/types/ui/messaging/modules/message/components/details/chat-message-size-exceeded-error.vue.d.ts +1 -1
  25. package/types/ui/messaging/modules/message/components/details/chat-message-time.vue.d.ts +1 -1
  26. package/types/ui/messaging/modules/message/composables/useChatMessageFile.d.ts +19 -19
  27. package/types/ui/the-chat-container.vue.d.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/ui-chats",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "description": "Reusable Webitel Frontend Code for Chats UI",
5
5
  "workspaces": [
6
6
  "../../",
@@ -0,0 +1,7 @@
1
+ // insert-text-at-cursor ships no types and has no @types package
2
+ declare module 'insert-text-at-cursor' {
3
+ export default function insertTextAtCursor(
4
+ el: HTMLInputElement | HTMLTextAreaElement,
5
+ text: string,
6
+ ): void;
7
+ }
@@ -20,7 +20,7 @@
20
20
  import { ComponentSize } from '@webitel/ui-sdk/enums';
21
21
  import { computed, inject } from 'vue';
22
22
 
23
- import { ChatAction, type SharedActionSlots } from '../enums/ChatAction.enum';
23
+ import { ChatAction, type ChatActionSlots } from '../enums/ChatAction.enum';
24
24
  import AttachFilesAction from './actions/attach-files-action.vue';
25
25
  import EmojiPickerAction from './actions/emoji-picker-action.vue';
26
26
  import SendMessageAction from './actions/send-message-action.vue';
@@ -36,7 +36,7 @@ const emit = defineEmits<{
36
36
  (e: typeof ChatAction.AttachFiles, files: File[]): void;
37
37
  }>();
38
38
 
39
- const slots = defineSlots<SharedActionSlots>();
39
+ defineSlots<ChatActionSlots>();
40
40
 
41
41
  const ShownActionComponentsList = computed(() => {
42
42
  /**
@@ -16,14 +16,14 @@ import { WtTextarea } from '@webitel/ui-sdk/components';
16
16
  import { ComponentSize } from '@webitel/ui-sdk/enums';
17
17
  import insertTextAtCursor from 'insert-text-at-cursor';
18
18
  import type { Emitter } from 'mitt';
19
- import { computed, inject, type MaybeRef, useTemplateRef } from 'vue';
19
+ import { computed, inject, useTemplateRef } from 'vue';
20
20
  import { useI18n } from 'vue-i18n';
21
21
 
22
22
  import type { UiChatsEmitterEvents } from '../../../../utils/emitter';
23
23
 
24
24
  const { t } = useI18n();
25
25
 
26
- const textModel = defineModel<MaybeRef<string>>('text', {
26
+ const textModel = defineModel<string>('text', {
27
27
  required: true,
28
28
  });
29
29
 
@@ -46,8 +46,8 @@ const textareaEl = computed(() =>
46
46
  chatTextFieldInputRef.value?.$el.querySelector('textarea'),
47
47
  );
48
48
 
49
- function send(text: string) {
50
- textModel.value = text;
49
+ function send(text?: string) {
50
+ textModel.value = text ?? '';
51
51
  }
52
52
 
53
53
  function focus() {
@@ -11,3 +11,8 @@ export type ChatAction = (typeof ChatAction)[keyof typeof ChatAction];
11
11
  export type SharedActionSlots = {
12
12
  [key in `action:${ChatAction}`]?: () => unknown;
13
13
  };
14
+
15
+ /** slots `chat-input-actions-bar` itself exposes, keyed by the bare action name */
16
+ export type ChatActionSlots = {
17
+ [key in ChatAction]?: (props: Record<string, unknown>) => unknown;
18
+ };
@@ -56,6 +56,7 @@ export const useChatScroll = ({
56
56
  let isLoadingNextMessages = false;
57
57
  let lastVisibleMessageEl: HTMLElement | null = null;
58
58
  let prevScrollHeight = 0;
59
+ let prevScrollTop = 0;
59
60
  let resizeObserver: ResizeObserver | null = null;
60
61
  let isViewportTransition = false;
61
62
  let viewportTransitionTimer: ReturnType<typeof setTimeout> | null = null;
@@ -72,6 +73,15 @@ export const useChatScroll = ({
72
73
  const lastMessage = computed(() => messages.value?.at(-1));
73
74
  const isLastMessageMy = computed(() => !!lastMessage.value?.member?.self);
74
75
 
76
+ const isViewingBottom = (el: HTMLElement) =>
77
+ el.clientHeight > 0 &&
78
+ el.scrollHeight - (el.scrollTop + el.clientHeight) <= bottomThreshold;
79
+
80
+ const markSeenIfAtBottom = () => {
81
+ const el = chatContainer.value;
82
+ if (el && isViewingBottom(el)) onSeen?.();
83
+ };
84
+
75
85
  const handleBtnAfterNewMessage = () => {
76
86
  if (isLastMessageMy.value) {
77
87
  scrollToBottom('instant');
@@ -89,8 +99,14 @@ export const useChatScroll = ({
89
99
  newUnseenMessagesCount.value = 0;
90
100
 
91
101
  resetScrollToBottomBtn();
92
- onSeen?.();
102
+ markSeenIfAtBottom();
103
+ };
104
+
105
+ const handleChatScrollWithSeen = () => {
106
+ handleChatScroll();
107
+ markSeenIfAtBottom();
93
108
  };
109
+
94
110
  const getTopMessageEl = () => {
95
111
  // help to fix chat viewing position when new messages was loaded
96
112
  if (!chatContainer.value?.children) return;
@@ -116,6 +132,7 @@ export const useChatScroll = ({
116
132
  if (!chatContent.value) return;
117
133
 
118
134
  prevScrollHeight = chatContainer.value?.scrollHeight ?? 0;
135
+ prevScrollTop = chatContainer.value?.scrollTop ?? 0;
119
136
 
120
137
  resizeObserver = new ResizeObserver(() => {
121
138
  const el = chatContainer.value;
@@ -125,11 +142,23 @@ export const useChatScroll = ({
125
142
 
126
143
  if (isViewportTransition) {
127
144
  prevScrollHeight = newScrollHeight;
145
+ prevScrollTop = el.scrollTop;
128
146
  updateScrollToBottomBtnVisibility(el);
129
147
  return;
130
148
  }
131
149
 
150
+ const heightDelta = newScrollHeight - prevScrollHeight;
151
+ const scrollTopDelta = el.scrollTop - prevScrollTop;
152
+ /**
153
+ * @author PolinaSukhorukova-webitel
154
+ *
155
+ * Prepend after pagination shifts scrollTop by the height it adds on top; bottom
156
+ * growth leaves scrollTop still. Matching deltas = prepend - skip scroll.
157
+ */
158
+ const isPrepend =
159
+ Math.abs(scrollTopDelta - heightDelta) <= bottomThreshold;
132
160
  const wasNearBottom =
161
+ !isPrepend &&
133
162
  el.scrollTop + el.clientHeight >= prevScrollHeight - bottomThreshold;
134
163
  const contentGrown = newScrollHeight > prevScrollHeight;
135
164
 
@@ -147,6 +176,7 @@ export const useChatScroll = ({
147
176
  }
148
177
 
149
178
  prevScrollHeight = newScrollHeight;
179
+ prevScrollTop = el.scrollTop;
150
180
  updateScrollToBottomBtnVisibility(el);
151
181
  });
152
182
 
@@ -247,7 +277,7 @@ export const useChatScroll = ({
247
277
  (bottom) => {
248
278
  if (bottom) {
249
279
  newUnseenMessagesCount.value = 0;
250
- onSeen?.();
280
+ markSeenIfAtBottom();
251
281
  }
252
282
  },
253
283
  );
@@ -262,7 +292,8 @@ export const useChatScroll = ({
262
292
  showScrollToBottomBtn,
263
293
  newUnseenMessagesCount,
264
294
  scrollToBottom,
295
+ markSeenIfAtBottom,
265
296
  loadNextMessages,
266
- handleChatScroll,
297
+ handleChatScroll: handleChatScrollWithSeen,
267
298
  };
268
299
  };
@@ -143,7 +143,7 @@ const getClientUsername = computed<string | undefined>(() => {
143
143
  return props.username || props.message.member?.name;
144
144
  });
145
145
 
146
- function handlePlayerInitialize(player) {
146
+ function handlePlayerInitialize(player: object) {
147
147
  emit(MessageAction.InitializedPlayer, {
148
148
  player,
149
149
  });
@@ -52,13 +52,14 @@ const isVideo = computed(() => {
52
52
  });
53
53
  const mediaSrc = computed(() => {
54
54
  return {
55
- src: props.file.streamUrl || props.file.url,
56
- type: props.type,
55
+ src: props.file.streamUrl || props.file.url || '',
56
+ type: props.type ?? '',
57
57
  };
58
58
  });
59
59
 
60
- function handlePlayerInitialize(player) {
61
- emit('initialized', player);
60
+ // `initialized` currently carries no payload, so the argument stays optional
61
+ function handlePlayerInitialize(player?: object) {
62
+ emit('initialized', player ?? {});
62
63
  }
63
64
  </script>
64
65
 
@@ -143,10 +143,10 @@ function closeMedia() {
143
143
 
144
144
  const draft = ref<string>('');
145
145
 
146
- const slottedChatActions = computed(() => {
146
+ const slottedChatActions = computed<ChatAction[]>(() => {
147
147
  return Object.keys(slots)
148
148
  .filter((key) => key.startsWith('action:'))
149
- .map((key) => key.replace('action:', ''));
149
+ .map((key) => key.replace('action:', '') as ChatAction);
150
150
  });
151
151
  const isDropzoneDisabled = computed(
152
152
  () => !props.chatActions.includes(ChatAction.AttachFiles),