✨ Chat message send and read history
This commit is contained in:
parent
8bb9816cd0
commit
a5efec89f2
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