✨ Basic chat layouts
This commit is contained in:
		
							
								
								
									
										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"
 | 
					            v-if="item.type === 1"
 | 
				
			||||||
            loading="lazy"
 | 
					            loading="lazy"
 | 
				
			||||||
            decoding="async"
 | 
					            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)"
 | 
					            :src="getUrl(item)"
 | 
				
			||||||
            :alt="item.filename"
 | 
					            :alt="item.filename"
 | 
				
			||||||
            @click="openLightbox(item, idx)"
 | 
					            @click="openLightbox(item, idx)"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,12 +3,14 @@
 | 
				
			|||||||
    <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">{{ 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>
 | 
					    </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>
 | 
				
			||||||
@@ -16,15 +18,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 } from "vue"
 | 
					import { ref, watch } 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}`)
 | 
				
			||||||
@@ -32,10 +34,17 @@ async function readMetadata() {
 | 
				
			|||||||
    error.value = await res.text()
 | 
					    error.value = await res.text()
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    error.value = null
 | 
					    error.value = null
 | 
				
			||||||
    metadata.value = await res.json()
 | 
					    channels.current = await res.json()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  loading.value = false
 | 
					  loading.value = false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
readMetadata()
 | 
					watch(
 | 
				
			||||||
 | 
					  () => route.params.channel,
 | 
				
			||||||
 | 
					  () => {
 | 
				
			||||||
 | 
					    channels.messages = []
 | 
				
			||||||
 | 
					    readMetadata()
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  { immediate: true }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@@ -38,12 +38,13 @@ const router = createRouter({
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          path: "/chat",
 | 
					          path: "/chat/:channel",
 | 
				
			||||||
 | 
					          component: () => import("@/layouts/chat.vue"),
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              path: ":channel",
 | 
					              path: "",
 | 
				
			||||||
              name: "chat.channel",
 | 
					              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 }
 | 
					  return { done, show, related: related_to, available, list }
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,8 @@ 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
 | 
				
			||||||
@@ -30,6 +32,10 @@ export async function checkLoggedIn(): Promise<boolean> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useUserinfo = defineStore("userinfo", () => {
 | 
					export const useUserinfo = defineStore("userinfo", () => {
 | 
				
			||||||
 | 
					  const userinfoHooks = {
 | 
				
			||||||
 | 
					    after: [useRealms().list, useChannels().list]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const userinfo = ref(defaultUserinfo)
 | 
					  const userinfo = ref(defaultUserinfo)
 | 
				
			||||||
  const isReady = ref(false)
 | 
					  const isReady = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,6 +60,8 @@ 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 }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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