Voice Chat yo!

This commit is contained in:
LittleSheep 2024-04-06 23:10:09 +08:00
parent 8eb28f0115
commit 76367bbd25
6 changed files with 177 additions and 16 deletions

View File

@ -5,6 +5,7 @@
<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" />
<script src="https://meet.solsynth.dev/external_api.js"></script>
<title>Solian</title>
</head>
<body>

View File

@ -1,10 +1,10 @@
<template>
<v-form class="flex-grow-1" ref="chat" @submit.prevent="sendMessage">
<v-form ref="chat" @submit.prevent="sendMessage">
<v-expand-transition>
<v-alert
v-show="channels.related?.messages?.reply_to"
class="mb-3 text-sm"
variant="tonal"
class="mb-2 text-sm"
variant="elevated"
density="compact"
type="info"
>
@ -20,7 +20,7 @@
<v-btn
icon="mdi-close"
size="x-small"
color="info"
color="white"
variant="text"
@click="channels.related.messages.reply_to = null"
/>
@ -31,8 +31,8 @@
<v-expand-transition>
<v-alert
v-show="channels.related?.messages?.edit_to"
class="mb-3 text-sm"
variant="tonal"
class="mb-2 text-sm"
variant="elevated"
density="compact"
type="info"
>
@ -48,7 +48,7 @@
<v-btn
icon="mdi-close"
size="x-small"
color="info"
color="white"
variant="text"
@click="channels.related.messages.edit_to = null"
/>
@ -60,8 +60,8 @@
auto-grow
hide-details
class="w-full"
variant="outlined"
density="compact"
variant="solo"
density="comfortable"
placeholder="Enter some messages..."
:rows="1"
:max-rows="6"

View File

@ -7,8 +7,8 @@
<v-list density="compact" lines="one">
<v-list-item disabled append-icon="mdi-flag" title="Report" />
<v-list-item v-if="isOwned" append-icon="mdi-pencil" title="Edit" @click="editChannel" />
<v-list-item v-if="isOwned" append-icon="mdi-account-supervisor-circle" title="Members" @click="manageChannel" />
<v-list-item v-if="isOwned" append-icon="mdi-delete" title="Delete" @click="deleteChannel" />
<v-list-item v-if="isOwned" append-icon="mdi-account-supervisor-circle" title="Members" @click="manageChannel" />
</v-list>
</v-menu>
</template>

View File

@ -9,6 +9,23 @@
<v-spacer />
<div v-if="channels.current">
<v-btn
v-if="channels.call"
icon="mdi-phone-hangup"
size="small"
variant="text"
:loading="calling"
@click="endsCall"
/>
<v-btn
v-else
icon="mdi-phone-plus"
size="small"
variant="text"
:loading="calling"
@click="makeCall"
/>
<channel-action :item="channels.current" />
</div>
</div>
@ -24,6 +41,7 @@ import { onMounted, ref, watch } from "vue"
import { useChannels } from "@/stores/channels"
import ChannelAction from "@/components/chat/channels/ChannelAction.vue"
import { useUI } from "@/stores/ui"
import { getAtk } from "@/stores/userinfo"
const { showErrorSnackbar } = useUI()
@ -31,6 +49,7 @@ const route = useRoute()
const channels = useChannels()
const loading = ref(false)
const calling = ref(false)
async function readMetadata() {
loading.value = true
@ -43,6 +62,30 @@ async function readMetadata() {
loading.value = false
}
async function makeCall() {
calling.value = true
const res = await request("messaging", `/api/channels/${route.params.channel}/calls`, {
method: "POST",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
}
calling.value = false
}
async function endsCall() {
calling.value = true
const res = await request("messaging", `/api/channels/${route.params.channel}/calls/ongoing`, {
method: "DELETE",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
}
calling.value = false
}
watch(
() => route.params.channel,
(val) => {

View File

@ -35,13 +35,16 @@ export const useChannels = defineStore("channels", () => {
const available = ref<any[]>([])
const current = ref<any>(null)
const messages = ref<any[]>([])
const call = ref<any>(null)
const route = useRoute()
watch(
() => route.params.channel,
(val) => {
if (!val) {
call.value = null
messages.value = []
current.value = null
}
@ -64,6 +67,22 @@ export const useChannels = defineStore("channels", () => {
const ui = useUI()
async function exchangeCallToken() {
if (!(await checkLoggedIn())) return
if (!current.value) return
const res = await request("messaging", `/api/channels/${current.value.alias}/calls/ongoing/token`, {
method: "POST",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
ui.showErrorSnackbar(`unable to exchange call token: ${await res.text()}`)
return null
} else {
return await res.json()
}
}
async function connect() {
if (!(await checkLoggedIn())) return
@ -112,6 +131,13 @@ export const useChannels = defineStore("channels", () => {
return x.id !== payload.id
})
break
case "calls.new":
call.value = payload
break
case "calls.end":
call.value = null
break
}
}
})
@ -121,5 +147,5 @@ export const useChannels = defineStore("channels", () => {
socket.close()
}
return { done, show, related, available, current, messages, list, connect, disconnect }
return { done, show, related, available, current, messages, call, list, exchangeCallToken, connect, disconnect }
})

View File

@ -7,11 +7,29 @@
<v-footer
app
class="flex items-center border-opacity-15 min-h-[64px]"
style="border-top-width: thin"
:style="`padding-bottom: max(${safeAreaBottom}, 8px)`"
class="footer-section flex-col gap-2 min-h-[64px]"
:style="`padding-bottom: max(${safeAreaBottom}, 12px)`"
>
<chat-editor class="flex-grow-1" @sent="scrollTop" />
<v-expand-transition>
<v-expansion-panels v-show="channels.call">
<v-expansion-panel
eager
icon="mdi-phone"
title="Call is ongoing"
elevation="1"
class="call-expansion"
@group:selected="(val) => val && mountJitsi()"
>
<template #text>
<div class="call-container w-full h-[380px]">
<div id="call" class="h-full w-full" v-if="channels.call"></div>
</div>
</template>
</v-expansion-panel>
</v-expansion-panels>
</v-expand-transition>
<chat-editor class="w-full" @sent="scrollTop" />
</v-footer>
</template>
@ -39,6 +57,20 @@ const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
async function readCall() {
loading.value = true
const res = await request(
"messaging",
`/api/channels/${route.params.channel}/calls/ongoing`
)
if (res.status !== 200 && res.status !== 404) {
showErrorSnackbar(await res.text())
} else {
channels.call = await res.json()
}
loading.value = false
}
async function readHistory() {
loading.value = true
const res = await request(
@ -86,9 +118,11 @@ watch(
() => channels.current,
(val) => {
if (val) {
unmountJitsi()
pagination.page = 1
pagination.total = 0
readHistory()
readCall()
}
},
{ immediate: true }
@ -97,4 +131,61 @@ watch(
function scrollTop() {
window.scroll({ top: 0 })
}
watch(
() => channels.call,
(val) => {
if (!val) {
unmountJitsi()
}
}
)
let mounted = false
let jitsiInstance: any
async function mountJitsi() {
if (mounted) return false
if (!channels.call) return
const tk = await channels.exchangeCallToken()
console.log(tk)
if (!tk) return
const domain = tk.endpoint.replace("http://", "").replace("https://", "")
const options = {
roomName: channels.call.external_id,
parentNode: document.querySelector("#call"),
jwt: tk.token
}
// This class imported by the script tag in index.html
// @ts-ignore
jitsiInstance = new JitsiMeetExternalAPI(domain, options)
mounted = true
}
function unmountJitsi() {
mounted = false
if (jitsiInstance) {
jitsiInstance.dispose()
jitsiInstance = null
}
}
onUnmounted(() => unmountJitsi())
</script>
<style scoped>
.footer-section {
background: rgba(255, 255, 255, 0.75);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.3);
padding-top: 12px !important;
}
</style>
<style>
.call-expansion .v-expansion-panel-text__wrapper {
padding: 0 !important;
}
</style>