vue-wiguet-chatweb 0.1.15 → 0.1.17

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue-wiguet-chatweb",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -15,6 +15,7 @@
15
15
  :canLoadMoreMessages="messagesData.canLoadMoreMessages"
16
16
  @loadMore="getMessages"
17
17
  @retry="retryMessage"
18
+ @on-qualifying="(args) => onQualifying(args)"
18
19
  />
19
20
  </div>
20
21
 
@@ -28,7 +29,11 @@
28
29
  required
29
30
  ref="textAreaRef"
30
31
  @input="() => autoAdjustHeight()"
31
- @keydown.enter.prevent
32
+ @keydown.enter="
33
+ (event) => {
34
+ !isMovil && event.preventDefault();
35
+ }
36
+ "
32
37
  @keyup.enter="saltoDeLineaOEnviar"
33
38
  />
34
39
 
@@ -43,7 +48,15 @@
43
48
  </template>
44
49
 
45
50
  <script setup lang="ts">
46
- import { ref, onMounted, nextTick, PropType, watch, onUnmounted } from "vue";
51
+ import {
52
+ ref,
53
+ onMounted,
54
+ nextTick,
55
+ PropType,
56
+ watch,
57
+ onUnmounted,
58
+ computed,
59
+ } from "vue";
47
60
  import { v4 as uuidv4 } from "uuid";
48
61
 
49
62
  import {
@@ -58,6 +71,7 @@ import {
58
71
  getMessagesApi,
59
72
  sendMessageApi,
60
73
  setVistoToTrueApi,
74
+ updateMessageApi,
61
75
  } from "../store/index";
62
76
  import { getInformationApi } from "../store";
63
77
  import MessageList from "./MessageList.vue";
@@ -84,6 +98,7 @@ const emit = defineEmits([
84
98
  "new-message",
85
99
  "clear-new-messages",
86
100
  "not-viewed-total",
101
+ "onQualifying",
87
102
  ]);
88
103
 
89
104
  const props = defineProps({
@@ -109,8 +124,8 @@ const messageContainerRef = ref<HTMLElement | null>(null);
109
124
  watch(
110
125
  () => props.visible,
111
126
  async (current) => {
112
- if(!current) return
113
-
127
+ if (!current) return;
128
+
114
129
  emit("clear-new-messages");
115
130
  scrollToBottom();
116
131
 
@@ -122,18 +137,23 @@ watch(
122
137
 
123
138
  const resp = await getInformationApi(props.tokenAuth);
124
139
 
125
- if(resp) {
140
+ if (resp) {
126
141
  appChatId.value = resp.appChat.id;
127
142
  }
128
143
  }
129
144
  );
130
145
 
131
146
  function saltoDeLineaOEnviar(event: KeyboardEvent) {
147
+ if(isMovil.value) {
148
+ autoAdjustHeight();
149
+ return;
150
+ }
151
+
132
152
  if (event.key === "Enter" && event.shiftKey) {
133
- const val = (event.target as any)?.value || '';
153
+ const val = (event.target as any)?.value || "";
134
154
  (event.target as any).value = val + "\n";
135
- autoAdjustHeight()
136
- return
155
+ autoAdjustHeight();
156
+ return;
137
157
  }
138
158
 
139
159
  submitMessage(event);
@@ -149,7 +169,7 @@ const submitMessage = async (event: Event) => {
149
169
  });
150
170
  return;
151
171
  }
152
-
172
+
153
173
  if (!message.value.trim()) {
154
174
  emit("show-toast", {
155
175
  severity: "warn",
@@ -176,11 +196,11 @@ const submitMessage = async (event: Event) => {
176
196
  msPersonaId: props.user.msPersonaId,
177
197
  },
178
198
  };
179
-
199
+
180
200
  const idx = messagesData.value.data.push(newMessage) - 1;
181
-
182
- sendApi(message.value, appChatId.value).then((newMsg)=>{
183
- if(!newMsg) {
201
+
202
+ sendApi(message.value, appChatId.value).then((newMsg) => {
203
+ if (!newMsg) {
184
204
  messagesData.value.data[idx].error = {
185
205
  error: true,
186
206
  id: newMessage.id,
@@ -194,13 +214,13 @@ const submitMessage = async (event: Event) => {
194
214
  });
195
215
  } else {
196
216
  messagesData.value.data[idx] = newMsg;
197
-
198
- const message = {...newMsg, chatId:newMessage.chatId }
217
+
218
+ const message = { ...newMsg, chatId: newMessage.chatId };
199
219
  socketService.value?.emit(
200
- 'sendMessage',
220
+ "sendMessage",
201
221
  { roomId: information?.value?.appChat.id, message },
202
222
  (response: any) => {
203
- console.log('🚀 ~ socketService.value.emit ~ response:', response);
223
+ console.log("🚀 ~ socketService.value.emit ~ response:", response);
204
224
  }
205
225
  );
206
226
  }
@@ -208,7 +228,7 @@ const submitMessage = async (event: Event) => {
208
228
 
209
229
  message.value = "";
210
230
  scrollToBottom();
211
- textAreaRef.value && (textAreaRef.value.style.height = "20px")
231
+ textAreaRef.value && (textAreaRef.value.style.height = "20px");
212
232
  };
213
233
 
214
234
  const sendApi = async (message: string, appChatId: string) => {
@@ -221,37 +241,35 @@ const sendApi = async (message: string, appChatId: string) => {
221
241
  };
222
242
 
223
243
  const getMessages = async () => {
224
-
225
244
  const lastMessagesId = messagesData.value.data[0]?.id;
226
245
  const body: ListMessageBody = {
227
246
  lastMessagesId,
228
247
  appChatId: appChatId.value,
229
- limit: 10
248
+ limit: 10,
230
249
  };
231
250
 
232
251
  isLoading.value = true;
233
252
  const resp = await getMessagesApi({ body, token: props.tokenAuth });
234
253
  isLoading.value = false;
235
-
254
+
236
255
  messagesData.value.data.unshift(
237
256
  ...resp.data.sort((a, b) => -b.createdAt.localeCompare(a.createdAt))
238
257
  );
239
-
258
+
240
259
  messagesData.value.canLoadMoreMessages =
241
260
  resp.pagination.total > resp.pagination.size;
242
-
261
+
243
262
  if (lastMessagesId && messageContainerRef.value?.scrollHeight) {
244
263
  mantainElementsOnViewport(messageContainerRef.value?.scrollHeight);
245
264
  }
246
-
265
+
247
266
  if (!lastMessagesId) scrollToBottom();
248
267
  };
249
268
 
250
269
  const retryMessage = async (message: Message) => {
251
270
  emit("show-confirm", async () => {
252
-
253
271
  if (!message.error?.id) return;
254
-
272
+
255
273
  const msg = await sendApi(message.message, appChatId.value);
256
274
 
257
275
  if (!msg) {
@@ -267,13 +285,13 @@ const retryMessage = async (message: Message) => {
267
285
  "id",
268
286
  message.error.id
269
287
  );
270
-
288
+
271
289
  messagesData.value.data[idx] = { ...msg, error: undefined };
272
290
 
273
- socketService.value?.emit(
274
- 'sendMessage',
275
- { roomId: information?.value?.appChat.id, message },
276
- );
291
+ socketService.value?.emit("sendMessage", {
292
+ roomId: information?.value?.appChat.id,
293
+ message,
294
+ });
277
295
  }
278
296
 
279
297
  scrollToBottom();
@@ -305,23 +323,28 @@ const fontSpace = 14;
305
323
  function autoAdjustHeight() {
306
324
  if (!textAreaRef.value) return;
307
325
 
308
- textAreaRef.value.style.height = textAreaRef.value.scrollHeight - fontSpace + 'px'
326
+ textAreaRef.value.style.height =
327
+ textAreaRef.value.scrollHeight - fontSpace + "px";
328
+
329
+ if (textAreaRef.value.scrollHeight === textAreaRef.value.clientHeight)
330
+ return;
309
331
 
310
- if(textAreaRef.value.scrollHeight === textAreaRef.value.clientHeight) return;
311
-
312
- textAreaRef.value.style.height = textAreaRef.value.scrollHeight + "px"
332
+ textAreaRef.value.style.height = textAreaRef.value.scrollHeight + "px";
313
333
  }
314
334
 
315
335
  const socketService = ref<Socket>();
316
336
  const information = ref<ChatInformation>();
317
337
 
318
- function connectMsWebSocket (userChat: ChatInformation ,app: APP_TYPE = APP_TYPE.WEBCHAT) {
319
- if (!userChat) throw new Error('user chat is required')
338
+ function connectMsWebSocket(
339
+ userChat: ChatInformation,
340
+ app: APP_TYPE = APP_TYPE.WEBCHAT
341
+ ) {
342
+ if (!userChat) throw new Error("user chat is required");
320
343
 
321
344
  socketService.value = io(window.VITE_SOCKET_URI, {
322
345
  query: {
323
346
  // TODO: confirmar si se quita o no
324
- usuarioId: `${userChat?.chat?.persona?.funcionarioId}`,
347
+ usuarioId: `${userChat?.chat?.persona?.funcionarioId}`,
325
348
  aplicacion: app,
326
349
  },
327
350
  extraHeaders: { Authorization: props.tokenAuth },
@@ -329,24 +352,24 @@ function connectMsWebSocket (userChat: ChatInformation ,app: APP_TYPE = APP_TYPE
329
352
 
330
353
  socketService.value.removeAllListeners();
331
354
 
332
- socketService.value.on('connect', () => {
333
- console.log('Conectado al servidor de sockets');
355
+ socketService.value.on("connect", () => {
356
+ console.log("Conectado al servidor de sockets");
334
357
 
335
- socketService.value?.emit(
336
- 'joinRoom',
337
- `${userChat?.appChat?.id}`
338
- );
339
- })
358
+ socketService.value?.emit("joinRoom", `${userChat?.appChat?.id}`);
359
+ });
340
360
 
341
- socketService.value.on('disconnect', () => {
342
- console.log('Desconectado del servidor de sockets');
361
+ socketService.value.on("disconnect", () => {
362
+ console.log("Desconectado del servidor de sockets");
343
363
  });
344
364
 
345
- socketService.value.on('receiveMessage', (data: any) => {
346
- console.log('Mensaje recibido:', data.message, userChat);
347
-
348
- if (data.message.sender.msPersonaId === userChat?.chat?.persona?.msPersonaId) return
349
-
365
+ socketService.value.on("receiveMessage", (data: any) => {
366
+ console.log("Mensaje recibido:", data.message, userChat);
367
+
368
+ if (
369
+ data.message.sender.msPersonaId === userChat?.chat?.persona?.msPersonaId
370
+ )
371
+ return;
372
+
350
373
  messagesData.value.data.push(data.message);
351
374
  setVistoToTrueApi(data.message.appChatId, props.tokenAuth);
352
375
  scrollToBottom();
@@ -354,17 +377,92 @@ function connectMsWebSocket (userChat: ChatInformation ,app: APP_TYPE = APP_TYPE
354
377
  });
355
378
  }
356
379
 
380
+ const isMovil = computed(() => {
381
+ return [OS.ANDROID, OS.IOS].includes(getOS() as OS);
382
+ });
383
+
384
+ const enum OS {
385
+ ANDROID = "Android",
386
+ IOS = "iPhone",
387
+ }
388
+
389
+ function getOS() {
390
+ const userAgent = window.navigator.userAgent;
391
+ const platform = (window.navigator as any)?.userAgentData?.platform || window.navigator.platform;
392
+ const macosPlatforms = [
393
+ "macOS",
394
+ "Macintosh",
395
+ "MacIntel",
396
+ "MacPPC",
397
+ "Mac68K",
398
+ ];
399
+ const windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
400
+ const iosPlatforms = ["iPhone", "iPad", "iPod"];
401
+
402
+ let os = null;
403
+
404
+ if (macosPlatforms.indexOf(platform) !== -1) {
405
+ os = "Mac OS";
406
+ } else if (iosPlatforms.indexOf(platform) !== -1) {
407
+ os = OS.IOS;
408
+ } else if (windowsPlatforms.indexOf(platform) !== -1) {
409
+ os = "Windows";
410
+ } else if (/Android/.test(userAgent)) {
411
+ os = OS.ANDROID;
412
+ } else if (/Linux/.test(platform)) {
413
+ os = "Linux";
414
+ }
415
+
416
+ return os;
417
+ }
418
+
419
+ function onQualifying({ message, emoji }: { message: Message, emoji: {icon: string, value: number } }) {
420
+ const callback = async () => {
421
+ if (!message.id || !emoji) return;
422
+
423
+ const idx = searchFromLast<Message>(
424
+ messagesData.value.data,
425
+ "id",
426
+ message.id
427
+ );
428
+
429
+ let prevMessage = messagesData.value?.data?.[idx].message
430
+ messagesData.value?.data?.[idx] && (messagesData.value.data[idx].message = emoji.icon);
431
+
432
+ const msg = await updateMessageApi(message.id, { message: emoji.icon, tipoCalificacionId: emoji.value }, props.tokenAuth);
433
+
434
+ if (!msg) {
435
+ messagesData.value?.data?.[idx] && (messagesData.value.data[idx].message = prevMessage);
436
+ emit("show-toast", {
437
+ severity: "error",
438
+ summary: "Error",
439
+ detail: "Ocurrio un error al enviar el mensaje, intente nuevamente",
440
+ life: 5000,
441
+ });
442
+ return
443
+ }
444
+
445
+ socketService.value?.emit("sendMessage", {
446
+ roomId: information?.value?.appChat.id,
447
+ message,
448
+ });
449
+ }
450
+
451
+ emit('onQualifying', callback)
452
+ };
453
+
454
+
357
455
  onMounted(async () => {
358
456
  if (appChatId.value) return;
359
-
457
+
360
458
  const resp = await getInformationApi(props.tokenAuth);
361
-
362
- if(!resp) return;
363
-
459
+
460
+ if (!resp) return;
461
+
364
462
  information.value = resp;
365
463
  appChatId.value = resp.appChat.id;
366
464
  notViewed.value = resp.appChat.totalNoVistosCliente;
367
- connectMsWebSocket(resp)
465
+ connectMsWebSocket(resp);
368
466
  getMessages();
369
467
  emit("not-viewed-total", resp.appChat.totalNoVistosCliente);
370
468
  });
@@ -26,7 +26,25 @@
26
26
  <div class="chat-message">
27
27
  <div class="bubble" :class="message.esCliente ? 'right' : 'left'">
28
28
  <div :class="message.esCliente ? 'content-right' : 'content-left'">
29
- <div class="message-text" style="white-space: pre-line" v-html="convertUrlsToLinks(message.message)"></div>
29
+ <div v-if="message.message === '😊😄🙂😐🙁'" class="flex gap-2" >
30
+ <div>
31
+ <strong style="display: block; margin-bottom: 0.5rem;">Ayúdanos a mejorar nuestro servicio.</strong>
32
+ <span>Que le pareció la atención:</span>
33
+ </div>
34
+ <a
35
+ v-for="emoji in emojis"
36
+ href="javascript:"
37
+ class="btn-icon"
38
+ :key="emoji.value"
39
+ @click="emit('onQualifying', { message, emoji })"
40
+ >
41
+ <div class="flex flex-col items-center">
42
+ <div class="icon">{{ emoji.icon }}</div>
43
+ <span>{{ emoji.label }}</span>
44
+ </div>
45
+ </a>
46
+ </div>
47
+ <div v-else class="message-text" style="white-space: pre-line" v-html="textToRichFormat(message.message)"></div>
30
48
  <div class="detail-message flex justify-content-between">
31
49
  <span class="mr-5" v-if="message.sender?.nombreCompleto">
32
50
  {{
@@ -53,13 +71,13 @@
53
71
  </template>
54
72
 
55
73
  <script setup lang="ts">
56
- import { PropType, onBeforeMount, ref, watch } from "vue";
74
+ import { PropType, onBeforeMount, ref, watch, h } from 'vue';
57
75
  import { useIntersectionObserver } from "@vueuse/core";
58
76
  import { type Message } from "../dto/app.dto";
59
77
  import { DateTime } from "luxon";
60
78
  import DangerIcon from "./DangerIcon.vue";
61
79
 
62
- const emit = defineEmits(["loadMore", "retry"]);
80
+ const emit = defineEmits(["loadMore", "retry", "onQualifying"]);
63
81
  const props = defineProps({
64
82
  messages: {
65
83
  type: Array as PropType<Message[]>,
@@ -98,15 +116,51 @@ watch(
98
116
  }
99
117
  );
100
118
 
101
- function convertUrlsToLinks(content:string) {
102
- // Expresión regular para detectar URLs
103
- const urlRegex = /(https?:\/\/[^\s]+)/g;
119
+ type RichTextType = {
120
+ regex: RegExp;
121
+ tag: string;
122
+ };
123
+
124
+ const richText: { [key in string]: RichTextType } = {
125
+ BOLD: { regex: /\*(.+?)\*(?!\*)/g, tag: '<strong--custom-tag>{val}</strong--custom-tag>' },
126
+ ITALIC: { regex: /_(.+?)_/, tag: '<i--custom-tag>{val}</i--custom-tag>' },
127
+ CROSSED_OUT: { regex: /~(.+?)~(?!~)/g, tag: '<del--custom-tag>{val}</del--custom-tag>' },
128
+ URL: {
129
+ regex: /(http?s?:\/\/[^\s]+)/g,
130
+ tag: '<a--custom-tag href="{val}" target="_blank">{val}</a--custom-tag>',
131
+ },
132
+ };
133
+
134
+ function textToRichFormat(text: string) {
135
+ const richTextValues = Object.values(richText);
136
+
137
+ if (!richTextValues || (Array.isArray(richTextValues) && richTextValues.length === 0))
138
+ return text;
139
+
140
+ const newMessage = () => {
141
+
142
+ return richTextValues.reduce((textPrev, rtv) => {
143
+ return textPrev.replace(rtv.regex, (middleValueAssert, middleValue) => {
144
+ const regexVerifyExistTag = /--custom-tag/;
104
145
 
105
- // Reemplazar las URLs con etiquetas <a>
106
- return content.replace(urlRegex, (url) => {
107
- return `<a href="${url}" target="_blank">${url}</a>`;
108
- });
146
+ if (regexVerifyExistTag.test(middleValueAssert)) return middleValueAssert;
147
+
148
+ return rtv.tag.replace(/{val}/g, middleValue);
149
+ });
150
+ }, text);
151
+ };
152
+
153
+ return newMessage().replace(/--custom-tag/g, '');
109
154
  }
155
+
156
+ const emojis = [
157
+ { label: 'Excelente', value: 1, icon: '😊' },
158
+ { label: 'Buena', value: 2, icon: '😄' },
159
+ { label: 'Aceptable', value: 3, icon: '🙂' },
160
+ { label: 'Mala', value: 4, icon: '😐' },
161
+ { label: 'Muy Mala', value: 5, icon: '🙁' },
162
+ ]
163
+
110
164
  </script>
111
165
 
112
166
  <style scoped>
@@ -232,4 +286,32 @@ function convertUrlsToLinks(content:string) {
232
286
  .messages-container-list {
233
287
  width: 100%;
234
288
  }
289
+
290
+ .btn-icon {
291
+ text-decoration: none;
292
+ border: none;
293
+ }
294
+ .btn-icon span {
295
+ color: currentColor;
296
+ text-wrap: nowrap;
297
+ }
298
+
299
+ .btn-icon .icon {
300
+ font-size: 2rem;
301
+ }
302
+
303
+ .flex {
304
+ display: flex;
305
+ flex-wrap: wrap;
306
+ }
307
+ .flex-col {
308
+ flex-direction: column;
309
+ }
310
+ .items-center {
311
+ align-items: center;
312
+ }
313
+ .gap-2 {
314
+ gap: 0.5rem;
315
+ }
316
+
235
317
  </style>
@@ -50,6 +50,7 @@
50
50
  @clear-new-messages="newMessages = 0"
51
51
  @new-message="() => newMessages++"
52
52
  @not-viewed-total="(val) => (newMessages = val)"
53
+ @on-qualifying="(args)=> emit('onQualifying', args)"
53
54
  />
54
55
  </div>
55
56
  </div>
@@ -63,7 +64,7 @@ import IconTelegram from "./IconTelegram.vue";
63
64
  import IconWhatsApp from "./IconWhatsApp.vue";
64
65
  import IconChat from "./IconChat.vue";
65
66
 
66
- const emit = defineEmits(["show-toast", "show-confirm"]);
67
+ const emit = defineEmits(["show-toast", "show-confirm", "onQualifying"]);
67
68
 
68
69
  const enum MeansCommunication{
69
70
  WHATSAPP,