✨ 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="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="apple-touch-icon" type="image/png" href="/apple-touch-icon.png" sizes="1024x1024">
|
<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" />
|
<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>
|
<title>Solian</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-form class="flex-grow-1" ref="chat" @submit.prevent="sendMessage">
|
<v-form ref="chat" @submit.prevent="sendMessage">
|
||||||
<v-expand-transition>
|
<v-expand-transition>
|
||||||
<v-alert
|
<v-alert
|
||||||
v-show="channels.related?.messages?.reply_to"
|
v-show="channels.related?.messages?.reply_to"
|
||||||
class="mb-3 text-sm"
|
class="mb-2 text-sm"
|
||||||
variant="tonal"
|
variant="elevated"
|
||||||
density="compact"
|
density="compact"
|
||||||
type="info"
|
type="info"
|
||||||
>
|
>
|
||||||
@ -20,7 +20,7 @@
|
|||||||
<v-btn
|
<v-btn
|
||||||
icon="mdi-close"
|
icon="mdi-close"
|
||||||
size="x-small"
|
size="x-small"
|
||||||
color="info"
|
color="white"
|
||||||
variant="text"
|
variant="text"
|
||||||
@click="channels.related.messages.reply_to = null"
|
@click="channels.related.messages.reply_to = null"
|
||||||
/>
|
/>
|
||||||
@ -31,8 +31,8 @@
|
|||||||
<v-expand-transition>
|
<v-expand-transition>
|
||||||
<v-alert
|
<v-alert
|
||||||
v-show="channels.related?.messages?.edit_to"
|
v-show="channels.related?.messages?.edit_to"
|
||||||
class="mb-3 text-sm"
|
class="mb-2 text-sm"
|
||||||
variant="tonal"
|
variant="elevated"
|
||||||
density="compact"
|
density="compact"
|
||||||
type="info"
|
type="info"
|
||||||
>
|
>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<v-btn
|
<v-btn
|
||||||
icon="mdi-close"
|
icon="mdi-close"
|
||||||
size="x-small"
|
size="x-small"
|
||||||
color="info"
|
color="white"
|
||||||
variant="text"
|
variant="text"
|
||||||
@click="channels.related.messages.edit_to = null"
|
@click="channels.related.messages.edit_to = null"
|
||||||
/>
|
/>
|
||||||
@ -60,8 +60,8 @@
|
|||||||
auto-grow
|
auto-grow
|
||||||
hide-details
|
hide-details
|
||||||
class="w-full"
|
class="w-full"
|
||||||
variant="outlined"
|
variant="solo"
|
||||||
density="compact"
|
density="comfortable"
|
||||||
placeholder="Enter some messages..."
|
placeholder="Enter some messages..."
|
||||||
:rows="1"
|
:rows="1"
|
||||||
:max-rows="6"
|
:max-rows="6"
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
<v-list density="compact" lines="one">
|
<v-list density="compact" lines="one">
|
||||||
<v-list-item disabled append-icon="mdi-flag" title="Report" />
|
<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-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-delete" title="Delete" @click="deleteChannel" />
|
||||||
|
<v-list-item v-if="isOwned" append-icon="mdi-account-supervisor-circle" title="Members" @click="manageChannel" />
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</template>
|
</template>
|
||||||
|
@ -9,6 +9,23 @@
|
|||||||
<v-spacer />
|
<v-spacer />
|
||||||
|
|
||||||
<div v-if="channels.current">
|
<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" />
|
<channel-action :item="channels.current" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -24,6 +41,7 @@ import { onMounted, ref, watch } from "vue"
|
|||||||
import { useChannels } from "@/stores/channels"
|
import { useChannels } from "@/stores/channels"
|
||||||
import ChannelAction from "@/components/chat/channels/ChannelAction.vue"
|
import ChannelAction from "@/components/chat/channels/ChannelAction.vue"
|
||||||
import { useUI } from "@/stores/ui"
|
import { useUI } from "@/stores/ui"
|
||||||
|
import { getAtk } from "@/stores/userinfo"
|
||||||
|
|
||||||
const { showErrorSnackbar } = useUI()
|
const { showErrorSnackbar } = useUI()
|
||||||
|
|
||||||
@ -31,6 +49,7 @@ const route = useRoute()
|
|||||||
const channels = useChannels()
|
const channels = useChannels()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const calling = ref(false)
|
||||||
|
|
||||||
async function readMetadata() {
|
async function readMetadata() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@ -43,6 +62,30 @@ async function readMetadata() {
|
|||||||
loading.value = false
|
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(
|
watch(
|
||||||
() => route.params.channel,
|
() => route.params.channel,
|
||||||
(val) => {
|
(val) => {
|
||||||
|
@ -35,13 +35,16 @@ export const useChannels = defineStore("channels", () => {
|
|||||||
|
|
||||||
const available = ref<any[]>([])
|
const available = ref<any[]>([])
|
||||||
const current = ref<any>(null)
|
const current = ref<any>(null)
|
||||||
|
|
||||||
const messages = ref<any[]>([])
|
const messages = ref<any[]>([])
|
||||||
|
const call = ref<any>(null)
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
watch(
|
watch(
|
||||||
() => route.params.channel,
|
() => route.params.channel,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
|
call.value = null
|
||||||
messages.value = []
|
messages.value = []
|
||||||
current.value = null
|
current.value = null
|
||||||
}
|
}
|
||||||
@ -64,6 +67,22 @@ export const useChannels = defineStore("channels", () => {
|
|||||||
|
|
||||||
const ui = useUI()
|
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() {
|
async function connect() {
|
||||||
if (!(await checkLoggedIn())) return
|
if (!(await checkLoggedIn())) return
|
||||||
|
|
||||||
@ -112,6 +131,13 @@ export const useChannels = defineStore("channels", () => {
|
|||||||
return x.id !== payload.id
|
return x.id !== payload.id
|
||||||
})
|
})
|
||||||
break
|
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()
|
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
|
<v-footer
|
||||||
app
|
app
|
||||||
class="flex items-center border-opacity-15 min-h-[64px]"
|
class="footer-section flex-col gap-2 min-h-[64px]"
|
||||||
style="border-top-width: thin"
|
:style="`padding-bottom: max(${safeAreaBottom}, 12px)`"
|
||||||
:style="`padding-bottom: max(${safeAreaBottom}, 8px)`"
|
|
||||||
>
|
>
|
||||||
<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>
|
</v-footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -39,6 +57,20 @@ const loading = ref(false)
|
|||||||
|
|
||||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
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() {
|
async function readHistory() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await request(
|
const res = await request(
|
||||||
@ -86,9 +118,11 @@ watch(
|
|||||||
() => channels.current,
|
() => channels.current,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
|
unmountJitsi()
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
pagination.total = 0
|
pagination.total = 0
|
||||||
readHistory()
|
readHistory()
|
||||||
|
readCall()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@ -97,4 +131,61 @@ watch(
|
|||||||
function scrollTop() {
|
function scrollTop() {
|
||||||
window.scroll({ top: 0 })
|
window.scroll({ top: 0 })
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
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