Friends

This commit is contained in:
LittleSheep 2024-04-06 02:08:57 +08:00
parent cbcb007517
commit 2d3f8a8bd7
8 changed files with 252 additions and 9 deletions

View File

@ -0,0 +1,62 @@
<template>
<v-list-item :title="otherside.nick">
<template #subtitle>@{{ otherside.name }}</template>
<template #prepend>
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card me-2"
size="small"
:image="othersidePicture"
/>
</template>
<template #append>
<v-btn
icon="mdi-check"
size="x-small"
color="success"
variant="text"
:disabled="!canApprove"
@click="emits('decline')"
/>
<v-btn
icon="mdi-close"
size="x-small"
color="error"
variant="text"
:disabled="!canDecline"
@click="emits('approve')"
/>
</template>
</v-list-item>
</template>
<script setup lang="ts">
import { computed } from "vue"
import { useUserinfo } from "@/stores/userinfo"
import { buildRequestUrl } from "@/scripts/request"
const id = useUserinfo()
const props = defineProps<{ item: any }>()
const emits = defineEmits(["approve", "decline"])
const canApprove = computed(() => {
return props.item.status !== 1 && props.item.account_id !== id.userinfo.data?.id
})
const canDecline = computed(() => {
return props.item.status !== 2
})
const otherside = computed(() => {
if (props.item.account_id === id.userinfo.data?.id) {
return props.item.related
} else {
return props.item.account
}
})
const othersidePicture = computed(() => otherside.value?.avatar ?
buildRequestUrl("identity", `/api/avatar/${otherside.value?.avatar}`) :
undefined
)
</script>

View File

@ -6,8 +6,15 @@
<div class="aside-nav max-md:order-first"> <div class="aside-nav max-md:order-first">
<v-card prepend-icon="mdi-cog" title="Settings"> <v-card prepend-icon="mdi-cog" title="Settings">
<v-list density="comfortable"> <v-list density="comfortable" class="overflow-auto">
<v-list-item title="Basis" prepend-icon="mdi-network" exact :to="{ name: 'settings' }" /> <v-list-item title="Basis" prepend-icon="mdi-network" exact :to="{ name: 'settings' }" />
<v-divider class="border-[#000] my-2" />
<v-list-item title="Friends" prepend-icon="mdi-handshake" :to="{ name: 'settings.account.friends' }" />
<v-divider class="border-[#000] my-2" />
<v-list-item title="Personalize" prepend-icon="mdi-card-bulleted-outline" :to="{ name: 'settings.account.personalize' }" /> <v-list-item title="Personalize" prepend-icon="mdi-card-bulleted-outline" :to="{ name: 'settings.account.personalize' }" />
<v-list-item title="Personal Page" prepend-icon="mdi-sitemap" :to="{ name: 'settings.account.personal-page' }" /> <v-list-item title="Personal Page" prepend-icon="mdi-sitemap" :to="{ name: 'settings.account.personal-page' }" />
<v-list-item title="Security" prepend-icon="mdi-security" :to="{ name: 'settings.account.security' }" /> <v-list-item title="Security" prepend-icon="mdi-security" :to="{ name: 'settings.account.security' }" />

View File

@ -5,6 +5,12 @@ export const settingRouter = [
component: () => import("@/views/settings.vue") component: () => import("@/views/settings.vue")
}, },
{
path: "account/friends",
name: "settings.account.friends",
component: () => import("@/views/users/me/friends.vue")
},
{ {
path: "account/personalize", path: "account/personalize",
name: "settings.account.personalize", name: "settings.account.personalize",

23
src/stores/friends.ts Normal file
View File

@ -0,0 +1,23 @@
import { reactive, ref } from "vue"
import { defineStore } from "pinia"
import { checkLoggedIn, getAtk } from "@/stores/userinfo"
import { request } from "@/scripts/request"
export const useFriends = defineStore("friends", () => {
const available = ref<any[]>([])
async function list() {
if (!(await checkLoggedIn())) return
const res = await request("identity", "/api/users/me/friends?status=1", {
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
throw new Error(await res.text())
} else {
available.value = await res.json()
}
}
return { available, list }
})

View File

@ -4,6 +4,7 @@ import { request } from "@/scripts/request"
import { Preferences } from "@capacitor/preferences" import { Preferences } from "@capacitor/preferences"
import { useRealms } from "@/stores/realms" import { useRealms } from "@/stores/realms"
import { useChannels } from "@/stores/channels" import { useChannels } from "@/stores/channels"
import { useFriends } from "@/stores/friends"
export interface Userinfo { export interface Userinfo {
isReady: boolean isReady: boolean
@ -51,7 +52,7 @@ export async function signout() {
export const useUserinfo = defineStore("userinfo", () => { export const useUserinfo = defineStore("userinfo", () => {
const userinfoHooks = { const userinfoHooks = {
after: [useRealms().list, useChannels().list, useChannels().connect] after: [useRealms().list, useChannels().list, useChannels().connect, useFriends().list]
} }
const userinfo = ref(defaultUserinfo) const userinfo = ref(defaultUserinfo)

View File

@ -0,0 +1,144 @@
<template>
<div class="flex flex-col gap-3">
<v-card prepend-icon="mdi-account-plus" title="Add a new friend" :loading="submitting">
<v-form @submit.prevent="sendRequest">
<div class="pl-4.5 pr-5.5 pt-0.5 pb-4">
<v-text-field
label="Username or email"
variant="outlined"
density="comfortable"
hint="Username not nickname. You need wait your friend accept your request to establish friend relationship."
append-icon="mdi-plus"
persistent-hint
clearable
v-model="username"
@click:append="sendRequest"
/>
</div>
</v-form>
</v-card>
<v-card prepend-icon="mdi-account-multiple" title="Friend list" :loading="loading">
<v-list>
<v-list-subheader>FRIENDS ({{ friends.available.length }})</v-list-subheader>
<friend-list-item
v-for="item in friends.available"
:item="item"
@decline="upgradeFriendship(item, 2)"
/>
<v-list-subheader>PENDING ({{ pending.length }})</v-list-subheader>
<friend-list-item
v-for="item in pending"
:item="item"
@approve="upgradeFriendship(item, 1)"
@decline="upgradeFriendship(item, 2)"
/>
<v-list-subheader>BLOCKED ({{ blocked.length }})</v-list-subheader>
<friend-list-item
v-for="item in blocked"
:item="item"
@approve="upgradeFriendship(item, 1)"
/>
</v-list>
</v-card>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { checkLoggedIn, getAtk, useUserinfo } from "@/stores/userinfo"
import { request } from "@/scripts/request"
import { useFriends } from "@/stores/friends"
import { useUI } from "@/stores/ui"
import FriendListItem from "@/components/friends/FriendListItem.vue"
const { showSnackbar, showErrorSnackbar } = useUI()
const id = useUserinfo()
const submitting = ref(false)
const loading = ref(false)
const username = ref("")
const friends = useFriends()
const pending = ref<any[]>([])
const blocked = ref<any[]>([])
async function sendRequest() {
if (submitting.value) return
submitting.value = true
const res = await request("identity", "/api/users/me/friends?" + new URLSearchParams({
related: username.value
}), {
method: "POST",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
showSnackbar("You have sent the friend invitation, go reach your friend out.")
await Promise.all([readFriendships(), friends.list()])
}
submitting.value = false
}
async function upgradeFriendship(item: any, status: number) {
if (submitting.value) return
let otherside: any;
if (item.account_id === id.userinfo.data?.id) {
otherside = item.related
} else {
otherside = item.account
}
submitting.value = true
const res = await request("identity", `/api/users/me/friends/${otherside.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
body: JSON.stringify({
status
})
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
showSnackbar("Friendship status has been updated.")
await Promise.all([readFriendships(), friends.list()])
}
submitting.value = false
}
async function readFriendships() {
if (!(await checkLoggedIn())) return
let res: Response
loading.value = true
res = await request("identity", "/api/users/me/friends?status=0", {
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
pending.value = await res.json()
}
res = await request("identity", "/api/users/me/friends?status=2", {
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
blocked.value = await res.json()
}
loading.value = false
}
readFriendships()
</script>

View File

@ -45,7 +45,7 @@
icon="mdi-account-circle" icon="mdi-account-circle"
class="rounded-card" class="rounded-card"
size="large" size="large"
:image="accountPicture ?? ''" :image="accountPicture"
/> />
<v-file-input <v-file-input
clearable clearable
@ -64,7 +64,7 @@
class="bg-grey-lighten-2" class="bg-grey-lighten-2"
max-height="280px" max-height="280px"
:aspect-ratio="16 / 9" :aspect-ratio="16 / 9"
:src="accountBanner ?? ''" :src="accountBanner"
/> />
<v-card-text> <v-card-text>
@ -191,11 +191,11 @@ function loadImage(event: InputEvent, type: string) {
const accountPicture = computed(() => id.userinfo.data?.avatar ? const accountPicture = computed(() => id.userinfo.data?.avatar ?
buildRequestUrl("identity", `/api/avatar/${id.userinfo.data?.avatar}`) : buildRequestUrl("identity", `/api/avatar/${id.userinfo.data?.avatar}`) :
null undefined
) )
const accountBanner = computed(() => id.userinfo.data?.banner ? const accountBanner = computed(() => id.userinfo.data?.banner ?
buildRequestUrl("identity", `/api/avatar/${id.userinfo.data?.banner}`) : buildRequestUrl("identity", `/api/avatar/${id.userinfo.data?.banner}`) :
null undefined
) )
onUnmounted(() => { onUnmounted(() => {

View File

@ -9,7 +9,7 @@
color="grey-lighten-2" color="grey-lighten-2"
icon="mdi-account-circle" icon="mdi-account-circle"
class="rounded-card me-2" class="rounded-card me-2"
:image="accountPicture ?? ''" :image="accountPicture"
/> />
<div> <div>
@ -100,11 +100,11 @@ const posts = ref<any[]>([])
const accountPicture = computed(() => metadata.value?.avatar ? const accountPicture = computed(() => metadata.value?.avatar ?
buildRequestUrl("identity", `/api/avatar/${metadata.value?.avatar}`) : buildRequestUrl("identity", `/api/avatar/${metadata.value?.avatar}`) :
null undefined
) )
const accountBanner = computed(() => metadata.value?.banner ? const accountBanner = computed(() => metadata.value?.banner ?
buildRequestUrl("identity", `/api/avatar/${metadata.value?.banner}`) : buildRequestUrl("identity", `/api/avatar/${metadata.value?.banner}`) :
null undefined
) )
async function readMetadata() { async function readMetadata() {