Compare commits
7 Commits
v2.0.0+2
...
a5efec89f2
Author | SHA1 | Date | |
---|---|---|---|
a5efec89f2 | |||
8bb9816cd0 | |||
05e8782557 | |||
e986ff8c5f | |||
c616214c3b | |||
f552cdcf74 | |||
d187ca0a88 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -29,3 +29,5 @@ coverage
|
||||
|
||||
*.tsbuildinfo
|
||||
*.lockb
|
||||
|
||||
*dist
|
21
index.html
21
index.html
@@ -1,13 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/xml+svg" 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">
|
||||
<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>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"icons": [
|
||||
{
|
||||
"src": "../icons/icon-48.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "48x48",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-72.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "72x72",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-96.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-128.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "128x128",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-192.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-256.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-512.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
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>
|
20
src/components/chat/ChatList.vue
Normal file
20
src/components/chat/ChatList.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<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>
|
27
src/components/chat/ChatMessage.vue
Normal file
27
src/components/chat/ChatMessage.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<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>
|
@@ -3,12 +3,25 @@
|
||||
Attached {{ props.attachments.length }} attachment(s)
|
||||
</v-chip>
|
||||
|
||||
<v-card v-else variant="outlined" class="max-w-[540px] max-h-[720px]">
|
||||
<v-carousel hide-delimiter-background height="100%" :show-arrows="false">
|
||||
<v-carousel-item v-for="item in attachments">
|
||||
<img v-if="item.type === 1" :src="getUrl(item)" :alt="item.filename" class="cursor-zoom-in"
|
||||
@click="openLightbox" />
|
||||
<video v-if="item.type === 2" controls class="w-full">
|
||||
<v-responsive v-else :aspect-ratio="16 / 9" max-height="720">
|
||||
<v-card variant="outlined" class="w-full h-full">
|
||||
<v-carousel
|
||||
hide-delimiter-background
|
||||
height="100%"
|
||||
:hide-delimiters="props.attachments.length <= 1"
|
||||
:show-arrows="false"
|
||||
>
|
||||
<v-carousel-item v-for="(item, idx) in attachments">
|
||||
<img
|
||||
v-if="item.type === 1"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="cursor-zoom-in content-visibility-auto w-full h-full object-contain"
|
||||
:src="getUrl(item)"
|
||||
:alt="item.filename"
|
||||
@click="openLightbox(item, idx)"
|
||||
/>
|
||||
<video v-if="item.type === 2" controls class="w-full content-visibility-auto">
|
||||
<source :src="getUrl(item)" />
|
||||
</video>
|
||||
<div v-if="item.type === 3" class="w-full px-7 py-12">
|
||||
@@ -17,7 +30,13 @@
|
||||
</v-carousel-item>
|
||||
</v-carousel>
|
||||
|
||||
<vue-easy-lightbox teleport="#app" :visible="lightbox" :imgs="[getUrl(current)]" @hide="lightbox = false">
|
||||
<vue-easy-lightbox
|
||||
teleport="#app"
|
||||
:visible="lightbox"
|
||||
:imgs="props.attachments.map((x) => getUrl(x))"
|
||||
v-model:index="currentIndex"
|
||||
@hide="lightbox = false"
|
||||
>
|
||||
<template v-slot:close-btn="{ close }">
|
||||
<v-btn
|
||||
class="fixed left-2 top-2"
|
||||
@@ -30,6 +49,7 @@
|
||||
</template>
|
||||
</vue-easy-lightbox>
|
||||
</v-card>
|
||||
</v-responsive>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -43,23 +63,24 @@ const props = defineProps<{ attachments: any[]; overview?: boolean }>()
|
||||
const ui = useUI()
|
||||
|
||||
const lightbox = ref(false)
|
||||
const focus = ref(0)
|
||||
|
||||
const current = computed(() => props.attachments[focus.value])
|
||||
const canLightbox = computed(() => current.value.type === 1)
|
||||
const current = ref<any>(null)
|
||||
const currentIndex = ref(0)
|
||||
|
||||
const safeAreaTop = computed(() => {
|
||||
return `${ui.safeArea.top}px`
|
||||
})
|
||||
|
||||
function getUrl(item: any) {
|
||||
return item.external_url ? item.external_url : buildRequestUrl("interactive", `/api/attachments/o/${item.file_id}`)
|
||||
return item.external_url
|
||||
? item.external_url
|
||||
: buildRequestUrl("interactive", `/api/attachments/o/${item.file_id}`)
|
||||
}
|
||||
|
||||
function openLightbox() {
|
||||
if (canLightbox.value) {
|
||||
function openLightbox(item: any, idx: number) {
|
||||
current.value = item
|
||||
currentIndex.value = idx
|
||||
lightbox.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="post-list">
|
||||
<div class="post-list mx-[-8px]">
|
||||
<v-infinite-scroll :items="props.posts" :onLoad="props.loader">
|
||||
<template v-for="(item, idx) in props.posts" :key="item">
|
||||
<div class="mb-3 px-1">
|
||||
<div class="mb-3 px-[8px]">
|
||||
<v-card>
|
||||
<template #text>
|
||||
<post-item brief :item="item" @update:item="(val) => updateItem(idx, val)" />
|
||||
|
50
src/layouts/chat.vue
Normal file
50
src/layouts/chat.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<v-app-bar :order="5" color="grey-lighten-3">
|
||||
<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" />
|
||||
|
||||
<h2 class="ml-2 text-lg font-500">{{ channels.current?.name }}</h2>
|
||||
|
||||
<p class="ml-3 text-xs opacity-80">{{ channels.current?.description }}</p>
|
||||
</div>
|
||||
</v-app-bar>
|
||||
|
||||
<router-view />
|
||||
|
||||
<!-- @vue-ignore -->
|
||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { request } from "@/scripts/request"
|
||||
import { useRoute } from "vue-router"
|
||||
import { ref, watch } from "vue"
|
||||
import { useChannels } from "@/stores/channels"
|
||||
|
||||
const route = useRoute()
|
||||
const channels = useChannels()
|
||||
|
||||
const error = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function readMetadata() {
|
||||
loading.value = true
|
||||
const res = await request("messaging", `/api/channels/${route.params.channel}`)
|
||||
if (res.status !== 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
error.value = null
|
||||
channels.current = await res.json()
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.params.channel,
|
||||
() => {
|
||||
channels.messages = []
|
||||
readMetadata()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
@@ -6,17 +6,24 @@
|
||||
:rail="drawerMini"
|
||||
:rail-width="58"
|
||||
:order="0"
|
||||
floating
|
||||
@click="drawerMini = false"
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex items-center justify-between px-3 pb-2.5 border-opacity-15 min-h-[64px]"
|
||||
style="border-bottom-width: thin"
|
||||
:style="`padding-top: max(${safeAreaTop}, 10px)`">
|
||||
<v-toolbar
|
||||
class="flex items-center justify-between px-[14px] border-opacity-15"
|
||||
color="primary"
|
||||
height="64"
|
||||
:style="`padding-top: ${safeAreaTop}`"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<img src="/favicon.png" alt="Logo" width="36" height="36" class="block" />
|
||||
<img src="/favicon.png" alt="Logo" width="32" height="32" class="block" />
|
||||
<div v-show="!drawerMini" class="ms-6 font-medium">Solar Network</div>
|
||||
</div>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<div>
|
||||
<v-btn
|
||||
v-show="!drawerMini"
|
||||
icon="mdi-arrow-collapse-left"
|
||||
@@ -25,6 +32,7 @@
|
||||
@click.stop="drawerMini = true"
|
||||
/>
|
||||
</div>
|
||||
</v-toolbar>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<realm-list />
|
||||
@@ -75,22 +83,6 @@
|
||||
<v-main id="main">
|
||||
<router-view />
|
||||
</v-main>
|
||||
|
||||
<v-menu open-on-hover open-on-click :open-delay="0" :close-delay="0" location="top"
|
||||
transition="scroll-y-reverse-transition">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-fab v-bind="props" appear class="editor-fab" icon="mdi-pencil" color="primary" size="64"
|
||||
:active="id.userinfo.isLoggedIn" />
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col items-center gap-4 mb-4">
|
||||
<v-btn variant="elevated" color="secondary" icon="mdi-newspaper-variant" @click="editor.show.article = true" />
|
||||
<v-btn variant="elevated" color="accent" icon="mdi-camera-iris" @click="editor.show.moment = true" />
|
||||
</div>
|
||||
</v-menu>
|
||||
|
||||
<post-tools />
|
||||
<realm-tools />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -115,7 +107,6 @@ const safeAreaBottom = computed(() => {
|
||||
})
|
||||
|
||||
const id = useUserinfo()
|
||||
const editor = useEditor()
|
||||
|
||||
const username = computed(() => {
|
||||
if (id.userinfo.isLoggedIn) {
|
||||
@@ -146,10 +137,3 @@ const drawerOpen = ref(true)
|
||||
const drawerMini = ref(false)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-fab {
|
||||
position: fixed !important;
|
||||
bottom: 16px;
|
||||
right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
65
src/layouts/plaza.vue
Normal file
65
src/layouts/plaza.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<router-view />
|
||||
|
||||
<v-fab
|
||||
appear
|
||||
class="editor-fab"
|
||||
color="primary"
|
||||
size="64"
|
||||
:active="id.userinfo.isLoggedIn"
|
||||
>
|
||||
<v-icon icon="mdi-pencil" />
|
||||
|
||||
<v-speed-dial
|
||||
target=".editor-fab"
|
||||
activator="parent"
|
||||
location="top center"
|
||||
class="editor-speed-dial"
|
||||
transition="slide-y-reverse-transition"
|
||||
open-on-hover
|
||||
open-on-click
|
||||
>
|
||||
<v-btn
|
||||
key="article"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
icon="mdi-newspaper-variant"
|
||||
@click="editor.show.article = true"
|
||||
/>
|
||||
<v-btn
|
||||
key="moment"
|
||||
variant="elevated"
|
||||
color="accent"
|
||||
icon="mdi-camera-iris"
|
||||
@click="editor.show.moment = true"
|
||||
/>
|
||||
</v-speed-dial>
|
||||
</v-fab>
|
||||
|
||||
<post-tools />
|
||||
<realm-tools />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PostTools from "@/components/publish/PostTools.vue"
|
||||
import RealmTools from "@/components/realms/RealmTools.vue"
|
||||
import { useEditor } from "@/stores/editor"
|
||||
import { useUserinfo } from "@/stores/userinfo"
|
||||
|
||||
const id = useUserinfo()
|
||||
const editor = useEditor()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-fab {
|
||||
position: fixed !important;
|
||||
bottom: 16px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.editor-speed-dial {
|
||||
position: fixed !important;
|
||||
bottom: 16px;
|
||||
right: 20px;
|
||||
}
|
||||
</style>
|
@@ -7,6 +7,10 @@ const router = createRouter({
|
||||
{
|
||||
path: "/",
|
||||
component: MasterLayout,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("@/layouts/plaza.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
@@ -29,6 +33,20 @@ const router = createRouter({
|
||||
path: "/realms/:realmId",
|
||||
name: "realms.page",
|
||||
component: () => import("@/views/realms/page.vue")
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: "/chat/:channel",
|
||||
component: () => import("@/layouts/chat.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "chat.channel",
|
||||
component: () => import("@/views/chat/page.vue"),
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
|
@@ -3,7 +3,8 @@ import { Preferences } from "@capacitor/preferences"
|
||||
|
||||
const serviceMap: { [id: string]: string } = {
|
||||
interactive: "https://co.solsynth.dev",
|
||||
identity: "https://id.solsynth.dev"
|
||||
identity: "https://id.solsynth.dev",
|
||||
messaging: "http://localhost:8447",
|
||||
}
|
||||
|
||||
export async function request(service: string, input: string, init?: RequestInit, noRetry?: boolean) {
|
||||
|
68
src/stores/channels.ts
Normal file
68
src/stores/channels.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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,7 +31,5 @@ export const useRealms = defineStore("realms", () => {
|
||||
}
|
||||
}
|
||||
|
||||
list().then(() => console.log("[STARTUP HOOK] Fetch available realm successes."))
|
||||
|
||||
return { done, show, related: related_to, available, list }
|
||||
})
|
||||
|
@@ -2,6 +2,8 @@ import { defineStore } from "pinia"
|
||||
import { ref } from "vue"
|
||||
import { request } from "@/scripts/request"
|
||||
import { Preferences } from "@capacitor/preferences"
|
||||
import { useRealms } from "@/stores/realms"
|
||||
import { useChannels } from "@/stores/channels"
|
||||
|
||||
export interface Userinfo {
|
||||
isReady: boolean
|
||||
@@ -30,6 +32,10 @@ export async function checkLoggedIn(): Promise<boolean> {
|
||||
}
|
||||
|
||||
export const useUserinfo = defineStore("userinfo", () => {
|
||||
const userinfoHooks = {
|
||||
after: [useRealms().list, useChannels().list, useChannels().connect]
|
||||
}
|
||||
|
||||
const userinfo = ref(defaultUserinfo)
|
||||
const isReady = ref(false)
|
||||
|
||||
@@ -54,6 +60,8 @@ export const useUserinfo = defineStore("userinfo", () => {
|
||||
displayName: data["nick"],
|
||||
data: data
|
||||
}
|
||||
|
||||
userinfoHooks.after.forEach((call) => call())
|
||||
}
|
||||
|
||||
return { userinfo, isReady, readProfiles }
|
||||
|
84
src/views/chat/page.vue
Normal file
84
src/views/chat/page.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<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>
|
@@ -16,10 +16,19 @@ export default defineConfig({
|
||||
registerType: "autoUpdate",
|
||||
useCredentials: true,
|
||||
manifest: {
|
||||
name: "Solian",
|
||||
name: "Solar Network",
|
||||
short_name: "Solian",
|
||||
description: "The Solar Network entrypoint.",
|
||||
theme_color: "#4b5094",
|
||||
display: "standalone",
|
||||
icons: [
|
||||
{
|
||||
src: "icon.png",
|
||||
sizes: "1024x1024",
|
||||
type: "image/png",
|
||||
purpose: "maskable"
|
||||
},
|
||||
]
|
||||
},
|
||||
})
|
||||
],
|
||||
|
Reference in New Issue
Block a user