✨ Voice Chat yo!
This commit is contained in:
		| @@ -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 }) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | 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> | </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