✨ Voice Chat yo!
This commit is contained in:
parent
8eb28f0115
commit
76367bbd25
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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) => {
|
||||
|
@ -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 }
|
||||
})
|
@ -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>
|
Reference in New Issue
Block a user