Compare commits
No commits in common. "a5efec89f234154f8cbb791a437bdafa183e5326" and "05e8782557490202fd61df1605d26bcf95685319" have entirely different histories.
a5efec89f2
...
05e8782557
28
index.html
28
index.html
@ -1,20 +1,14 @@
|
|||||||
<!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>
|
||||||
<style>
|
<body>
|
||||||
html, body {
|
<div id="app"></div>
|
||||||
scroll-behavior: smooth;
|
<script type="module" src="/src/main.ts"></script>
|
||||||
}
|
</body>
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
<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,20 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-infinite-scroll
|
|
||||||
class="mt-[-16px] overflow-hidden"
|
|
||||||
:onLoad="props.loader"
|
|
||||||
>
|
|
||||||
<template v-for="item in props.messages" :key="item">
|
|
||||||
<chat-message class="mb-4" :item="item" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #empty>
|
|
||||||
<div class="flex-grow-1"></div>
|
|
||||||
</template>
|
|
||||||
</v-infinite-scroll>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import ChatMessage from "@/components/chat/ChatMessage.vue"
|
|
||||||
|
|
||||||
const props = defineProps<{ loader: (opts: any) => Promise<any>, messages: any[] }>()
|
|
||||||
</script>
|
|
@ -1,27 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<div>
|
|
||||||
<v-avatar
|
|
||||||
color="grey-lighten-2"
|
|
||||||
icon="mdi-account-circle"
|
|
||||||
class="rounded-card"
|
|
||||||
:image="props.item?.sender.account.avatar"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="font-bold text-sm">{{ props.item?.sender.account.nick }}</div>
|
|
||||||
<div>{{ props.item?.content }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ item: any }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.rounded-card {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -16,7 +16,7 @@
|
|||||||
v-if="item.type === 1"
|
v-if="item.type === 1"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
class="cursor-zoom-in content-visibility-auto w-full h-full object-contain"
|
class="cursor-zoom-in content-visibility-auto"
|
||||||
:src="getUrl(item)"
|
:src="getUrl(item)"
|
||||||
:alt="item.filename"
|
:alt="item.filename"
|
||||||
@click="openLightbox(item, idx)"
|
@click="openLightbox(item, idx)"
|
||||||
|
@ -38,13 +38,12 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/chat/:channel",
|
path: "/chat",
|
||||||
component: () => import("@/layouts/chat.vue"),
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: ":channel",
|
||||||
name: "chat.channel",
|
name: "chat.channel",
|
||||||
component: () => import("@/views/chat/page.vue"),
|
component: () => import("@/views/chat/channel.vue")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import { defineStore } from "pinia"
|
|
||||||
import { reactive, ref } from "vue"
|
|
||||||
import { checkLoggedIn, getAtk } from "@/stores/userinfo"
|
|
||||||
import { buildRequestUrl, request } from "@/scripts/request"
|
|
||||||
|
|
||||||
export const useChannels = defineStore("channels", () => {
|
|
||||||
let socket: WebSocket
|
|
||||||
|
|
||||||
const done = ref(false)
|
|
||||||
|
|
||||||
const show = reactive({
|
|
||||||
editor: false,
|
|
||||||
delete: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const related_to = reactive<{ edit_to: any; delete_to: any }>({
|
|
||||||
edit_to: null,
|
|
||||||
delete_to: null
|
|
||||||
})
|
|
||||||
|
|
||||||
const available = ref<any[]>([])
|
|
||||||
const current = ref<any>(null)
|
|
||||||
const messages = ref<any[]>([])
|
|
||||||
|
|
||||||
async function list() {
|
|
||||||
if (!(await checkLoggedIn())) return
|
|
||||||
|
|
||||||
const res = await request("messaging", "/api/channels/me/available", {
|
|
||||||
headers: { Authorization: `Bearer ${await getAtk()}` }
|
|
||||||
})
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error(await res.text())
|
|
||||||
} else {
|
|
||||||
available.value = await res.json()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
})
|
|
@ -31,5 +31,7 @@ export const useRealms = defineStore("realms", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list().then(() => console.log("[STARTUP HOOK] Fetch available realm successes."))
|
||||||
|
|
||||||
return { done, show, related: related_to, available, list }
|
return { done, show, related: related_to, available, list }
|
||||||
})
|
})
|
||||||
|
@ -2,8 +2,6 @@ import { defineStore } from "pinia"
|
|||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import { request } from "@/scripts/request"
|
import { request } from "@/scripts/request"
|
||||||
import { Preferences } from "@capacitor/preferences"
|
import { Preferences } from "@capacitor/preferences"
|
||||||
import { useRealms } from "@/stores/realms"
|
|
||||||
import { useChannels } from "@/stores/channels"
|
|
||||||
|
|
||||||
export interface Userinfo {
|
export interface Userinfo {
|
||||||
isReady: boolean
|
isReady: boolean
|
||||||
@ -32,10 +30,6 @@ export async function checkLoggedIn(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useUserinfo = defineStore("userinfo", () => {
|
export const useUserinfo = defineStore("userinfo", () => {
|
||||||
const userinfoHooks = {
|
|
||||||
after: [useRealms().list, useChannels().list, useChannels().connect]
|
|
||||||
}
|
|
||||||
|
|
||||||
const userinfo = ref(defaultUserinfo)
|
const userinfo = ref(defaultUserinfo)
|
||||||
const isReady = ref(false)
|
const isReady = ref(false)
|
||||||
|
|
||||||
@ -60,8 +54,6 @@ export const useUserinfo = defineStore("userinfo", () => {
|
|||||||
displayName: data["nick"],
|
displayName: data["nick"],
|
||||||
data: data
|
data: data
|
||||||
}
|
}
|
||||||
|
|
||||||
userinfoHooks.after.forEach((call) => call())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { userinfo, isReady, readProfiles }
|
return { userinfo, isReady, readProfiles }
|
||||||
|
@ -3,14 +3,12 @@
|
|||||||
<div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center">
|
<div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center">
|
||||||
<v-app-bar-nav-icon icon="mdi-chat" :loading="loading" />
|
<v-app-bar-nav-icon icon="mdi-chat" :loading="loading" />
|
||||||
|
|
||||||
<h2 class="ml-2 text-lg font-500">{{ channels.current?.name }}</h2>
|
<h2 class="ml-2 text-lg font-500">{{ metadata?.name }}</h2>
|
||||||
|
|
||||||
<p class="ml-3 text-xs opacity-80">{{ channels.current?.description }}</p>
|
<p class="ml-3 text-xs opacity-80">{{ metadata?.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
<router-view />
|
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
<!-- @vue-ignore -->
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
||||||
</template>
|
</template>
|
||||||
@ -18,15 +16,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { request } from "@/scripts/request"
|
import { request } from "@/scripts/request"
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute } from "vue-router"
|
||||||
import { ref, watch } from "vue"
|
import { ref } from "vue"
|
||||||
import { useChannels } from "@/stores/channels"
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const channels = useChannels()
|
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const metadata = ref<any>(null)
|
||||||
|
|
||||||
async function readMetadata() {
|
async function readMetadata() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await request("messaging", `/api/channels/${route.params.channel}`)
|
const res = await request("messaging", `/api/channels/${route.params.channel}`)
|
||||||
@ -34,17 +32,10 @@ async function readMetadata() {
|
|||||||
error.value = await res.text()
|
error.value = await res.text()
|
||||||
} else {
|
} else {
|
||||||
error.value = null
|
error.value = null
|
||||||
channels.current = await res.json()
|
metadata.value = await res.json()
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
readMetadata()
|
||||||
() => route.params.channel,
|
|
||||||
() => {
|
|
||||||
channels.messages = []
|
|
||||||
readMetadata()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
@ -1,84 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-container fluid class="px-6">
|
|
||||||
<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">
|
|
||||||
<chat-editor class="flex-grow-1" @sent="scrollTop" />
|
|
||||||
</v-footer>
|
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
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 pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
|
||||||
|
|
||||||
async function readHistory() {
|
|
||||||
loading.value = true
|
|
||||||
const res = await request(
|
|
||||||
"messaging",
|
|
||||||
`/api/channels/${route.params.channel}/messages?` + new URLSearchParams({
|
|
||||||
take: pagination.pageSize.toString(),
|
|
||||||
offset: ((pagination.page - 1) * pagination.pageSize).toString()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
if (res.status !== 200) {
|
|
||||||
error.value = await res.text()
|
|
||||||
} else {
|
|
||||||
const data = await res.json()
|
|
||||||
pagination.total = data["count"]
|
|
||||||
channels.messages.push(...(data["data"] ?? []))
|
|
||||||
error.value = null
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
pagination.page++
|
|
||||||
await readHistory()
|
|
||||||
|
|
||||||
if (error.value != null) done("error")
|
|
||||||
else {
|
|
||||||
if (pagination.total > 0) done("ok")
|
|
||||||
else done("empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => channels.current, (val) => {
|
|
||||||
if (val) {
|
|
||||||
readHistory()
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
function scrollTop() {
|
|
||||||
window.scroll({ top: 0 })
|
|
||||||
}
|
|
||||||
</script>
|
|
Reference in New Issue
Block a user