✨ Basic chat layouts
This commit is contained in:
parent
05e8782557
commit
8bb9816cd0
21
src/components/chat/ChatList.vue
Normal file
21
src/components/chat/ChatList.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<v-infinite-scroll
|
||||
side="start"
|
||||
class="mt-[-16px]"
|
||||
@load="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: 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.avatar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<div class="font-bold text-sm">{{ props.item?.sender.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"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="cursor-zoom-in content-visibility-auto"
|
||||
class="cursor-zoom-in content-visibility-auto w-full h-full object-contain"
|
||||
:src="getUrl(item)"
|
||||
:alt="item.filename"
|
||||
@click="openLightbox(item, idx)"
|
||||
|
@ -3,12 +3,14 @@
|
||||
<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">{{ metadata?.name }}</h2>
|
||||
<h2 class="ml-2 text-lg font-500">{{ channels.current?.name }}</h2>
|
||||
|
||||
<p class="ml-3 text-xs opacity-80">{{ metadata?.description }}</p>
|
||||
<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>
|
||||
@ -16,15 +18,15 @@
|
||||
<script setup lang="ts">
|
||||
import { request } from "@/scripts/request"
|
||||
import { useRoute } from "vue-router"
|
||||
import { ref } from "vue"
|
||||
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)
|
||||
|
||||
const metadata = ref<any>(null)
|
||||
|
||||
async function readMetadata() {
|
||||
loading.value = true
|
||||
const res = await request("messaging", `/api/channels/${route.params.channel}`)
|
||||
@ -32,10 +34,17 @@ async function readMetadata() {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
error.value = null
|
||||
metadata.value = await res.json()
|
||||
channels.current = await res.json()
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.params.channel,
|
||||
() => {
|
||||
channels.messages = []
|
||||
readMetadata()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
@ -38,12 +38,13 @@ const router = createRouter({
|
||||
},
|
||||
|
||||
{
|
||||
path: "/chat",
|
||||
path: "/chat/:channel",
|
||||
component: () => import("@/layouts/chat.vue"),
|
||||
children: [
|
||||
{
|
||||
path: ":channel",
|
||||
path: "",
|
||||
name: "chat.channel",
|
||||
component: () => import("@/views/chat/channel.vue")
|
||||
component: () => import("@/views/chat/page.vue"),
|
||||
}
|
||||
]
|
||||
},
|
||||
|
37
src/stores/channels.ts
Normal file
37
src/stores/channels.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { reactive, ref } from "vue"
|
||||
import { checkLoggedIn, getAtk } from "@/stores/userinfo"
|
||||
import { request } from "@/scripts/request"
|
||||
|
||||
export const useChannels = defineStore("channels", () => {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
return { done, show, related_to, available, current, messages, list }
|
||||
})
|
@ -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]
|
||||
}
|
||||
|
||||
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 }
|
||||
|
85
src/views/chat/page.vue
Normal file
85
src/views/chat/page.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<v-container fluid class="px-6">
|
||||
<chat-list :loader="readMore" :messages="channels.messages" />
|
||||
</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>
|
||||
</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 ChatList from "@/components/chat/ChatList.vue"
|
||||
|
||||
const channels = useChannels()
|
||||
|
||||
const error = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function readHistory() {
|
||||
loading.value = true
|
||||
const res = await request(
|
||||
"messaging",
|
||||
`/api/channels/${channels.current.alias}/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 = data["data"]
|
||||
error.value = null
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function readMore({ done }: any) {
|
||||
// Reach the end of data
|
||||
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 })
|
||||
</script>
|
Reference in New Issue
Block a user