✨ Chat message send and read history
This commit is contained in:
		
							
								
								
									
										28
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								index.html
									
									
									
									
									
								
							| @@ -1,14 +1,20 @@ | |||||||
| <!doctype html> | <!doctype html> | ||||||
| <html lang="en"> | <html lang="en"> | ||||||
|   <head> | <head> | ||||||
|     <meta charset="UTF-8" /> |   <meta charset="UTF-8" /> | ||||||
|     <link rel="icon" type="image/png" href="/favicon.png" /> |   <link rel="icon" type="image/png" href="/favicon.png" /> | ||||||
|     <link rel="apple-touch-icon" type="image/png" href="/apple-touch-icon.png" sizes="1024x1024"> |   <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" /> |   <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> | ||||||
|     <title>Solian</title> |   <title>Solian</title> | ||||||
|   </head> |  | ||||||
|   <body> |   <style> | ||||||
|     <div id="app"></div> |       html, body { | ||||||
|     <script type="module" src="/src/main.ts"></script> |           scroll-behavior: smooth; | ||||||
|   </body> |       } | ||||||
|  |   </style> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | <div id="app"></div> | ||||||
|  | <script type="module" src="/src/main.ts"></script> | ||||||
|  | </body> | ||||||
| </html> | </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> | <template> | ||||||
|   <v-infinite-scroll |   <v-infinite-scroll | ||||||
|     side="start" |     class="mt-[-16px] overflow-hidden" | ||||||
|     class="mt-[-16px]" |     :onLoad="props.loader" | ||||||
|     @load="props.loader" |  | ||||||
|   > |   > | ||||||
|     <template v-for="item in props.messages" :key="item"> |     <template v-for="item in props.messages" :key="item"> | ||||||
|       <chat-message class="mb-4" :item="item" /> |       <chat-message class="mb-4" :item="item" /> | ||||||
| @@ -17,5 +16,5 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import ChatMessage from "@/components/chat/ChatMessage.vue" | 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> | </script> | ||||||
| @@ -5,12 +5,12 @@ | |||||||
|         color="grey-lighten-2" |         color="grey-lighten-2" | ||||||
|         icon="mdi-account-circle" |         icon="mdi-account-circle" | ||||||
|         class="rounded-card" |         class="rounded-card" | ||||||
|         :image="props.item?.sender.avatar" |         :image="props.item?.sender.account.avatar" | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="flex-grow-1"> |     <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>{{ props.item?.content }}</div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
| import { defineStore } from "pinia" | import { defineStore } from "pinia" | ||||||
| import { reactive, ref } from "vue" | import { reactive, ref } from "vue" | ||||||
| import { checkLoggedIn, getAtk } from "@/stores/userinfo" | import { checkLoggedIn, getAtk } from "@/stores/userinfo" | ||||||
| import { request } from "@/scripts/request" | import { buildRequestUrl, request } from "@/scripts/request" | ||||||
|  |  | ||||||
| export const useChannels = defineStore("channels", () => { | export const useChannels = defineStore("channels", () => { | ||||||
|  |   let socket: WebSocket | ||||||
|  |  | ||||||
|   const done = ref(false) |   const done = ref(false) | ||||||
|  |  | ||||||
|   const show = reactive({ |   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", () => { | export const useUserinfo = defineStore("userinfo", () => { | ||||||
|   const userinfoHooks = { |   const userinfoHooks = { | ||||||
|     after: [useRealms().list, useChannels().list] |     after: [useRealms().list, useChannels().list, useChannels().connect] | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const userinfo = ref(defaultUserinfo) |   const userinfo = ref(defaultUserinfo) | ||||||
|   | |||||||
| @@ -1,25 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <v-container fluid class="px-6"> |   <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-container> | ||||||
|  |  | ||||||
|   <v-footer app class="flex items-center border-opacity-15 min-h-[64px]" style="border-top-width: thin"> |   <v-footer app class="flex items-center border-opacity-15 min-h-[64px]" style="border-top-width: thin"> | ||||||
|     <v-form class="flex-grow-1"> |     <chat-editor class="flex-grow-1" @sent="scrollTop" /> | ||||||
|       <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> |  | ||||||
|   </v-footer> |   </v-footer> | ||||||
|  |  | ||||||
|   <!-- @vue-ignore --> |   <!-- @vue-ignore --> | ||||||
| @@ -30,13 +17,17 @@ | |||||||
| import { useChannels } from "@/stores/channels" | import { useChannels } from "@/stores/channels" | ||||||
| import { request } from "@/scripts/request" | import { request } from "@/scripts/request" | ||||||
| import { reactive, ref, watch } from "vue" | import { reactive, ref, watch } from "vue" | ||||||
|  | import { useRoute } from "vue-router" | ||||||
| import ChatList from "@/components/chat/ChatList.vue" | import ChatList from "@/components/chat/ChatList.vue" | ||||||
|  | import ChatEditor from "@/components/chat/ChatEditor.vue" | ||||||
|  |  | ||||||
|  | const route = useRoute() | ||||||
| const channels = useChannels() | const channels = useChannels() | ||||||
|  |  | ||||||
|  | const chatList = ref<HTMLDivElement>() | ||||||
|  |  | ||||||
| const error = ref<string | null>(null) | const error = ref<string | null>(null) | ||||||
| const loading = ref(false) | const loading = ref(false) | ||||||
| const submitting = ref(false) |  | ||||||
|  |  | ||||||
| const pagination = reactive({ page: 1, pageSize: 10, total: 0 }) | const pagination = reactive({ page: 1, pageSize: 10, total: 0 }) | ||||||
|  |  | ||||||
| @@ -44,7 +35,7 @@ async function readHistory() { | |||||||
|   loading.value = true |   loading.value = true | ||||||
|   const res = await request( |   const res = await request( | ||||||
|     "messaging", |     "messaging", | ||||||
|     `/api/channels/${channels.current.alias}/messages?` + new URLSearchParams({ |     `/api/channels/${route.params.channel}/messages?` + new URLSearchParams({ | ||||||
|       take: pagination.pageSize.toString(), |       take: pagination.pageSize.toString(), | ||||||
|       offset: ((pagination.page - 1) * pagination.pageSize).toString() |       offset: ((pagination.page - 1) * pagination.pageSize).toString() | ||||||
|     }) |     }) | ||||||
| @@ -54,7 +45,7 @@ async function readHistory() { | |||||||
|   } else { |   } else { | ||||||
|     const data = await res.json() |     const data = await res.json() | ||||||
|     pagination.total = data["count"] |     pagination.total = data["count"] | ||||||
|     channels.messages = data["data"] |     channels.messages.push(...(data["data"] ?? [])) | ||||||
|     error.value = null |     error.value = null | ||||||
|   } |   } | ||||||
|   loading.value = false |   loading.value = false | ||||||
| @@ -62,6 +53,10 @@ async function readHistory() { | |||||||
|  |  | ||||||
| async function readMore({ done }: any) { | async function readMore({ done }: any) { | ||||||
|   // Reach the end of data |   // Reach the end of data | ||||||
|  |   if (pagination.total === 0) { | ||||||
|  |     done("ok") | ||||||
|  |     return | ||||||
|  |   } | ||||||
|   if (pagination.total <= pagination.page * pagination.pageSize) { |   if (pagination.total <= pagination.page * pagination.pageSize) { | ||||||
|     done("empty") |     done("empty") | ||||||
|     return |     return | ||||||
| @@ -82,4 +77,8 @@ watch(() => channels.current, (val) => { | |||||||
|     readHistory() |     readHistory() | ||||||
|   } |   } | ||||||
| }, { immediate: true }) | }, { immediate: true }) | ||||||
|  |  | ||||||
|  | function scrollTop() { | ||||||
|  |   window.scroll({ top: 0 }) | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
		Reference in New Issue
	
	Block a user