✨ Chat message send and read history
This commit is contained in:
		
							
								
								
									
										28
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								index.html
									
									
									
									
									
								
							| @@ -1,14 +1,20 @@ | ||||
| <!doctype html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" type="image/png" href="/favicon.png" /> | ||||
|     <link rel="apple-touch-icon" type="image/png" href="/apple-touch-icon.png" sizes="1024x1024"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> | ||||
|     <title>Solian</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|     <script type="module" src="/src/main.ts"></script> | ||||
|   </body> | ||||
| <head> | ||||
|   <meta charset="UTF-8" /> | ||||
|   <link rel="icon" type="image/png" href="/favicon.png" /> | ||||
|   <link rel="apple-touch-icon" type="image/png" href="/apple-touch-icon.png" sizes="1024x1024"> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> | ||||
|   <title>Solian</title> | ||||
|  | ||||
|   <style> | ||||
|       html, body { | ||||
|           scroll-behavior: smooth; | ||||
|       } | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
| <div id="app"></div> | ||||
| <script type="module" src="/src/main.ts"></script> | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
							
								
								
									
										62
									
								
								src/components/chat/ChatEditor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/components/chat/ChatEditor.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| <template> | ||||
|   <v-form class="flex-grow-1" ref="chat" @submit.prevent="sendMessage"> | ||||
|     <v-textarea | ||||
|       auto-grow | ||||
|       hide-details | ||||
|       class="w-full" | ||||
|       variant="outlined" | ||||
|       density="compact" | ||||
|       placeholder="Enter some messages..." | ||||
|       :rows="1" | ||||
|       :max-rows="6" | ||||
|       :loading="loading" | ||||
|       v-model="data.content" | ||||
|       @keyup.ctrl.enter="sendMessage" | ||||
|       @keyup.meta.enter="sendMessage" | ||||
|     > | ||||
|       <template #append> | ||||
|         <v-btn type="submit" icon="mdi-send" size="small" variant="text" :disabled="loading" /> | ||||
|       </template> | ||||
|     </v-textarea> | ||||
|  | ||||
|     <!-- @vue-ignore --> | ||||
|     <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar> | ||||
|   </v-form> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref } from "vue" | ||||
| import { request } from "@/scripts/request" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
| import { useChannels } from "@/stores/channels" | ||||
|  | ||||
| const emits = defineEmits(["sent"]) | ||||
|  | ||||
| const chat = ref<HTMLFormElement>() | ||||
| const channels = useChannels() | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
| const loading = ref(false) | ||||
|  | ||||
| const data = ref<any>({ | ||||
|   content: "", | ||||
|   attachments: [] | ||||
| }) | ||||
|  | ||||
| async function sendMessage() { | ||||
|   loading.value = true | ||||
|   const res = await request("messaging", `/api/channels/${channels.current.alias}/messages`, { | ||||
|     method: "POST", | ||||
|     headers: { Authorization: `Bearer ${await getAtk()}`, "Content-Type": "application/json" }, | ||||
|     body: JSON.stringify(data.value) | ||||
|   }) | ||||
|   if (res.status !== 200) { | ||||
|     error.value = await res.text() | ||||
|   } else { | ||||
|     emits("sent") | ||||
|     chat.value?.reset() | ||||
|     error.value = null | ||||
|   } | ||||
|   loading.value = false | ||||
| } | ||||
| </script> | ||||
| @@ -1,8 +1,7 @@ | ||||
| <template> | ||||
|   <v-infinite-scroll | ||||
|     side="start" | ||||
|     class="mt-[-16px]" | ||||
|     @load="props.loader" | ||||
|     class="mt-[-16px] overflow-hidden" | ||||
|     :onLoad="props.loader" | ||||
|   > | ||||
|     <template v-for="item in props.messages" :key="item"> | ||||
|       <chat-message class="mb-4" :item="item" /> | ||||
| @@ -17,5 +16,5 @@ | ||||
| <script setup lang="ts"> | ||||
| import ChatMessage from "@/components/chat/ChatMessage.vue" | ||||
|  | ||||
| const props = defineProps<{ loader: any, messages: any[] }>() | ||||
| const props = defineProps<{ loader: (opts: any) => Promise<any>, messages: any[] }>() | ||||
| </script> | ||||
| @@ -5,12 +5,12 @@ | ||||
|         color="grey-lighten-2" | ||||
|         icon="mdi-account-circle" | ||||
|         class="rounded-card" | ||||
|         :image="props.item?.sender.avatar" | ||||
|         :image="props.item?.sender.account.avatar" | ||||
|       /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex-grow-1"> | ||||
|       <div class="font-bold text-sm">{{ props.item?.sender.nick }}</div> | ||||
|       <div class="font-bold text-sm">{{ props.item?.sender.account.nick }}</div> | ||||
|       <div>{{ props.item?.content }}</div> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import { defineStore } from "pinia" | ||||
| import { reactive, ref } from "vue" | ||||
| import { checkLoggedIn, getAtk } from "@/stores/userinfo" | ||||
| import { request } from "@/scripts/request" | ||||
| import { buildRequestUrl, request } from "@/scripts/request" | ||||
|  | ||||
| export const useChannels = defineStore("channels", () => { | ||||
|   let socket: WebSocket | ||||
|  | ||||
|   const done = ref(false) | ||||
|  | ||||
|   const show = reactive({ | ||||
| @@ -33,5 +35,34 @@ export const useChannels = defineStore("channels", () => { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return { done, show, related_to, available, current, messages, list } | ||||
|   async function connect() { | ||||
|     if (!(await checkLoggedIn())) return | ||||
|  | ||||
|     const uri = buildRequestUrl("messaging", "/api/unified").replace("http", "ws") | ||||
|  | ||||
|     socket = new WebSocket(uri + `?tk=${await getAtk() as string}`) | ||||
|  | ||||
|     socket.addEventListener("open", (event) => { | ||||
|       console.log("[MESSAGING] The unified websocket has been established... ", event.type) | ||||
|     }) | ||||
|     socket.addEventListener("close", (event) => { | ||||
|       console.warn("[MESSAGING] The unified websocket is disconnected... ", event.reason, event.code) | ||||
|     }) | ||||
|     socket.addEventListener("message", (event) => { | ||||
|       const data = JSON.parse(event.data) | ||||
|       const payload = data["p"] | ||||
|       if (payload?.channel_id === current.value.id) { | ||||
|         switch (data["w"]) { | ||||
|           case "messages.new": | ||||
|             messages.value.unshift(payload) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   function disconnect() { | ||||
|     socket.close() | ||||
|   } | ||||
|  | ||||
|   return { done, show, related_to, available, current, messages, list, connect, disconnect } | ||||
| }) | ||||
| @@ -33,7 +33,7 @@ export async function checkLoggedIn(): Promise<boolean> { | ||||
|  | ||||
| export const useUserinfo = defineStore("userinfo", () => { | ||||
|   const userinfoHooks = { | ||||
|     after: [useRealms().list, useChannels().list] | ||||
|     after: [useRealms().list, useChannels().list, useChannels().connect] | ||||
|   } | ||||
|  | ||||
|   const userinfo = ref(defaultUserinfo) | ||||
|   | ||||
| @@ -1,25 +1,12 @@ | ||||
| <template> | ||||
|   <v-container fluid class="px-6"> | ||||
|     <chat-list :loader="readMore" :messages="channels.messages" /> | ||||
|     <div class="message-list"> | ||||
|       <chat-list :loader="readMore" :messages="channels.messages" /> | ||||
|     </div> | ||||
|   </v-container> | ||||
|  | ||||
|   <v-footer app class="flex items-center border-opacity-15 min-h-[64px]" style="border-top-width: thin"> | ||||
|     <v-form class="flex-grow-1"> | ||||
|       <v-textarea | ||||
|         class="w-full" | ||||
|         variant="outlined" | ||||
|         density="compact" | ||||
|         placeholder="Enter some messages..." | ||||
|         :rows="1" | ||||
|         :max-rows="6" | ||||
|         auto-grow | ||||
|         hide-details | ||||
|       > | ||||
|         <template #append-inner> | ||||
|           <v-btn icon="mdi-send" size="x-small" variant="text" /> | ||||
|         </template> | ||||
|       </v-textarea> | ||||
|     </v-form> | ||||
|     <chat-editor class="flex-grow-1" @sent="scrollTop" /> | ||||
|   </v-footer> | ||||
|  | ||||
|   <!-- @vue-ignore --> | ||||
| @@ -30,13 +17,17 @@ | ||||
| import { useChannels } from "@/stores/channels" | ||||
| import { request } from "@/scripts/request" | ||||
| import { reactive, ref, watch } from "vue" | ||||
| import { useRoute } from "vue-router" | ||||
| import ChatList from "@/components/chat/ChatList.vue" | ||||
| import ChatEditor from "@/components/chat/ChatEditor.vue" | ||||
|  | ||||
| const route = useRoute() | ||||
| const channels = useChannels() | ||||
|  | ||||
| const chatList = ref<HTMLDivElement>() | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
| const loading = ref(false) | ||||
| const submitting = ref(false) | ||||
|  | ||||
| const pagination = reactive({ page: 1, pageSize: 10, total: 0 }) | ||||
|  | ||||
| @@ -44,7 +35,7 @@ async function readHistory() { | ||||
|   loading.value = true | ||||
|   const res = await request( | ||||
|     "messaging", | ||||
|     `/api/channels/${channels.current.alias}/messages?` + new URLSearchParams({ | ||||
|     `/api/channels/${route.params.channel}/messages?` + new URLSearchParams({ | ||||
|       take: pagination.pageSize.toString(), | ||||
|       offset: ((pagination.page - 1) * pagination.pageSize).toString() | ||||
|     }) | ||||
| @@ -54,7 +45,7 @@ async function readHistory() { | ||||
|   } else { | ||||
|     const data = await res.json() | ||||
|     pagination.total = data["count"] | ||||
|     channels.messages = data["data"] | ||||
|     channels.messages.push(...(data["data"] ?? [])) | ||||
|     error.value = null | ||||
|   } | ||||
|   loading.value = false | ||||
| @@ -62,6 +53,10 @@ async function readHistory() { | ||||
|  | ||||
| async function readMore({ done }: any) { | ||||
|   // Reach the end of data | ||||
|   if (pagination.total === 0) { | ||||
|     done("ok") | ||||
|     return | ||||
|   } | ||||
|   if (pagination.total <= pagination.page * pagination.pageSize) { | ||||
|     done("empty") | ||||
|     return | ||||
| @@ -82,4 +77,8 @@ watch(() => channels.current, (val) => { | ||||
|     readHistory() | ||||
|   } | ||||
| }, { immediate: true }) | ||||
|  | ||||
| function scrollTop() { | ||||
|   window.scroll({ top: 0 }) | ||||
| } | ||||
| </script> | ||||
		Reference in New Issue
	
	Block a user