เพิ่ม Socket mark-read , notify-message

This commit is contained in:
net 2024-02-16 16:20:18 +07:00 committed by Net
parent 5b081878c4
commit 081c1e0589
6 changed files with 191 additions and 78 deletions

View file

@ -4,7 +4,11 @@ export const supportIssue = `${env.API_SUPPORT_URI}/issue`;
export const supportMessage = (id: string) => export const supportMessage = (id: string) =>
`${env.API_SUPPORT_URI}/issue/${id}/message`; `${env.API_SUPPORT_URI}/issue/${id}/message`;
export const supportMessageStatus = (id: string) =>
`${env.API_SUPPORT_URI}/message-status?issueId=${id}`;
export default { export default {
supportIssue, supportIssue,
supportMessage, supportMessage,
supportMessageStatus,
}; };

View file

@ -4,10 +4,9 @@ import { useSupportStore } from "@/modules/00_support/store/Main.ts";
import MessageChat from "@/modules/00_support/components/MessageChat.vue"; import MessageChat from "@/modules/00_support/components/MessageChat.vue";
const store = useSupportStore(); const store = useSupportStore();
const content = ref<string>(""); const content = ref<string>("");
const scrollContainerRef = ref(); const scrollContainerRef = ref();
const readStatus = ref<boolean>(false); const readStatus = ref<boolean>(true);
onUpdated(() => { onUpdated(() => {
nextTick(() => { nextTick(() => {
@ -43,75 +42,77 @@ onMounted(async () => {
</q-avatar> </q-avatar>
<q-item-section class="q-pl-sm"> <q-item-section class="q-pl-sm">
<q-item-label>{{ currentTitle }}</q-item-label> <q-item-label>{{ store.currentTitle }}</q-item-label>
<!-- <q-item-label caption>
<q-icon name="noise_control_off" color="positive" />
ออนไลน
</q-item-label> -->
</q-item-section> </q-item-section>
<q-space /> <q-space />
<!-- <q-btn
flat
round
dense
icon="o_videocam"
class="q-mr-xs"
text-color="primary"
/> -->
<q-btn flat round dense icon="o_info" text-color="grey" /> <q-btn flat round dense icon="o_info" text-color="grey" />
</q-toolbar> </q-toolbar>
</div> </div>
<div class="i3 bg-white"> <div class="i3 bg-white">
<q-list class="text-primary"> <q-scroll-area ref="scrollContainerRef" style="height: 400px; width: 1fr">
<div v-for="data in store.issue?.result"> <q-list class="text-primary">
<q-item <div v-for="data in store.issue?.result">
clickable <q-item
v-ripple clickable
:active="currentIssue === data.id" v-ripple
@click=" :active="store.currentIssue === data.id"
() => { @click="
store.currentIssue = data.id; store.currentIssue = data.id;
store.currentTitle = data.title; store.currentTitle = data.title;
store.fetchMessage(data.id); store.fetchMessage(data.id);
} store.fetchMessageStatus(data.id);
" store.issue.result = store.issue.result.map((v) => {
active-class="my-menu-link" if (v.id === data.id) {
> v.unreadCount = 0;
<q-avatar> }
<img src="https://cdn.quasar.dev/img/avatar1.jpg" /> return v;
</q-avatar> });
"
active-class="my-menu-link"
>
<q-avatar>
<img src="https://cdn.quasar.dev/img/avatar1.jpg" />
</q-avatar>
<q-item-section class="q-pl-sm"> <q-item-section class="q-pl-sm">
<q-item-label>{{ data.title }}</q-item-label> <q-item-label>{{ data.title }}</q-item-label>
<q-item-label class="flex" caption> <q-item-label class="flex" caption>
{{ data.lastMessage }} {{ data.lastMessage }}
<q-space />
<q-space /> <q-icon
<q-toggle v-model="readStatus" label="test" /> v-if="data.lastMessage?.length === 0"
<q-badge color="green"
v-if="readStatus" size="18px"
class="" name="mdi-send"
rounded />
color="red" <div v-else>
label="1" <q-badge
/> v-if="data.unreadCount > 0"
<q-icon v-else size="18px" name="done_all" /> class=""
</q-item-label> rounded
</q-item-section> color="red"
</q-item> :label="data.unreadCount"
<q-separator inset /> />
</div> <q-icon v-else size="18px" name="done_all" />
</q-list> </div>
</q-item-label>
</q-item-section>
</q-item>
<q-separator inset />
</div>
</q-list>
</q-scroll-area>
</div> </div>
<div class="i4 bg-grey-3"> <div class="i4 bg-grey-3">
<!-- <div class="i4 bg-grey-3 q-pa-sm"> -->
<q-scroll-area ref="scrollContainerRef" style="height: 400px; width: 1fr"> <q-scroll-area ref="scrollContainerRef" style="height: 400px; width: 1fr">
<message-chat class="q-pr-md" /> <message-chat v-if="store.currentIssue" class="q-pr-md" />
</q-scroll-area> </q-scroll-area>
</div> </div>
<div class="grid-manage bg-white"></div>
<div class="i5 bg-white container-input align-center"> <div class="i5 bg-white container-input align-center">
<div class="input-file"> <div class="input-file">
<q-btn flat> <q-btn flat>
@ -166,7 +167,7 @@ onMounted(async () => {
grid-template-areas: grid-template-areas:
"search toolbar" "search toolbar"
"menu chat" "menu chat"
"menu input"; "manage input";
grid-template-rows: 60px 400px 60px; grid-template-rows: 60px 400px 60px;
grid-template-columns: 400px 1fr; grid-template-columns: 400px 1fr;
} }
@ -206,6 +207,10 @@ onMounted(async () => {
.i5 { .i5 {
grid-area: input; grid-area: input;
} }
.grid-manage {
grid-area: manage;
}
.container-1 { .container-1 {
display: grid; display: grid;
/* grid-template-columns: 1fr 2fr; */ /* grid-template-columns: 1fr 2fr; */

View file

@ -7,21 +7,59 @@ import { useSupportStore } from "@/modules/00_support/store/Main";
const store = useSupportStore(); const store = useSupportStore();
onMounted(() => {}); onMounted(() => {});
function onLoad() {
console.log("ทำงานเเล้ว");
}
</script> </script>
<template> <template>
<div class="bg-grey-3 q-pb-sm"> <div class="bg-grey-3 q-pb-sm">
<q-chat-message label="Sunday, 19th" /> <q-infinite-scroll @load="onLoad" reverse>
<q-chat-message <template v-slot:loading>
v-for="(data, index) in store.message?.result.message" <div class="row justify-center q-my-md">
:key="index" <q-spinner color="primary" name="dots" size="40px" />
:id="data.id" </div>
avatar="https://cdn.quasar.dev/img/avatar4.jpg" </template>
:text="[data.content]" <q-chat-message label="Sunday, 19th" />
:bg-color="data.fromUserId === store.userId ? 'primary' : 'white'" <div v-for="(data, index) in store.message?.result.message">
:text-color="data.fromUserId === store.userId ? 'white' : 'black'" <q-item-section class="q-pl-sm">
:sent="data.fromUserId === store.userId" <q-item-label>
:stamp="moment(data.createdAt).fromNow()" <q-chat-message
/> :key="index"
:id="data.id"
avatar="https://cdn.quasar.dev/img/avatar4.jpg"
:text="[data.content]"
:bg-color="data.fromUserId === store.userId ? 'primary' : 'white'"
:text-color="data.fromUserId === store.userId ? 'white' : 'black'"
:sent="data.fromUserId === store.userId"
:stamp="moment(data.createdAt).fromNow()"
/>
</q-item-label>
<q-item-label
v-if="data.fromUserId === store.userId"
class="flex"
caption
>
<!-- <q-space v-if="data.fromUserId === store.userId" /> -->
<q-space />
<div
v-if="
store.messageStatus.result.some(
(v) =>
new Date(v.lastAccessDate).getTime() >=
new Date(data.createdAt).getTime() &&
index === store.message?.result.message.length - 1
)
"
>
านเเล
</div>
<!-- <q-toggle v-model="readStatus" label="test" /> -->
</q-item-label>
</q-item-section>
</div>
</q-infinite-scroll>
</div> </div>
</template> </template>

View file

@ -1,3 +1,14 @@
export interface SupportMessageStatus {
result: SupportResult[];
}
export interface SupportResult {
fromUserId: string;
fromUserName: string;
lastAccessDate: string;
issueId: string;
}
export interface SupportStatusUser { export interface SupportStatusUser {
socketId: string; socketId: string;
userId: string; userId: string;
@ -59,4 +70,9 @@ export interface SupportIssueMessage {
issueId: string; issueId: string;
} }
export type { SupportMessageResponse, SupportIssueResponse, SupportStatusUser }; export type {
SupportMessageStatus,
SupportMessageResponse,
SupportIssueResponse,
SupportStatusUser,
};

View file

@ -1,5 +1,5 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref } from "vue"; import { ref, onMounted } from "vue";
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import http from "@/plugins/http"; import http from "@/plugins/http";
import config from "@/app.config"; import config from "@/app.config";
@ -17,6 +17,7 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
const userId = ref<string>(keycloak.subject); const userId = ref<string>(keycloak.subject);
const issue = ref<SupportIssueResponse>(); const issue = ref<SupportIssueResponse>();
const message = ref<SupportMessageResponse>(); const message = ref<SupportMessageResponse>();
const messageStatus = ref<SupportMessageStatus>();
const statusUser = ref<SupportStatusUser>([]); const statusUser = ref<SupportStatusUser>([]);
const currentIssue = ref<string>(""); const currentIssue = ref<string>("");
const currentTitle = ref<string>(""); const currentTitle = ref<string>("");
@ -24,32 +25,56 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
const scrollContainer = ref(); const scrollContainer = ref();
function scrollToEnd() { function scrollToEnd() {
scrollContainer.value.setScrollPosition("vertical", 10000); scrollContainer.value.setScrollPosition("vertical", 1000000);
} }
const socket = io("http://192.168.1.10:3000/", { const socket = io("http://192.168.1.10:3000/", {
auth: { token: keycloak.token }, auth: { token: keycloak.token },
autoConnect: false,
}); });
socket.on("users", (users) => { socket.on("users", (data: SupportStatusUser[]) => {
// console.log(users); statusUser.value = data;
}); });
socket.on("online", (r) => { socket.on("online", (r) => {
console.log(r);
statusUser.value.push({ statusUser.value.push({
socketId: r.socketId, socketId: r.socketId,
userId: r.userId, userId: r.userId,
name: r.name, name: r.name,
role: r.role, role: r.role,
}); });
});
console.log(statusUser.value); socket.on("offline", (socketId: string) => {
if (socketId === socket.id) return;
statusUser.value = statusUser.value.filter((v) => v.socketId !== socketId);
});
// console.log(JSON.stringify(r, null, -2)); socket.on("notify-message", (r) => {
issue.value.result = issue.value.result.map((v) => {
if (v.id === r.issueId) {
v.unreadCount++;
v.lastMessage = r.content;
}
return v;
});
});
socket.on("read", (r) => {
setTimeout(() => {
messageStatus.value.result = messageStatus.value.result.map((v) => {
if (v.issueId === r.issueId) {
v.lastAccessDate = r.lastAccessDate;
}
return v;
});
}, 100);
}); });
socket.on("message", (r) => { socket.on("message", (r) => {
console.log(r);
message.value.result.message.push({ message.value.result.message.push({
id: r.id, id: r.id,
fromUserId: r.fromUserId, fromUserId: r.fromUserId,
@ -60,17 +85,29 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
read: r.read, read: r.read,
issueId: r.issueId, issueId: r.issueId,
}); });
socket.emit("mark-read", { currentIssue }); socket.emit("mark-read", { issueId: currentIssue.value });
scrollToEnd();
}); });
function sendMessage(content: string, to: string) { function sendMessage(content: string, to: string) {
socket.emit("message", { to, content }); socket.emit("message", { to, content });
setTimeout(() => { setTimeout(() => {
scrollToEnd(); scrollToEnd();
}, 100); }, 100);
} }
async function fetchMessageStatus(issueId: string) {
const res = await http
.get(config.API.supportMessageStatus(issueId))
.catch((err) => {
messageError($q, err);
})
.finally(() => {});
if (res && res.data) {
messageStatus.value = res.data;
}
}
async function fetchMessage(issueId: string) { async function fetchMessage(issueId: string) {
showLoader(); showLoader();
const res = await http const res = await http
@ -106,7 +143,6 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
if (res && res.data) { if (res && res.data) {
issue.value = res.data; issue.value = res.data;
// console.log(JSON.stringify(res.data, null, -2));
} }
} }
@ -116,11 +152,14 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
message, message,
fetchIssue, fetchIssue,
fetchMessage, fetchMessage,
fetchMessageStatus,
sendMessage, sendMessage,
items, items,
scrollToEnd, scrollToEnd,
scrollContainer, scrollContainer,
currentIssue, currentIssue,
currentTitle, currentTitle,
socket,
messageStatus,
}; };
}); });

View file

@ -1,5 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import FormChat from "@/modules/00_support/components/FormChat.vue"; import FormChat from "@/modules/00_support/components/FormChat.vue";
import { useSupportStore } from "@/modules/00_support/store/Main.ts";
import { onMounted, onUnmounted, ref } from "vue";
const store = useSupportStore();
onMounted(async () => {
store.socket.connect();
});
onUnmounted(async () => {
store.socket.disconnect();
});
</script> </script>
<template> <template>