mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-09 19:27:44 +02:00
Office: refactor meeting creation (#9905)
Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
@@ -719,6 +719,12 @@ export function createModel (builder: Builder): void {
|
||||
love.completion.MeetingMinutesCategory
|
||||
)
|
||||
|
||||
// Extensions
|
||||
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
|
||||
extension: contact.extension.EmployeePopupActions,
|
||||
component: love.component.InviteEmployeeButton
|
||||
})
|
||||
|
||||
createAttributePresenter(
|
||||
builder,
|
||||
view.component.DateTimePresenter,
|
||||
|
||||
@@ -32,7 +32,8 @@ export default mergeIds(loveId, love, {
|
||||
WidgetSwitcher: '' as AnyComponent,
|
||||
RoomLanguageEditor: '' as AnyComponent,
|
||||
MediaPopupItemExt: '' as AnyComponent,
|
||||
SharingStateIndicator: '' as AnyComponent
|
||||
SharingStateIndicator: '' as AnyComponent,
|
||||
InviteEmployeeButton: '' as AnyComponent
|
||||
},
|
||||
app: {
|
||||
Love: '' as Ref<Doc>
|
||||
|
||||
@@ -44,12 +44,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModernButton
|
||||
label={type === 'type-button-icon' ? undefined : chunter.string.Message}
|
||||
icon={icon ?? view.icon.Bubble}
|
||||
size="small"
|
||||
iconSize="small"
|
||||
{type}
|
||||
{kind}
|
||||
on:click={openDirect}
|
||||
/>
|
||||
<div class="button-container">
|
||||
<ModernButton
|
||||
label={type === 'type-button-icon' ? undefined : chunter.string.Message}
|
||||
icon={icon ?? view.icon.Bubble}
|
||||
size="small"
|
||||
iconSize="small"
|
||||
{type}
|
||||
{kind}
|
||||
on:click={openDirect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.button-container {
|
||||
border-radius: var(--small-BorderRadius);
|
||||
display: flex;
|
||||
background-color: var(--theme-button-container-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -133,12 +133,10 @@
|
||||
<div slot="actions">
|
||||
{#if !disabled}
|
||||
<div class="flex-presenter flex-gap-2 flex-center">
|
||||
<div class="button-container">
|
||||
<ComponentExtensions
|
||||
extension={contact.extension.EmployeePopupActions}
|
||||
props={{ employee, icon: contact.icon.Chat, type: 'type-button-icon' }}
|
||||
/>
|
||||
</div>
|
||||
<ComponentExtensions
|
||||
extension={contact.extension.EmployeePopupActions}
|
||||
props={{ employee, icon: contact.icon.Chat, type: 'type-button-icon', class: 'button-container' }}
|
||||
/>
|
||||
<div class="button-container">
|
||||
<ButtonIcon icon={contact.icon.User} size="small" iconSize="small" on:click={viewProfile} />
|
||||
</div>
|
||||
|
||||
@@ -82,10 +82,8 @@
|
||||
<symbol id="stopRecord" viewBox="0 0 256 256">
|
||||
<path d="M128,24A104,104,0,1,0,232,128,104.11791,104.11791,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.09957,88.09957,0,0,1,128,216Zm72-88a72,72,0,1,1-72-72A72.08124,72.08124,0,0,1,200,128Z" fill="#CB4B42"/>
|
||||
</symbol>
|
||||
<symbol id="invite" viewBox="0 0 32 32">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 18C20.4183 18 24 14.4183 24 10C24 5.58172 20.4183 2 16 2C11.5817 2 8 5.58172 8 10C8 14.4183 11.5817 18 16 18ZM16 16C19.3137 16 22 13.3137 22 10C22 6.68629 19.3137 4 16 4C12.6863 4 10 6.68629 10 10C10 13.3137 12.6863 16 16 16Z" />
|
||||
<path d="M24 21C24 20.4477 24.4477 20 25 20C25.5523 20 26 20.4477 26 21V24H29C29.5523 24 30 24.4477 30 25C30 25.5523 29.5523 26 29 26H26V29C26 29.5523 25.5523 30 25 30C24.4477 30 24 29.5523 24 29V26H21C20.4477 26 20 25.5523 20 25C20 24.4477 20.4477 24 21 24H24V21Z" />
|
||||
<path d="M13 20C9.13401 20 6 23.134 6 27V29C6 29.5523 6.44772 30 7 30C7.55228 30 8 29.5523 8 29V27C8 24.2386 10.2386 22 13 22H17C17.5523 22 18 21.5523 18 21C18 20.4477 17.5523 20 17 20H13Z" />
|
||||
<symbol id="invite" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M10.9745053,6.25438069 C11.5604671,6.90391332 11.3746817,7.63976469 10.8565778,8.33797195 C10.7337406,8.50350982 10.5921521,8.6666145 10.4211441,8.84634226 C10.3390625,8.93260918 10.2750591,8.99748744 10.141183,9.13149508 C9.83714115,9.43583583 9.58155513,9.69156272 9.37441088,9.89868984 C9.27396046,9.99913195 9.95978257,11.3696024 11.2907766,12.7019048 C12.6210476,14.0334833 13.9914431,14.7197765 14.0923663,14.6187976 L14.8586096,13.852132 C15.2805737,13.4297532 15.5040355,13.2259664 15.8111037,13.0245121 C16.4494656,12.6057102 17.1457524,12.4919023 17.7329975,13.0170075 C19.6503895,14.3885354 20.7354185,15.2301771 21.2669798,15.782495 C22.303783,16.8597835 22.1679037,18.5180455 21.2728679,19.4640525 C20.9625009,19.7920945 20.5689704,20.1858419 20.1041752,20.6339203 C17.2926326,23.4470127 11.3589665,21.7350681 6.81145433,17.1830859 C2.26291105,12.6300716 0.5518801,6.69583839 3.35753082,3.88868121 C3.86122573,3.37707043 4.02729858,3.211082 4.51785466,2.72771931 C5.43117982,1.82778693 7.16594962,1.68687606 8.22050841,2.7286095 C8.77521019,3.27656509 9.65955176,4.41440275 10.9745053,6.25438069 Z M16.2721965,15.266193 L15.5058008,16.0330112 C14.203091,17.336439 11.9845452,16.2253927 9.8770373,14.1158132 C7.76808363,12.0047866 6.65827534,9.78706944 7.96142436,8.48402821 C8.16828995,8.27717972 8.42363443,8.0216945 8.72744369,7.71758662 C8.8500234,7.59488642 8.90609452,7.5380489 8.97339653,7.46731514 C9.06509326,7.37094278 9.1404434,7.28630078 9.20077275,7.211402 C8.03540499,5.58806095 7.24320651,4.57370892 6.8161396,4.15183592 C6.59558525,3.93396391 6.1017247,3.97407893 5.9204189,4.1527261 C5.43686641,4.6291879 5.27792422,4.78804929 4.77626041,5.29755675 C2.9719475,7.10286418 4.35321008,11.8933879 8.22519368,15.7691775 C12.0959638,19.6437524 16.8857659,21.0256764 18.7038097,19.2068681 C19.161375,18.7655298 19.5342402,18.3924591 19.8212354,18.08912 C20.0286173,17.8699279 20.0656783,17.4176384 19.8271235,17.1697684 C19.4297888,16.7569185 18.4570205,15.9984643 16.777362,14.7922626 C16.6549304,14.8908077 16.5044234,15.033738 16.2721965,15.266193 Z M17,5 L17,2 L19,2 L19,5 L22,5 L22,7 L19,7 L19,10 L17,10 L17,7 L14,7 L14,5 L17,5 Z"/>
|
||||
</symbol>
|
||||
<symbol id="kick" viewBox="0 0 32 32">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 10C24 14.4183 20.4183 18 16 18C11.5817 18 8 14.4183 8 10C8 5.58172 11.5817 2 16 2C20.4183 2 24 5.58172 24 10ZM16 16C19.3137 16 22 13.3137 22 10C22 6.68629 19.3137 4 16 4C12.6863 4 10 6.68629 10 10C10 13.3137 12.6863 16 16 16Z" />
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
@@ -64,6 +64,7 @@
|
||||
"@hcengineering/emoji": "^0.6.0",
|
||||
"@hcengineering/emoji-resources": "^0.6.0",
|
||||
"@hcengineering/theme": "^0.6.5",
|
||||
"@hcengineering/account-client": "^0.6.0",
|
||||
"@livekit/krisp-noise-filter": "^0.3.0",
|
||||
"@livekit/track-processors": "^0.5.6",
|
||||
"livekit-client": "^2.15.6",
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
|
||||
import love from '../plugin'
|
||||
import { getRoomName, tryConnect } from '../utils'
|
||||
import { infos, invites, myInfo, myRequests, selectedRoomPlace, myOffice, currentRoom } from '../stores'
|
||||
import { getRoomName } from '../utils'
|
||||
import { infos, myOffice, currentRoom } from '../stores'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
import { createMeeting, joinMeeting } from '../meetings'
|
||||
|
||||
export let object: Room
|
||||
|
||||
@@ -38,21 +39,14 @@
|
||||
dispatch('open', { ignoreKeys: ['name'] })
|
||||
})
|
||||
|
||||
let tryConnecting = false
|
||||
const tryConnecting = false
|
||||
|
||||
async function connect (): Promise<void> {
|
||||
tryConnecting = true
|
||||
const place = $selectedRoomPlace
|
||||
await tryConnect(
|
||||
$myInfo,
|
||||
object,
|
||||
$infos,
|
||||
$myRequests,
|
||||
$invites,
|
||||
place?._id === object._id ? { x: place.x, y: place.y } : undefined
|
||||
)
|
||||
tryConnecting = false
|
||||
selectedRoomPlace.set(undefined)
|
||||
if ($infos.some(({ room }) => room === object._id)) {
|
||||
await joinMeeting(object)
|
||||
} else {
|
||||
await createMeeting(object)
|
||||
}
|
||||
}
|
||||
|
||||
$: connecting = tryConnecting || ($currentRoom?._id === object._id && !$lkSessionConnected)
|
||||
|
||||
@@ -15,22 +15,10 @@
|
||||
<script lang="ts">
|
||||
import { Contact, Person } from '@hcengineering/contact'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import love, { Floor as FloorType, Office, Room, RoomInfo, isOffice } from '@hcengineering/love'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { deviceOptionsStore as deviceInfo, getCurrentLocation, navigate } from '@hcengineering/ui'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import {
|
||||
activeFloor,
|
||||
floors,
|
||||
infos,
|
||||
invites,
|
||||
myInfo,
|
||||
myRequests,
|
||||
rooms,
|
||||
selectedFloor,
|
||||
waitForOfficeLoaded
|
||||
} from '../stores'
|
||||
import { connectToMeeting, tryConnect } from '../utils'
|
||||
import { Floor as FloorType, Office, Room, isOffice } from '@hcengineering/love'
|
||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { activeFloor, floors, rooms, selectedFloor } from '../stores'
|
||||
import Floor from './Floor.svelte'
|
||||
import FloorConfigure from './FloorConfigure.svelte'
|
||||
|
||||
@@ -49,35 +37,6 @@
|
||||
|
||||
$: $deviceInfo.replacedPanel = replacedPanel
|
||||
onDestroy(() => ($deviceInfo.replacedPanel = undefined))
|
||||
|
||||
async function connectToSession (sessionId: string): Promise<void> {
|
||||
const client = getClient()
|
||||
const info = await client.findOne(love.class.RoomInfo, { _id: sessionId as Ref<RoomInfo> })
|
||||
if (info === undefined) return
|
||||
const room = $rooms.find((p) => p._id === info.room)
|
||||
if (room === undefined) return
|
||||
await tryConnect(
|
||||
$myInfo,
|
||||
room,
|
||||
$infos.filter((p) => p.room === room._id),
|
||||
$myRequests,
|
||||
$invites
|
||||
)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const loc = getCurrentLocation()
|
||||
const { sessionId, meetId, ...query } = loc.query ?? {}
|
||||
loc.query = Object.keys(query).length === 0 ? undefined : query
|
||||
navigate(loc, true)
|
||||
if (sessionId) {
|
||||
await waitForOfficeLoaded()
|
||||
await connectToSession(sessionId)
|
||||
} else if (meetId) {
|
||||
await waitForOfficeLoaded()
|
||||
await connectToMeeting($myInfo, $infos, $myRequests, $invites, meetId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="antiPanel-component filledNav" bind:this={replacedPanel}>
|
||||
|
||||
@@ -14,15 +14,8 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
|
||||
import { onDestroy } from 'svelte'
|
||||
import Hall from './Hall.svelte'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import love from '../plugin'
|
||||
import { tryConnect } from '../utils'
|
||||
import { infos, invites, myInfo, myRequests, waitForOfficeLoaded, currentRoom } from '../stores'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
const localNav: boolean = $deviceInfo.navigator.visible
|
||||
const savedNav = localStorage.getItem('love-visibleNav')
|
||||
@@ -32,28 +25,6 @@
|
||||
onDestroy(() => {
|
||||
$deviceInfo.navigator.visible = localNav
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
const wsURL = getMetadata(love.metadata.WebSocketURL)
|
||||
|
||||
if (wsURL === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await waitForOfficeLoaded()
|
||||
const room = $currentRoom
|
||||
|
||||
if (room === undefined) return
|
||||
|
||||
if (
|
||||
!$lkSessionConnected &&
|
||||
$myInfo?.sessionId !== undefined &&
|
||||
$myInfo.sessionId === getMetadata(presentation.metadata.SessionId)
|
||||
) {
|
||||
const info = $infos.filter((p) => p.room === room._id)
|
||||
await tryConnect($myInfo, room, info, $myRequests, $invites)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="hulyPanels-container">
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Avatar, getPersonByPersonRefStore } from '@hcengineering/contact-resources'
|
||||
import { isOffice } from '@hcengineering/love'
|
||||
import { Button } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { currentRoom, infos, myInfo, myOffice, rooms } from '../stores'
|
||||
import { endMeeting, isSharingEnabled, leaveRoom, liveKitClient } from '../utils'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
import { currentRoom, infos, myInfo, myOffice } from '../stores'
|
||||
import { liveKitClient } from '../utils'
|
||||
import { lkSessionConnected, ScreenSharingState, screenSharingState } from '../liveKitClient'
|
||||
import MeetingHeader from './meeting/MeetingHeader.svelte'
|
||||
import { leaveMeeting } from '../meetings'
|
||||
|
||||
export let limit: number = 4
|
||||
|
||||
@@ -20,17 +20,14 @@
|
||||
let leaving = false
|
||||
async function handleLeaveClick (): Promise<void> {
|
||||
leaving = true
|
||||
await leaveRoom($myInfo, $myOffice)
|
||||
await leaveMeeting()
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
let ending = false
|
||||
async function handleEndMeetingClick (): Promise<void> {
|
||||
ending = true
|
||||
const room = $currentRoom
|
||||
if (room !== undefined && isOffice(room) && $myInfo !== undefined) {
|
||||
await endMeeting(room, $rooms, $infos, $myInfo)
|
||||
}
|
||||
await leaveMeeting()
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
@@ -82,7 +79,7 @@
|
||||
{#if allowLeave || leaving}
|
||||
<Button
|
||||
icon={love.icon.LeaveRoom}
|
||||
kind={$isSharingEnabled ? 'regular' : 'dangerous'}
|
||||
kind={$screenSharingState === ScreenSharingState.Local ? 'regular' : 'dangerous'}
|
||||
size={'x-small'}
|
||||
label={view.string.Leave}
|
||||
showTooltip={{ label: love.string.LeaveRoom }}
|
||||
@@ -92,7 +89,7 @@
|
||||
{:else if isMyOffice || ending}
|
||||
<Button
|
||||
icon={love.icon.LeaveRoom}
|
||||
kind={$isSharingEnabled ? 'regular' : 'dangerous'}
|
||||
kind={$screenSharingState === ScreenSharingState.Local ? 'regular' : 'dangerous'}
|
||||
size={'x-small'}
|
||||
label={love.string.EndMeeting}
|
||||
showTooltip={{ label: love.string.EndMeeting }}
|
||||
@@ -100,7 +97,7 @@
|
||||
on:click={handleEndMeetingClick}
|
||||
/>
|
||||
{/if}
|
||||
{#if $isSharingEnabled}
|
||||
{#if $screenSharingState === ScreenSharingState.Local}
|
||||
<Button
|
||||
icon={love.icon.SharingEnabled}
|
||||
kind="dangerous"
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
import { isOffice, Room, RoomAccess } from '@hcengineering/love'
|
||||
import { ActionIcon } from '@hcengineering/ui'
|
||||
import love from '../plugin'
|
||||
import { infos, invites, myInfo, myRequests, rooms } from '../stores'
|
||||
import { invite, kick, tryConnect } from '../utils'
|
||||
import { myInfo } from '../stores'
|
||||
import { joinMeeting, kick, sendInvites } from '../meetings'
|
||||
|
||||
export let room: Room
|
||||
export let person: Ref<Person>
|
||||
|
||||
$: isMyOffice = isOffice(room) && room.person === $myInfo?.person
|
||||
|
||||
$: info = $infos.filter((p) => p.room === room._id)
|
||||
</script>
|
||||
|
||||
<div class="p-3 flex-gap-2 antiPopup">
|
||||
@@ -22,7 +20,7 @@
|
||||
label={love.string.Kick}
|
||||
icon={love.icon.Kick}
|
||||
action={() => {
|
||||
kick(person, $rooms, $infos)
|
||||
void kick(person)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -32,7 +30,7 @@
|
||||
label={love.string.Invite}
|
||||
icon={love.icon.Invite}
|
||||
action={() => {
|
||||
invite(person, $myInfo?.room)
|
||||
void sendInvites([person])
|
||||
}}
|
||||
/>
|
||||
{#if room.access === RoomAccess.Knock}
|
||||
@@ -41,7 +39,7 @@
|
||||
label={love.string.KnockAction}
|
||||
icon={love.icon.Knock}
|
||||
action={() => {
|
||||
tryConnect($myInfo, room, info, $myRequests, $invites)
|
||||
void joinMeeting(room)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -13,21 +13,19 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import presentation, { ActionContext } from '@hcengineering/presentation'
|
||||
import { ActionContext } from '@hcengineering/presentation'
|
||||
import { Room as TypeRoom } from '@hcengineering/love'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import { Label, Loading, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
import love from '../plugin'
|
||||
import { waitForOfficeLoaded, currentRoom, infos, invites, myInfo, myRequests } from '../stores'
|
||||
import { isFullScreen, lk, tryConnect } from '../utils'
|
||||
import { waitForOfficeLoaded, currentRoom } from '../stores'
|
||||
import { isFullScreen, lk } from '../utils'
|
||||
import ControlBar from './meeting/ControlBar.svelte'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
import ParticipantsListView from './meeting/ParticipantsListView.svelte'
|
||||
import ScreenSharingView from './meeting/ScreenSharingView.svelte'
|
||||
|
||||
export let withVideo: boolean
|
||||
export let canMaximize: boolean = true
|
||||
export let room: TypeRoom
|
||||
|
||||
@@ -39,21 +37,15 @@
|
||||
|
||||
onMount(async () => {
|
||||
loading = true
|
||||
|
||||
const wsURL = getMetadata(love.metadata.WebSocketURL)
|
||||
|
||||
if (wsURL === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
configured = true
|
||||
|
||||
await waitForOfficeLoaded()
|
||||
|
||||
if (!$lkSessionConnected && $myInfo?.sessionId === getMetadata(presentation.metadata.SessionId)) {
|
||||
const info = $infos.filter((p) => p.room === room._id)
|
||||
await tryConnect($myInfo, room, info, $myRequests, $invites)
|
||||
}
|
||||
roomEl && roomEl.addEventListener('fullscreenchange', handleFullScreen)
|
||||
loading = false
|
||||
})
|
||||
@@ -147,16 +139,14 @@
|
||||
<div class="screenContainer">
|
||||
<ScreenSharingView bind:hasActiveTrack={withScreenSharing} />
|
||||
</div>
|
||||
{#if withVideo}
|
||||
<div class="videoGrid" style={withScreenSharing ? '' : gridStyle} class:scroll-m-0={withScreenSharing}>
|
||||
<ParticipantsListView
|
||||
room={room._id}
|
||||
on:participantsCount={(evt) => {
|
||||
updateStyle(evt.detail, withScreenSharing)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="videoGrid" style={withScreenSharing ? '' : gridStyle} class:scroll-m-0={withScreenSharing}>
|
||||
<ParticipantsListView
|
||||
room={room._id}
|
||||
on:participantsCount={(evt) => {
|
||||
updateStyle(evt.detail, withScreenSharing)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if $currentRoom}
|
||||
<ControlBar room={$currentRoom} fullScreen={$isFullScreen} {onFullScreen} {canMaximize} />
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
import { languagesDisplayData } from '../types'
|
||||
import LanguageIcon from './LanguageIcon.svelte'
|
||||
import { updateSessionLanguage } from '../utils'
|
||||
|
||||
export let room: Room
|
||||
export let kind: 'dropdown' | 'icon' = 'dropdown'
|
||||
@@ -44,7 +43,6 @@
|
||||
if (newLang == null) return
|
||||
|
||||
await client.diffUpdate(room, { language: newLang })
|
||||
await updateSessionLanguage(room)
|
||||
}
|
||||
|
||||
function showLanguagesPopup (): void {
|
||||
|
||||
@@ -39,6 +39,6 @@
|
||||
<svelte:fragment slot="title">
|
||||
{$currentRoom.name}
|
||||
</svelte:fragment>
|
||||
<RoomComponent withVideo={$currentRoom.type === RoomType.Video} room={$currentRoom} canMaximize={false} />
|
||||
<RoomComponent room={$currentRoom} canMaximize={false} />
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
import { getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { currentMeetingMinutes, infos, invites, myInfo, myRequests } from '../stores'
|
||||
import { tryConnect } from '../utils'
|
||||
import { currentMeetingMinutes, infos, myInfo } from '../stores'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
import MicrophoneButton from './meeting/controls/MicrophoneButton.svelte'
|
||||
import CameraButton from './meeting/controls/CameraButton.svelte'
|
||||
import ShareScreenButton from './meeting/controls/ShareScreenButton.svelte'
|
||||
import LeaveRoomButton from './meeting/controls/LeaveRoomButton.svelte'
|
||||
import MeetingHeader from './meeting/MeetingHeader.svelte'
|
||||
import { joinMeeting } from '../meetings'
|
||||
|
||||
export let room: Room
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function connect (): Promise<void> {
|
||||
await tryConnect($myInfo, room, info, $myRequests, $invites)
|
||||
await joinMeeting(room)
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Label, Toggle } from '@hcengineering/ui'
|
||||
import love from '../plugin'
|
||||
import { isShareWithSound, isSharingEnabled, liveKitClient } from '../utils'
|
||||
import { isShareWithSound, liveKitClient } from '../utils'
|
||||
import { ScreenSharingState, screenSharingState } from '../liveKitClient'
|
||||
</script>
|
||||
|
||||
<div class="antiPopup p-4 grid">
|
||||
@@ -11,8 +12,8 @@
|
||||
on={$isShareWithSound}
|
||||
on:change={(e) => {
|
||||
$isShareWithSound = e.detail
|
||||
if ($isSharingEnabled) {
|
||||
void liveKitClient.setScreenShareEnabled($isSharingEnabled, e.detail)
|
||||
if ($screenSharingState === ScreenSharingState.Local) {
|
||||
void liveKitClient.setScreenShareEnabled(true, e.detail)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -16,13 +16,12 @@
|
||||
import { tooltip, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
|
||||
import love from '../plugin'
|
||||
import { isSharingEnabled, isShareWithSound, screenSharing, liveKitClient } from '../utils'
|
||||
import { isShareWithSound, liveKitClient } from '../utils'
|
||||
|
||||
import SharingStatePopup from './SharingStatePopup.svelte'
|
||||
import IconShare from './icons/Share.svelte'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
import { lkSessionConnected, ScreenSharingState, screenSharingState } from '../liveKitClient'
|
||||
|
||||
let disabled: boolean = false
|
||||
let pressed: boolean = false
|
||||
|
||||
function handleShowPopup (ev: MouseEvent): void {
|
||||
@@ -33,15 +32,13 @@
|
||||
}
|
||||
|
||||
function handleShare (): void {
|
||||
if (disabled) return
|
||||
if ($screenSharingState !== ScreenSharingState.Inactive) return
|
||||
void liveKitClient.setScreenShareEnabled(true, $isShareWithSound)
|
||||
}
|
||||
|
||||
$: disabled = !$screenSharing && !$lkSessionConnected
|
||||
</script>
|
||||
|
||||
{#if $lkSessionConnected}
|
||||
{#if $isSharingEnabled}
|
||||
{#if $screenSharingState === ScreenSharingState.Local}
|
||||
<button
|
||||
class="hulyStatusBarButton mini positive positiveContent"
|
||||
class:pressed
|
||||
@@ -54,7 +51,6 @@
|
||||
<button
|
||||
class="hulyStatusBarButton mini disabled"
|
||||
class:pressed
|
||||
{disabled}
|
||||
use:tooltip={{ label: love.string.Share, direction: 'bottom' }}
|
||||
on:click={handleShare}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { RemoteParticipant, RemoteTrack, RemoteTrackPublication, RoomEvent, Track } from 'livekit-client'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import love from '../plugin'
|
||||
import { disconnect, lk } from '../utils'
|
||||
import { liveKitClient, lk } from '../utils'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
|
||||
let parentElement: HTMLDivElement
|
||||
@@ -11,7 +11,7 @@
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
_participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
const element = track.attach()
|
||||
@@ -23,7 +23,7 @@
|
||||
function handleTrackUnsubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
_participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
const element = document.getElementById(publication.trackSid)
|
||||
@@ -43,7 +43,7 @@
|
||||
lk.off(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.off(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
if ($lkSessionConnected) {
|
||||
await disconnect()
|
||||
await liveKitClient.disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Room, RoomType } from '@hcengineering/love'
|
||||
import { Room } from '@hcengineering/love'
|
||||
import { IconMaximize, ModernButton, Popup, showPopup, TooltipInstance } from '@hcengineering/ui'
|
||||
|
||||
import love from '../../plugin'
|
||||
import { myInfo, myOffice } from '../../stores'
|
||||
import { isFullScreen, screenSharing } from '../../utils'
|
||||
import { isFullScreen } from '../../utils'
|
||||
import ControlBarContainer from './ControlBarContainer.svelte'
|
||||
import RoomModal from '../RoomModal.svelte'
|
||||
import { lkSessionConnected } from '../../liveKitClient'
|
||||
@@ -31,6 +31,7 @@
|
||||
import MicrophoneButton from './controls/MicrophoneButton.svelte'
|
||||
import CameraButton from './controls/CameraButton.svelte'
|
||||
import ShareScreenButton from './controls/ShareScreenButton.svelte'
|
||||
import InviteEmployeeButton from './invites/InviteEmployeeButton.svelte'
|
||||
|
||||
export let room: Room
|
||||
export let canMaximize: boolean = true
|
||||
@@ -38,22 +39,26 @@
|
||||
export let onFullScreen: (() => void) | undefined = undefined
|
||||
|
||||
let allowLeave: boolean = false
|
||||
let noLabel: boolean = false
|
||||
|
||||
$: allowLeave = $myInfo?.room !== ($myOffice?._id ?? love.ids.Reception)
|
||||
|
||||
$: withVideo = $screenSharing || room.type === RoomType.Video
|
||||
|
||||
function maximize (): void {
|
||||
showPopup(RoomModal, { room }, 'full-centered')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control-bar">
|
||||
<ControlBarContainer bind:noLabel>
|
||||
<svelte:fragment slot="right">
|
||||
<ControlBarContainer>
|
||||
<svelte:fragment slot="left">
|
||||
{#if room._id !== love.ids.Reception && $lkSessionConnected}
|
||||
<RoomAccessButton {room} />
|
||||
<InviteEmployeeButton
|
||||
kind={'secondary'}
|
||||
type={'type-button-icon'}
|
||||
size={'large'}
|
||||
iconSize={'medium'}
|
||||
withBackground={false}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="center">
|
||||
@@ -68,8 +73,8 @@
|
||||
<RoomAccessButton {room} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="left">
|
||||
{#if $lkSessionConnected && withVideo && onFullScreen}
|
||||
<svelte:fragment slot="right">
|
||||
{#if $lkSessionConnected && onFullScreen}
|
||||
<ModernButton
|
||||
icon={$isFullScreen ? love.icon.ExitFullScreen : love.icon.FullScreen}
|
||||
tooltip={{
|
||||
@@ -99,7 +104,7 @@
|
||||
{/if}
|
||||
<MeetingOptionsButton {room} />
|
||||
{#if allowLeave}
|
||||
<LeaveRoomButton {room} {noLabel} />
|
||||
<LeaveRoomButton {room} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
|
||||
@@ -13,72 +13,81 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { resizeObserver } from '@hcengineering/ui'
|
||||
import { afterUpdate } from 'svelte'
|
||||
|
||||
export let noLabel: boolean = false
|
||||
|
||||
let grow: HTMLElement
|
||||
let leftPanel: HTMLElement
|
||||
let leftPanelSize: number = 0
|
||||
|
||||
let combinePanel: boolean = false
|
||||
|
||||
const checkBar = (): void => {
|
||||
if (grow === undefined || leftPanel === undefined) return
|
||||
if (!noLabel && leftPanel.clientWidth > leftPanelSize) leftPanelSize = leftPanel.clientWidth
|
||||
if (grow.clientWidth - 16 < leftPanel.clientWidth && !noLabel && !combinePanel) noLabel = true
|
||||
else if (grow.clientWidth - 16 < leftPanel.clientWidth && noLabel && !combinePanel) combinePanel = true
|
||||
else if (grow.clientWidth * 2 - 32 > leftPanel.clientWidth && noLabel && combinePanel) combinePanel = false
|
||||
else if (grow.clientWidth - 32 >= leftPanelSize && noLabel && !combinePanel) noLabel = false
|
||||
}
|
||||
afterUpdate(() => {
|
||||
checkBar()
|
||||
})
|
||||
export let size: 'small' | 'big' = 'big'
|
||||
</script>
|
||||
|
||||
<div class="bar w-full flex-center flex-gap-2 flex-no-shrink" class:combinePanel use:resizeObserver={checkBar}>
|
||||
<div class="bar__right-panel flex-gap-2 flex-center">
|
||||
<slot name="right" />
|
||||
<div class="bar" data-size={size}>
|
||||
<div class="row">
|
||||
<div class="left"><slot name="left" /></div>
|
||||
<div class="center"><slot name="center" /></div>
|
||||
<div class="right"><slot name="right" /></div>
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
<div bind:this={grow} class="flex-grow" />
|
||||
<slot name="center" />
|
||||
<div bind:this={leftPanel} class="bar__left-panel flex-gap-2 flex-center">
|
||||
<slot name="left" />
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.bar {
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
--g: 0.5rem;
|
||||
}
|
||||
|
||||
.bar {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
container-type: inline-size;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__left-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 1rem;
|
||||
height: 100%;
|
||||
.row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--g);
|
||||
}
|
||||
.left,
|
||||
.center,
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--g);
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
.center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@container (max-width: 350px) {
|
||||
.bar[data-size='small'] .row {
|
||||
justify-content: center;
|
||||
gap: var(--g);
|
||||
}
|
||||
|
||||
&__right-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 1rem;
|
||||
height: 100%;
|
||||
.bar[data-size='small'] .left,
|
||||
.bar[data-size='small'] .center,
|
||||
.bar[data-size='small'] .right {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
&.combinePanel .bar__left-panel {
|
||||
.bar[data-size='small'] .center {
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.combinePanel .bar__right-panel {
|
||||
@container (max-width: 960px) {
|
||||
.bar[data-size='big'] .row {
|
||||
justify-content: center;
|
||||
gap: var(--g);
|
||||
}
|
||||
.bar[data-size='big'] .left,
|
||||
.bar[data-size='big'] .center,
|
||||
.bar[data-size='big'] .right {
|
||||
display: contents;
|
||||
}
|
||||
.bar[data-size='big'] .center {
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IdMap, Ref, toIdMap } from '@hcengineering/core'
|
||||
import { getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { Invite, isOffice, JoinRequest, Office, ParticipantInfo, RequestStatus, Room } from '@hcengineering/love'
|
||||
import { Invite, isOffice, JoinRequest, ParticipantInfo, RequestStatus, Room } from '@hcengineering/love'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
closePopup,
|
||||
@@ -28,27 +27,19 @@
|
||||
} from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
import { closeWidget, closeWidgetTab, sidebarStore } from '@hcengineering/workbench-resources'
|
||||
import { closeWidget, sidebarStore } from '@hcengineering/workbench-resources'
|
||||
|
||||
import love from '../../plugin'
|
||||
import { activeInvites, currentRoom, infos, myInfo, myInvites, myOffice, myRequests, rooms } from '../../stores'
|
||||
import {
|
||||
connectRoom,
|
||||
createMeetingVideoWidgetTab,
|
||||
createMeetingWidget,
|
||||
disconnect,
|
||||
endMeeting,
|
||||
getRoomName,
|
||||
leaveRoom
|
||||
} from '../../utils'
|
||||
import ActiveInvitesPopup from '../ActiveInvitesPopup.svelte'
|
||||
import InvitePopup from '../InvitePopup.svelte'
|
||||
import { activeInvites, currentRoom, infos, myInfo, myInvites, myRequests, rooms } from '../../stores'
|
||||
import { createMeetingWidget, getRoomName } from '../../utils'
|
||||
import ActiveInvitesPopup from './invites/ActiveInvitesPopup.svelte'
|
||||
import InvitePopup from './invites/InvitePopup.svelte'
|
||||
import PersonActionPopup from '../PersonActionPopup.svelte'
|
||||
import RequestPopup from '../RequestPopup.svelte'
|
||||
import RequestingPopup from '../RequestingPopup.svelte'
|
||||
import RequestPopup from './invites/RequestPopup.svelte'
|
||||
import RequestingPopup from './invites/RequestingPopup.svelte'
|
||||
import RoomPopup from '../RoomPopup.svelte'
|
||||
import RoomButton from '../RoomButton.svelte'
|
||||
import { getPersonByPersonRef } from '@hcengineering/contact-resources'
|
||||
import { leaveMeeting, currentMeetingRoom } from '../../meetings'
|
||||
import { lkSessionConnected } from '../../liveKitClient'
|
||||
|
||||
const client = getClient()
|
||||
@@ -160,32 +151,6 @@
|
||||
|
||||
$: checkInvites($myInvites)
|
||||
|
||||
async function checkOwnRoomConnection (
|
||||
infos: ParticipantInfo[],
|
||||
myInfo: ParticipantInfo | undefined,
|
||||
myOffice: Office | undefined,
|
||||
isConnected: boolean
|
||||
): Promise<void> {
|
||||
if (myInfo !== undefined && myInfo.room === (myOffice?._id ?? love.ids.Reception)) {
|
||||
if (myOffice === undefined) {
|
||||
await disconnect()
|
||||
return
|
||||
}
|
||||
const filtered = infos.filter((p) => p.room === myOffice._id && p.person !== myInfo.person)
|
||||
if (filtered.length === 0) {
|
||||
if (isConnected) {
|
||||
await disconnect()
|
||||
}
|
||||
} else if (!isConnected) {
|
||||
const myPerson = await getPersonByPersonRef(getCurrentEmployee())
|
||||
if (myPerson == null) return
|
||||
await connectRoom(0, 0, myInfo, myPerson, myOffice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: checkOwnRoomConnection($infos, $myInfo, $myOffice, $lkSessionConnected)
|
||||
|
||||
const myInvitesCategory = 'myInvites'
|
||||
|
||||
let myInvitesPopup: PopupResult | undefined = undefined
|
||||
@@ -193,7 +158,7 @@
|
||||
function checkActiveInvites (invites: Invite[]): void {
|
||||
if (invites.length > 0) {
|
||||
if (myInvitesPopup === undefined) {
|
||||
myInvitesPopup = showPopup(ActiveInvitesPopup, { invites }, undefined, undefined, undefined, {
|
||||
myInvitesPopup = showPopup(ActiveInvitesPopup, {}, undefined, undefined, undefined, {
|
||||
category: myInvitesCategory,
|
||||
overlay: false,
|
||||
fixed: true
|
||||
@@ -211,7 +176,7 @@
|
||||
|
||||
$: checkActiveInvites($activeInvites)
|
||||
|
||||
function checkActiveVideo (loc: Location, video: boolean, room: Ref<Room> | undefined): void {
|
||||
function checkActiveMeeting (loc: Location, meetingSessionConnected: boolean, room: Ref<Room> | undefined): void {
|
||||
const meetingWidgetState = $sidebarStore.widgetsState.get(love.ids.MeetingWidget)
|
||||
const isMeetingWidgetCreated = meetingWidgetState !== undefined
|
||||
|
||||
@@ -222,16 +187,16 @@
|
||||
return
|
||||
}
|
||||
|
||||
if ($lkSessionConnected) {
|
||||
if (meetingSessionConnected) {
|
||||
if (currentMeetingRoom !== undefined && room !== currentMeetingRoom) {
|
||||
void leaveMeeting()
|
||||
return
|
||||
}
|
||||
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: love.ids.MeetingWidget })[0]
|
||||
if (widget === undefined) return
|
||||
|
||||
if (!isMeetingWidgetCreated) {
|
||||
createMeetingWidget(widget, room, video)
|
||||
} else if (video && !meetingWidgetState.tabs.some(({ id }) => id === 'video')) {
|
||||
createMeetingVideoWidgetTab(widget)
|
||||
} else if (!video && meetingWidgetState.tabs.some(({ id }) => id === 'video')) {
|
||||
void closeWidgetTab(widget, 'video')
|
||||
createMeetingWidget(widget, room, meetingSessionConnected)
|
||||
}
|
||||
} else {
|
||||
if (isMeetingWidgetCreated) {
|
||||
@@ -240,7 +205,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: checkActiveVideo($location, $lkSessionConnected, $currentRoom?._id)
|
||||
$: checkActiveMeeting($location, $lkSessionConnected, $currentRoom?._id)
|
||||
|
||||
$: joined = activeRooms.filter((r) => $myInfo?.room === r._id)
|
||||
|
||||
@@ -270,11 +235,7 @@
|
||||
|
||||
const beforeUnloadListener = () => {
|
||||
if ($myInfo !== undefined && $lkSessionConnected) {
|
||||
if ($myOffice !== undefined && $myInfo.room === $myOffice._id) {
|
||||
endMeeting($myOffice, $rooms, $infos, $myInfo)
|
||||
} else {
|
||||
leaveRoom($myInfo, $myOffice)
|
||||
}
|
||||
leaveMeeting()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Button, Notification, NotificationToast } from '@hcengineering/ui'
|
||||
import { myInfo, myOffice } from '../../stores'
|
||||
import { leaveRoom } from '../../utils'
|
||||
import love from '../../plugin'
|
||||
import { onMount } from 'svelte'
|
||||
import { playSound } from '@hcengineering/presentation'
|
||||
import { leaveMeeting } from '../../meetings'
|
||||
|
||||
export let onRemove: () => void
|
||||
export let notification: Notification
|
||||
|
||||
function stayInMeeting (e: MouseEvent): void {
|
||||
function stayInMeeting (_e: MouseEvent): void {
|
||||
onRemove()
|
||||
}
|
||||
|
||||
async function leaveMeeting (e: MouseEvent): Promise<void> {
|
||||
await leaveRoom($myInfo, $myOffice)
|
||||
async function leave (_e: MouseEvent): Promise<void> {
|
||||
await leaveMeeting()
|
||||
onRemove()
|
||||
}
|
||||
|
||||
@@ -30,7 +29,7 @@
|
||||
<svelte:fragment slot="buttons">
|
||||
<div style="width: auto" />
|
||||
<div class="flex-between gap-2">
|
||||
<Button label={love.string.LeaveRoom} stopPropagation={false} kind="negative" on:click={leaveMeeting} />
|
||||
<Button label={love.string.LeaveRoom} stopPropagation={false} kind="negative" on:click={leave} />
|
||||
<Button label={love.string.StayInRoom} stopPropagation={false} kind="primary" on:click={stayInMeeting} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import love from '../../../plugin'
|
||||
import { ModernButton } from '@hcengineering/ui'
|
||||
import { endMeeting, leaveRoom } from '../../../utils'
|
||||
import { infos, myInfo, myOffice, rooms } from '../../../stores'
|
||||
import { myInfo, myOffice } from '../../../stores'
|
||||
import { isOffice, Room } from '@hcengineering/love'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { leaveMeeting } from '../../../meetings'
|
||||
|
||||
export let room: Room
|
||||
export let size: 'large' | 'medium' | 'small' | 'extra-small' | 'min' = 'large'
|
||||
@@ -15,11 +15,7 @@
|
||||
$: isMyOffice = isOffice(room) && $myInfo?.room === $myOffice?._id
|
||||
|
||||
async function leave (): Promise<void> {
|
||||
if (isMyOffice && isOffice(room) && $myInfo !== undefined) {
|
||||
await endMeeting(room, $rooms, $infos, $myInfo)
|
||||
} else {
|
||||
await leaveRoom($myInfo, $myOffice)
|
||||
}
|
||||
await leaveMeeting()
|
||||
dispatch('leave')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { AccountRole, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
||||
import { ModernButton } from '@hcengineering/ui'
|
||||
import { isRecording, isRecordingAvailable, record } from '../../../utils'
|
||||
import { ButtonBaseSize, ModernButton } from '@hcengineering/ui'
|
||||
import { isRecording, isRecordingAvailable, loveClient } from '../../../utils'
|
||||
import love from '../../../plugin'
|
||||
import { lkSessionConnected } from '../../../liveKitClient'
|
||||
import { Room } from '@hcengineering/love'
|
||||
|
||||
export let room: Room
|
||||
export let size: 'large' | 'medium' | 'small' | 'extra-small' | 'min' = 'large'
|
||||
export let size: ButtonBaseSize = 'large'
|
||||
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
|
||||
</script>
|
||||
|
||||
{#if hasAccountRole(getCurrentAccount(), AccountRole.User) && $isRecordingAvailable}
|
||||
@@ -15,8 +16,8 @@
|
||||
icon={$isRecording ? love.icon.StopRecord : love.icon.Record}
|
||||
tooltip={{ label: $isRecording ? love.string.StopRecord : love.string.Record }}
|
||||
disabled={!$lkSessionConnected}
|
||||
kind={'secondary'}
|
||||
{kind}
|
||||
{size}
|
||||
on:click={() => record(room)}
|
||||
on:click={() => loveClient.record(room)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { isOffice, Room, RoomAccess, roomAccessIcon } from '@hcengineering/love'
|
||||
import love from '../../../plugin'
|
||||
import { eventToHTMLElement, ModernButton, showPopup } from '@hcengineering/ui'
|
||||
import { ButtonBaseSize, eventToHTMLElement, ModernButton, showPopup } from '@hcengineering/ui'
|
||||
import RoomAccessPopup from '../../RoomAccessPopup.svelte'
|
||||
import { getCurrentEmployee } from '@hcengineering/contact'
|
||||
|
||||
export let room: Room
|
||||
export let size: 'large' | 'medium' | 'small' | 'extra-small' | 'min' = 'large'
|
||||
export let size: ButtonBaseSize = 'large'
|
||||
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
|
||||
|
||||
const me = getCurrentEmployee()
|
||||
|
||||
@@ -27,7 +28,7 @@
|
||||
: 'currentColor'
|
||||
}}
|
||||
tooltip={{ label: love.string.ChangeAccess }}
|
||||
kind={'secondary'}
|
||||
{kind}
|
||||
{size}
|
||||
disabled={isOffice(room) && room.person !== me}
|
||||
on:click={setAccess}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { eventToHTMLElement, IconUpOutline, showPopup, SplitButton } from '@hcengineering/ui'
|
||||
import { isShareWithSound, isSharingEnabled, liveKitClient, screenSharing } from '../../../utils'
|
||||
import { isShareWithSound, liveKitClient } from '../../../utils'
|
||||
import ShareSettingPopup from '../../ShareSettingPopup.svelte'
|
||||
import love from '../../../plugin'
|
||||
import { lkSessionConnected } from '../../../liveKitClient'
|
||||
import { lkSessionConnected, ScreenSharingState, screenSharingState } from '../../../liveKitClient'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let size: 'large' | 'medium' | 'small' | 'extra-small' | 'min' = 'large'
|
||||
@@ -11,7 +11,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function changeShare (): Promise<void> {
|
||||
const newValue = !$isSharingEnabled
|
||||
const newValue = $screenSharingState !== ScreenSharingState.Local
|
||||
const audio = newValue && $isShareWithSound
|
||||
await liveKitClient.setScreenShareEnabled(newValue, audio)
|
||||
dispatch('changeShare')
|
||||
@@ -20,16 +20,18 @@
|
||||
function shareSettings (e: MouseEvent): void {
|
||||
showPopup(ShareSettingPopup, {}, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
$: localScreenShare = $screenSharingState === ScreenSharingState.Local
|
||||
</script>
|
||||
|
||||
<SplitButton
|
||||
{size}
|
||||
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
|
||||
icon={localScreenShare ? love.icon.SharingEnabled : love.icon.SharingDisabled}
|
||||
iconProps={{
|
||||
fill: $isSharingEnabled ? 'var(--bg-negative-default)' : 'var(--bg-positive-default)'
|
||||
fill: localScreenShare ? 'var(--bg-negative-default)' : 'var(--bg-positive-default)'
|
||||
}}
|
||||
showTooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
|
||||
disabled={($screenSharing && !$isSharingEnabled) || !$lkSessionConnected}
|
||||
showTooltip={{ label: localScreenShare ? love.string.StopShare : love.string.Share }}
|
||||
disabled={$screenSharingState === ScreenSharingState.Remote || !$lkSessionConnected}
|
||||
action={changeShare}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={shareSettings}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import { lkSessionConnected } from '../../../liveKitClient'
|
||||
import love from '../../../plugin'
|
||||
import view from '@hcengineering/view'
|
||||
import { ModernButton } from '@hcengineering/ui'
|
||||
import { ButtonBaseSize, ModernButton } from '@hcengineering/ui'
|
||||
import { Room } from '@hcengineering/love'
|
||||
|
||||
export let room: Room
|
||||
export let size: 'large' | 'medium' | 'small' | 'extra-small' | 'min' = 'large'
|
||||
export let size: ButtonBaseSize = 'large'
|
||||
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
|
||||
</script>
|
||||
|
||||
{#if hasAccountRole(getCurrentAccount(), AccountRole.User) && isTranscriptionAllowed() && $lkSessionConnected}
|
||||
@@ -16,7 +17,7 @@
|
||||
icon={view.icon.Feather}
|
||||
iconProps={$isTranscription ? { fill: 'var(--button-negative-BackgroundColor)' } : {}}
|
||||
tooltip={{ label: $isTranscription ? love.string.StopTranscription : love.string.StartTranscription }}
|
||||
kind="secondary"
|
||||
{kind}
|
||||
{size}
|
||||
on:click={() => {
|
||||
if ($isTranscription) {
|
||||
|
||||
+6
-14
@@ -15,23 +15,15 @@
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { CombineAvatars } from '@hcengineering/contact-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Button, Label } from '@hcengineering/ui'
|
||||
import { Invite, RequestStatus } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import love from '../../../plugin'
|
||||
import { cancelInvites } from '../../../meetings'
|
||||
import { activeInvites } from '../../../stores'
|
||||
|
||||
export let invites: Invite[]
|
||||
$: persons = $activeInvites.map((p) => p.target)
|
||||
|
||||
$: persons = invites.map((p) => p.target)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function cancel () {
|
||||
for (const invite of invites) {
|
||||
if (invite.status === RequestStatus.Pending) {
|
||||
await client.remove(invite)
|
||||
}
|
||||
}
|
||||
async function cancel (): Promise<void> {
|
||||
await cancelInvites($activeInvites)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { ButtonBaseSize, IconSize, ModernButton, showPopup } from '@hcengineering/ui'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { sendInvites } from '../../../meetings'
|
||||
import love from '../../../plugin'
|
||||
import { SelectUsersPopup } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let employee: Employee | undefined = undefined
|
||||
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
|
||||
export let type: 'type-button' | 'type-button-icon' = 'type-button'
|
||||
export let size: ButtonBaseSize = 'small'
|
||||
export let iconSize: IconSize = 'small'
|
||||
export let withBackground: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function invite (): Promise<void> {
|
||||
if (employee !== undefined) {
|
||||
await sendInvites([employee._id])
|
||||
} else {
|
||||
openSelectUsersPopup()
|
||||
}
|
||||
}
|
||||
|
||||
function openSelectUsersPopup (): void {
|
||||
showPopup(
|
||||
SelectUsersPopup,
|
||||
{
|
||||
okLabel: love.string.Invite,
|
||||
skipCurrentAccount: true,
|
||||
skipInactive: true,
|
||||
showStatus: true
|
||||
},
|
||||
'top',
|
||||
(result?: Ref<Employee>[]) => {
|
||||
if (result != null) {
|
||||
void sendInvites(result)
|
||||
}
|
||||
dispatch('close')
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class:button-container={withBackground}>
|
||||
<ModernButton
|
||||
label={type === 'type-button-icon' ? undefined : love.string.Invite}
|
||||
icon={love.icon.Invite}
|
||||
{size}
|
||||
{iconSize}
|
||||
{type}
|
||||
{kind}
|
||||
on:click={invite}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.button-container {
|
||||
border-radius: var(--small-BorderRadius);
|
||||
display: flex;
|
||||
background-color: var(--theme-button-container-color);
|
||||
}
|
||||
</style>
|
||||
+8
-20
@@ -14,15 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { formatName, Person } from '@hcengineering/contact'
|
||||
import { Avatar, getPersonByPersonRef, getPersonByPersonRefCb } from '@hcengineering/contact-resources'
|
||||
import { getClient, playNotificationSound } from '@hcengineering/presentation'
|
||||
import { Avatar, getPersonByPersonRefCb } from '@hcengineering/contact-resources'
|
||||
import { playNotificationSound } from '@hcengineering/presentation'
|
||||
import { Button, Label } from '@hcengineering/ui'
|
||||
import { Invite, RequestStatus, getFreeRoomPlace } from '@hcengineering/love'
|
||||
import { Invite } from '@hcengineering/love'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
import love from '../plugin'
|
||||
import { infos, myInfo, rooms } from '../stores'
|
||||
import { connectRoom } from '../utils'
|
||||
import love from '../../../plugin'
|
||||
import { acceptInvite, rejectInvite } from '../../../meetings'
|
||||
|
||||
export let invite: Invite
|
||||
|
||||
@@ -31,25 +30,14 @@
|
||||
person = p ?? undefined
|
||||
})
|
||||
|
||||
const client = getClient()
|
||||
let stopSound: (() => void) | null = null
|
||||
|
||||
async function accept (): Promise<void> {
|
||||
const room = $rooms.find((p) => p._id === invite.room)
|
||||
if (room === undefined) return
|
||||
const myPerson = await getPersonByPersonRef(invite.target)
|
||||
if (myPerson == null) return
|
||||
if ($myInfo === undefined) return
|
||||
await client.update(invite, { status: RequestStatus.Approved })
|
||||
const place = getFreeRoomPlace(
|
||||
room,
|
||||
$infos.filter((p) => p.room === room?._id),
|
||||
myPerson._id
|
||||
)
|
||||
await connectRoom(place.x, place.y, $myInfo, myPerson, room)
|
||||
await acceptInvite(invite)
|
||||
}
|
||||
|
||||
async function decline (): Promise<void> {
|
||||
await client.update(invite, { status: RequestStatus.Rejected })
|
||||
await rejectInvite(invite)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
+8
-16
@@ -13,36 +13,28 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { formatName, getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { Avatar, getPersonByPersonRef, getPersonByPersonRefStore } from '@hcengineering/contact-resources'
|
||||
import { getClient, playNotificationSound } from '@hcengineering/presentation'
|
||||
import { formatName } from '@hcengineering/contact'
|
||||
import { Avatar, getPersonByPersonRefStore } from '@hcengineering/contact-resources'
|
||||
import { playNotificationSound } from '@hcengineering/presentation'
|
||||
import { Button, Label } from '@hcengineering/ui'
|
||||
import { JoinRequest, RequestStatus } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { myInfo, myOffice } from '../stores'
|
||||
import { connectRoom } from '../utils'
|
||||
import { JoinRequest } from '@hcengineering/love'
|
||||
import love from '../../../plugin'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { lkSessionConnected } from '../liveKitClient'
|
||||
import { acceptJoinRequest, rejectJoinRequest } from '../../../meetings'
|
||||
|
||||
export let request: JoinRequest
|
||||
|
||||
$: personByRefStore = getPersonByPersonRefStore([request.person])
|
||||
$: person = $personByRefStore.get(request.person)
|
||||
|
||||
const client = getClient()
|
||||
let stopSound: (() => void) | null = null
|
||||
|
||||
async function accept (): Promise<void> {
|
||||
await client.update(request, { status: RequestStatus.Approved })
|
||||
if (request.room === $myOffice?._id && !$lkSessionConnected) {
|
||||
const person = await getPersonByPersonRef(getCurrentEmployee())
|
||||
if (person == null) return
|
||||
await connectRoom(0, 0, $myInfo, person, $myOffice)
|
||||
}
|
||||
await acceptJoinRequest(request)
|
||||
}
|
||||
|
||||
async function decline (): Promise<void> {
|
||||
await client.update(request, { status: RequestStatus.Rejected })
|
||||
await rejectJoinRequest(request)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
+6
-9
@@ -13,22 +13,19 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Button, Label } from '@hcengineering/ui'
|
||||
import { JoinRequest, RequestStatus } from '@hcengineering/love'
|
||||
import love from '../plugin'
|
||||
import { rooms } from '../stores'
|
||||
import { getRoomLabel } from '../utils'
|
||||
import { JoinRequest } from '@hcengineering/love'
|
||||
import love from '../../../plugin'
|
||||
import { rooms } from '../../../stores'
|
||||
import { getRoomLabel } from '../../../utils'
|
||||
import { cancelJoinRequest } from '../../../meetings'
|
||||
|
||||
export let request: JoinRequest
|
||||
|
||||
$: room = $rooms.find((p) => p._id === request.room)
|
||||
|
||||
async function cancel (): Promise<void> {
|
||||
if (request.status === RequestStatus.Pending) {
|
||||
const client = getClient()
|
||||
await client.remove(request)
|
||||
}
|
||||
await cancelJoinRequest(request)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -25,12 +25,13 @@
|
||||
import TranscriptionTab from './TranscriptionTab.svelte'
|
||||
import { lkSessionConnected } from '../../../liveKitClient'
|
||||
import LeaveRoomButton from '../controls/LeaveRoomButton.svelte'
|
||||
import RoomAccessButton from '../controls/RoomAccessButton.svelte'
|
||||
import RecordingButton from '../controls/RecordingButton.svelte'
|
||||
import TranscriptionButton from '../controls/TranscriptionButton.svelte'
|
||||
import ControlBarContainer from '../ControlBarContainer.svelte'
|
||||
import SendReactionButton from '../controls/SendReactionButton.svelte'
|
||||
import MeetingWidgetHeader from './MeetingWidgetHeader.svelte'
|
||||
import InviteEmployeeButton from '../invites/InviteEmployeeButton.svelte'
|
||||
import MicrophoneButton from '../controls/MicrophoneButton.svelte'
|
||||
import CameraButton from '../controls/CameraButton.svelte'
|
||||
import ShareScreenButton from '../controls/ShareScreenButton.svelte'
|
||||
|
||||
export let widgetState: WidgetState | undefined
|
||||
export let height: string
|
||||
@@ -107,16 +108,23 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<ControlBarContainer>
|
||||
<svelte:fragment slot="right">
|
||||
<RoomAccessButton {room} size="small" />
|
||||
<ControlBarContainer size="small">
|
||||
<svelte:fragment slot="left">
|
||||
<InviteEmployeeButton
|
||||
kind="secondary"
|
||||
type="type-button-icon"
|
||||
size="small"
|
||||
iconSize="medium"
|
||||
withBackground={false}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="center">
|
||||
<SendReactionButton size="small" />
|
||||
<RecordingButton {room} size="small" />
|
||||
<TranscriptionButton {room} size="small" />
|
||||
<MicrophoneButton size="small" />
|
||||
<CameraButton size="small" />
|
||||
<ShareScreenButton size="small" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="left">
|
||||
<svelte:fragment slot="right">
|
||||
<LeaveRoomButton {room} noLabel={true} size="small" />
|
||||
</svelte:fragment>
|
||||
</ControlBarContainer>
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
import { MeetingMinutes, Room } from '@hcengineering/love'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
import love from '../../../plugin'
|
||||
import RoomModal from '../../RoomModal.svelte'
|
||||
import { currentRoom } from '../../../stores'
|
||||
import MeetingOptionsButton from '../controls/MeetingOptionsButton.svelte'
|
||||
import RecordingButton from '../controls/RecordingButton.svelte'
|
||||
import TranscriptionButton from '../controls/TranscriptionButton.svelte'
|
||||
import RoomAccessButton from '../controls/RoomAccessButton.svelte'
|
||||
|
||||
export let room: Room
|
||||
export let doc: MeetingMinutes | undefined = undefined
|
||||
@@ -39,7 +41,6 @@
|
||||
$: breadcrumbs = [
|
||||
{
|
||||
id: 'meeting',
|
||||
icon: love.icon.Cam,
|
||||
title: doc?.title ?? room.name
|
||||
}
|
||||
]
|
||||
@@ -57,8 +58,11 @@
|
||||
<Breadcrumbs items={breadcrumbs} currentOnly />
|
||||
<svelte:fragment slot="actions">
|
||||
{#if $currentRoom !== undefined}
|
||||
<ButtonIcon icon={IconMaximize} kind="tertiary" size="small" noPrint on:click={maximize} />
|
||||
<RoomAccessButton {room} kind="tertiary" size="small" />
|
||||
<RecordingButton {room} kind="tertiary" size="small" />
|
||||
<TranscriptionButton {room} kind="tertiary" size="small" />
|
||||
<MeetingOptionsButton {room} kind="tertiary" size="small" />
|
||||
<ButtonIcon icon={IconMaximize} kind="tertiary" size="small" noPrint on:click={maximize} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Header>
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
import { AppItem } from '@hcengineering/workbench-resources'
|
||||
import { RoomType } from '@hcengineering/love'
|
||||
import { currentRoom } from '../../../stores'
|
||||
import { isSharingEnabled } from '../../../utils'
|
||||
import { state } from '@hcengineering/media-resources'
|
||||
import love from '../../../plugin'
|
||||
import { lkSessionConnected } from '../../../liveKitClient'
|
||||
import { lkSessionConnected, ScreenSharingState, screenSharingState } from '../../../liveKitClient'
|
||||
|
||||
export let label: IntlString
|
||||
export let icon: Asset | AnySvelteComponent
|
||||
@@ -35,7 +34,7 @@
|
||||
|
||||
<AppItem
|
||||
{label}
|
||||
icon={$isSharingEnabled
|
||||
icon={$screenSharingState === ScreenSharingState.Local
|
||||
? love.icon.SharingDisabled
|
||||
: $lkSessionConnected && allowCam && !isCamEnabled && !isMicEnabled
|
||||
? love.icon.CamDisabled
|
||||
@@ -46,7 +45,7 @@
|
||||
: icon}
|
||||
{selected}
|
||||
{size}
|
||||
kind={$isSharingEnabled
|
||||
kind={$screenSharingState === ScreenSharingState.Local
|
||||
? 'negative'
|
||||
: $lkSessionConnected && isCamEnabled
|
||||
? 'positive'
|
||||
|
||||
@@ -27,6 +27,7 @@ import MediaPopupItemExt from './components/MediaPopupItemExt.svelte'
|
||||
import SharingStateIndicator from './components/SharingStateIndicator.svelte'
|
||||
import MeetingScheduleData from './components/MeetingScheduleData.svelte'
|
||||
import EditMeetingScheduleData from './components/EditMeetingScheduleData.svelte'
|
||||
import InviteEmployeeButton from './components/meeting/invites/InviteEmployeeButton.svelte'
|
||||
|
||||
import {
|
||||
copyGuestLink,
|
||||
@@ -68,7 +69,8 @@ export default async (): Promise<Resources> => ({
|
||||
MediaPopupItemExt,
|
||||
SharingStateIndicator,
|
||||
MeetingScheduleData,
|
||||
EditMeetingScheduleData
|
||||
EditMeetingScheduleData,
|
||||
InviteEmployeeButton
|
||||
},
|
||||
function: {
|
||||
CreateMeeting: createMeeting,
|
||||
|
||||
@@ -3,29 +3,42 @@ import {
|
||||
type RemoteParticipant,
|
||||
Room as LKRoom,
|
||||
RoomEvent,
|
||||
type VideoCaptureOptions
|
||||
type VideoCaptureOptions,
|
||||
type RemoteTrack,
|
||||
type RemoteTrackPublication,
|
||||
Track,
|
||||
type LocalTrackPublication,
|
||||
type LocalParticipant,
|
||||
type TrackPublication,
|
||||
type Participant
|
||||
} from 'livekit-client'
|
||||
import { getMetadata, translate } from '@hcengineering/platform'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { getMediaDevices, getSelectedSpeakerId, type MediaSession } from '@hcengineering/media'
|
||||
import { LoveEvents } from '@hcengineering/love'
|
||||
import { useMedia } from '@hcengineering/media-resources'
|
||||
import { get, writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { addNotification, NotificationSeverity } from '@hcengineering/ui'
|
||||
import { getCurrentLanguage } from '@hcengineering/theme'
|
||||
import LastParticipantNotification from './components/meeting/LastParticipantNotification.svelte'
|
||||
import love from './plugin'
|
||||
import { leaveRoom } from './utils'
|
||||
import { $myPreferences, myInfo, myOffice } from './stores'
|
||||
import { $myPreferences } from './stores'
|
||||
import { leaveMeeting } from './meetings'
|
||||
|
||||
export enum ScreenSharingState {
|
||||
Inactive,
|
||||
Local,
|
||||
Remote
|
||||
}
|
||||
|
||||
export const screenSharingState = writable<ScreenSharingState>(ScreenSharingState.Inactive)
|
||||
export const lkSessionConnected = writable<boolean>(false)
|
||||
|
||||
const LAST_PARTICIPANT_NOTIFICATION_DELAY_MS = 2 * 60 * 1000
|
||||
const AUTO_DISCONNECT_DELAY_MS = 60 * 1000
|
||||
|
||||
export function getLiveKitClient (): LiveKitClient {
|
||||
const wsURL = getMetadata(love.metadata.WebSocketURL)
|
||||
return new LiveKitClient(wsURL ?? '')
|
||||
return new LiveKitClient()
|
||||
}
|
||||
|
||||
const defaultCaptureOptions: VideoCaptureOptions = {
|
||||
@@ -46,7 +59,7 @@ export class LiveKitClient {
|
||||
private lastParticipantNotificationTimeout: number = -1
|
||||
private lastParticipantDisconnectTimeout: number = -1
|
||||
|
||||
constructor (wsUrl: string) {
|
||||
constructor () {
|
||||
const lkRoom = new LKRoom({
|
||||
adaptiveStream: true,
|
||||
dynacast: true,
|
||||
@@ -68,12 +81,15 @@ export class LiveKitClient {
|
||||
},
|
||||
videoCaptureDefaults: defaultCaptureOptions
|
||||
})
|
||||
void lkRoom.prepareConnection(wsUrl)
|
||||
lkRoom.on(RoomEvent.Connected, this.onConnected)
|
||||
lkRoom.on(RoomEvent.Disconnected, this.onDisconnected)
|
||||
this.liveKitRoom = lkRoom
|
||||
}
|
||||
|
||||
async prepareConnection (wsUrl: string, token: string): Promise<void> {
|
||||
await this.liveKitRoom.prepareConnection(wsUrl, token)
|
||||
}
|
||||
|
||||
async connect (wsURL: string, token: string, withVideo: boolean): Promise<void> {
|
||||
this.isConnecting = true
|
||||
this.currentSessionSupportsVideo = withVideo
|
||||
@@ -127,6 +143,7 @@ export class LiveKitClient {
|
||||
}
|
||||
|
||||
async disconnect (): Promise<void> {
|
||||
screenSharingState.set(ScreenSharingState.Inactive)
|
||||
clearTimeout(this.lastParticipantNotificationTimeout)
|
||||
const me = this.liveKitRoom.localParticipant
|
||||
await Promise.all([me.setScreenShareEnabled(false), me.setCameraEnabled(false), me.setMicrophoneEnabled(false)])
|
||||
@@ -153,21 +170,33 @@ export class LiveKitClient {
|
||||
lkSessionConnected.set(true)
|
||||
this.liveKitRoom.on(RoomEvent.ParticipantConnected, this.onParticipantConnected)
|
||||
this.liveKitRoom.on(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected)
|
||||
this.liveKitRoom.on(RoomEvent.TrackSubscribed, this.onTrackSubscribed)
|
||||
this.liveKitRoom.on(RoomEvent.TrackUnsubscribed, this.onTrackUnsubscribed)
|
||||
this.liveKitRoom.on(RoomEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
||||
this.liveKitRoom.on(RoomEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
||||
this.liveKitRoom.on(RoomEvent.TrackMuted, this.onTrackMuted)
|
||||
this.liveKitRoom.on(RoomEvent.TrackUnmuted, this.onTrackUnmuted)
|
||||
}
|
||||
|
||||
onDisconnected = (): void => {
|
||||
lkSessionConnected.set(false)
|
||||
this.liveKitRoom.off(RoomEvent.ParticipantConnected, this.onParticipantConnected)
|
||||
this.liveKitRoom.off(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected)
|
||||
this.liveKitRoom.off(RoomEvent.TrackSubscribed, this.onTrackSubscribed)
|
||||
this.liveKitRoom.off(RoomEvent.TrackUnsubscribed, this.onTrackUnsubscribed)
|
||||
this.liveKitRoom.off(RoomEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
||||
this.liveKitRoom.off(RoomEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
||||
this.liveKitRoom.off(RoomEvent.TrackMuted, this.onTrackMuted)
|
||||
this.liveKitRoom.off(RoomEvent.TrackUnmuted, this.onTrackUnmuted)
|
||||
Analytics.handleEvent(LoveEvents.DisconnectedFromRoom)
|
||||
}
|
||||
|
||||
onParticipantConnected = (participant: RemoteParticipant): void => {
|
||||
onParticipantConnected = (_participant: RemoteParticipant): void => {
|
||||
clearTimeout(this.lastParticipantDisconnectTimeout)
|
||||
clearTimeout(this.lastParticipantNotificationTimeout)
|
||||
}
|
||||
|
||||
onParticipantDisconnected = (participant: RemoteParticipant): void => {
|
||||
onParticipantDisconnected = (_participant: RemoteParticipant): void => {
|
||||
if (this.liveKitRoom.remoteParticipants.size === 0) {
|
||||
clearTimeout(this.lastParticipantDisconnectTimeout)
|
||||
clearTimeout(this.lastParticipantNotificationTimeout)
|
||||
@@ -177,6 +206,78 @@ export class LiveKitClient {
|
||||
}
|
||||
}
|
||||
|
||||
onTrackSubscribed = (
|
||||
track: RemoteTrack,
|
||||
_publication: RemoteTrackPublication,
|
||||
_participant: RemoteParticipant
|
||||
): void => {
|
||||
if (track.kind === Track.Kind.Video && track.source === Track.Source.ScreenShare) {
|
||||
screenSharingState.set(ScreenSharingState.Remote)
|
||||
}
|
||||
}
|
||||
|
||||
onTrackUnsubscribed = (
|
||||
track: RemoteTrack,
|
||||
_publication: RemoteTrackPublication,
|
||||
_participant: RemoteParticipant
|
||||
): void => {
|
||||
if (track.kind === Track.Kind.Video && track.source === Track.Source.ScreenShare) {
|
||||
screenSharingState.set(ScreenSharingState.Inactive)
|
||||
}
|
||||
}
|
||||
|
||||
onLocalTrackPublished = (publication: LocalTrackPublication, _participant: LocalParticipant): void => {
|
||||
const session = this.currentMediaSession
|
||||
const track = publication.track?.mediaStreamTrack
|
||||
const deviceId = track?.getSettings().deviceId
|
||||
if (publication.track?.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
session?.setFeature('sharing', { enabled: true, track, deviceId })
|
||||
screenSharingState.set(ScreenSharingState.Local)
|
||||
} else {
|
||||
session?.setCamera({ enabled: true, track, deviceId })
|
||||
}
|
||||
} else if (publication.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: true, track, deviceId })
|
||||
}
|
||||
}
|
||||
|
||||
onLocalTrackUnpublished = (publication: LocalTrackPublication, _participant: LocalParticipant): void => {
|
||||
const session = this.currentMediaSession
|
||||
if (publication.track?.kind === Track.Kind.Video) {
|
||||
if (publication.track.source === Track.Source.ScreenShare) {
|
||||
session?.setFeature('sharing', { enabled: false })
|
||||
screenSharingState.set(ScreenSharingState.Inactive)
|
||||
} else {
|
||||
session?.setCamera({ enabled: false })
|
||||
}
|
||||
} else if (publication.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: false })
|
||||
}
|
||||
}
|
||||
|
||||
onTrackMuted = (publication: TrackPublication, participant: Participant): void => {
|
||||
if (!participant.isLocal) return
|
||||
const session = this.currentMediaSession
|
||||
if (publication.track?.kind === Track.Kind.Video && publication.track.source === Track.Source.Camera) {
|
||||
session?.setCamera({ enabled: false })
|
||||
} else if (publication.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: false })
|
||||
}
|
||||
}
|
||||
|
||||
onTrackUnmuted = (publication: TrackPublication, participant: Participant): void => {
|
||||
if (!participant.isLocal) return
|
||||
const session = this.currentMediaSession
|
||||
const track = publication.track?.mediaStreamTrack
|
||||
const deviceId = track?.getSettings().deviceId
|
||||
if (publication.track?.kind === Track.Kind.Video && publication.track?.source === Track.Source.Camera) {
|
||||
session?.setCamera({ enabled: true, track, deviceId })
|
||||
} else if (publication.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: true, track, deviceId })
|
||||
}
|
||||
}
|
||||
|
||||
async showLastParticipantNotification (): Promise<void> {
|
||||
addNotification(
|
||||
await translate(love.string.MeetingEmptyTitle, {}, getCurrentLanguage()),
|
||||
@@ -187,7 +288,7 @@ export class LiveKitClient {
|
||||
'love'
|
||||
)
|
||||
this.lastParticipantDisconnectTimeout = window.setTimeout(() => {
|
||||
void leaveRoom(get(myInfo), get(myOffice))
|
||||
void leaveMeeting()
|
||||
}, AUTO_DISCONNECT_DELAY_MS)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { concatLink } from '@hcengineering/core'
|
||||
import love, { type Room } from '@hcengineering/love'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { getPlatformToken, lk } from './utils'
|
||||
import { getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { getPersonByPersonRef } from '@hcengineering/contact-resources'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { currentMeetingMinutes } from './stores'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
export function getLoveClient (): LoveClient {
|
||||
return new LoveClient()
|
||||
}
|
||||
|
||||
export class LoveClient {
|
||||
async getRoomToken (room: Room): Promise<string> {
|
||||
return await this.refreshRoomToken(room)
|
||||
}
|
||||
|
||||
async updateSessionLanguage (room: Room): Promise<void> {
|
||||
try {
|
||||
const endpoint = this.getLoveEndpoint()
|
||||
const token = getPlatformToken()
|
||||
const roomName = this.getTokenRoomName(room)
|
||||
|
||||
await fetch(concatLink(endpoint, '/language'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName, room: room.name, language: room.language })
|
||||
})
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async record (room: Room): Promise<void> {
|
||||
try {
|
||||
const endpoint = this.getLoveEndpoint()
|
||||
const token = getPlatformToken()
|
||||
const roomName = this.getTokenRoomName(room)
|
||||
if (lk.isRecording) {
|
||||
await fetch(concatLink(endpoint, '/stopRecord'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName, room: room.name })
|
||||
})
|
||||
} else {
|
||||
await fetch(concatLink(endpoint, '/startRecord'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName, room: room.name, meetingMinutes: get(currentMeetingMinutes)?._id })
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
private getLoveEndpoint (): string {
|
||||
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
|
||||
if (endpoint === undefined) {
|
||||
throw new Error('Love service endpoint not found')
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
|
||||
private async refreshRoomToken (room: Room): Promise<string> {
|
||||
const sessionName = this.getTokenRoomName(room)
|
||||
const endpoint = this.getLoveEndpoint()
|
||||
if (endpoint === undefined) {
|
||||
throw new Error('Love service endpoint not found')
|
||||
}
|
||||
const myPerson = await getPersonByPersonRef(getCurrentEmployee())
|
||||
if (myPerson == null) {
|
||||
throw new Error('Cannot find current person')
|
||||
}
|
||||
const platformToken = getPlatformToken()
|
||||
const res = await fetch(concatLink(endpoint, '/getToken'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${platformToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName: sessionName, _id: myPerson._id, participantName: myPerson.name })
|
||||
})
|
||||
return await res.text()
|
||||
}
|
||||
|
||||
private getTokenRoomName (room: Room): string {
|
||||
const currentWorkspaceUuid = getMetadata(presentation.metadata.WorkspaceUuid)
|
||||
if (currentWorkspaceUuid === undefined) {
|
||||
throw new Error('Current workspace not found')
|
||||
}
|
||||
return `${currentWorkspaceUuid}_${room.name}_${room._id}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import core, { AccountRole, getCurrentAccount, type Ref } from '@hcengineering/core'
|
||||
import love, {
|
||||
getFreeRoomPlace,
|
||||
MeetingStatus,
|
||||
type Room,
|
||||
RoomType,
|
||||
isOffice,
|
||||
RoomAccess,
|
||||
RequestStatus,
|
||||
type JoinRequest,
|
||||
type Invite
|
||||
} from '@hcengineering/love'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
closeMeetingMinutes,
|
||||
getLiveKitEndpoint,
|
||||
getRoomName,
|
||||
liveKitClient,
|
||||
loveClient,
|
||||
navigateToOfficeDoc
|
||||
} from './utils'
|
||||
import { get } from 'svelte/store'
|
||||
import { infos, myInfo, myOffice, rooms } from './stores'
|
||||
import { getCurrentEmployee, type Person } from '@hcengineering/contact'
|
||||
import { getPersonByPersonRef } from '@hcengineering/contact-resources'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
|
||||
export let currentMeetingRoom: Ref<Room> | undefined
|
||||
const requestsQuery = createQuery(true)
|
||||
const invitesQuery = createQuery(true)
|
||||
|
||||
export async function createMeeting (room: Room): Promise<void> {
|
||||
if (room.access === RoomAccess.DND) return
|
||||
|
||||
const me = getCurrentEmployee()
|
||||
const currentPerson = await getPersonByPersonRef(me)
|
||||
|
||||
if (isOffice(room) && room.person !== currentPerson?._id) {
|
||||
await sendJoinRequest(room)
|
||||
return
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
const meeting = await client.findOne(love.class.MeetingMinutes, {
|
||||
attachedTo: room._id,
|
||||
status: MeetingStatus.Active
|
||||
})
|
||||
|
||||
if (meeting !== undefined) {
|
||||
await joinMeeting(room)
|
||||
return
|
||||
}
|
||||
|
||||
await createMeetingDocument(room)
|
||||
await connectToMeeting(room)
|
||||
}
|
||||
|
||||
export async function leaveMeeting (): Promise<void> {
|
||||
const client = getClient()
|
||||
const currentParticipationInfo = get(myInfo)
|
||||
const office = get(myOffice)
|
||||
if (currentParticipationInfo === undefined) return
|
||||
|
||||
if (office === undefined) {
|
||||
await client.update(currentParticipationInfo, { room: love.ids.Reception, x: 0, y: 0 })
|
||||
} else {
|
||||
if (currentParticipationInfo.room !== office._id) {
|
||||
await client.update(currentParticipationInfo, { room: office._id, x: 0, y: 0 })
|
||||
} else {
|
||||
// kick all participants in case of own office
|
||||
const allRooms = get(rooms)
|
||||
const participantsInfo = get(infos)
|
||||
const otherParticipants = participantsInfo.filter((p) => p.room === office._id && p !== currentParticipationInfo)
|
||||
for (const participantInfo of otherParticipants) {
|
||||
const participantOffice = allRooms.find((r) => isOffice(r) && r.person === participantInfo.person)
|
||||
await client.update(participantInfo, { room: participantOffice?._id ?? love.ids.Reception, x: 0, y: 0 })
|
||||
}
|
||||
}
|
||||
}
|
||||
await liveKitClient.disconnect()
|
||||
closeMeetingMinutes()
|
||||
currentMeetingRoom = undefined
|
||||
}
|
||||
|
||||
export async function joinMeeting (room: Room): Promise<void> {
|
||||
if (room.access === RoomAccess.DND) return
|
||||
|
||||
const isGuest = getCurrentAccount().role === AccountRole.Guest
|
||||
if (room.access === RoomAccess.Knock || isOffice(room) || isGuest) {
|
||||
await sendJoinRequest(room)
|
||||
return
|
||||
}
|
||||
|
||||
await connectToMeeting(room)
|
||||
}
|
||||
|
||||
export async function acceptInvite (invite: Invite): Promise<void> {
|
||||
const client = getClient()
|
||||
await client.update(invite, { status: RequestStatus.Approved })
|
||||
const room = getRoomById(invite.room)
|
||||
|
||||
if (room === undefined) return
|
||||
|
||||
const meeting = await client.findOne(love.class.MeetingMinutes, {
|
||||
attachedTo: room._id,
|
||||
status: MeetingStatus.Active
|
||||
})
|
||||
if (meeting === undefined) {
|
||||
await createMeetingDocument(room)
|
||||
}
|
||||
|
||||
await connectToMeeting(room)
|
||||
}
|
||||
|
||||
export async function rejectInvite (invite: Invite): Promise<void> {
|
||||
const client = getClient()
|
||||
await client.update(invite, { status: RequestStatus.Rejected })
|
||||
}
|
||||
|
||||
export async function cancelInvites (invites: Invite[]): Promise<void> {
|
||||
const client = getClient()
|
||||
for (const invite of invites) {
|
||||
if (invite.status === RequestStatus.Pending) {
|
||||
await client.remove(invite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendInvites (persons: Array<Ref<Person>>): Promise<void> {
|
||||
const myParticipation = get(myInfo)
|
||||
const room = myParticipation?.room
|
||||
if (room === undefined || room === love.ids.Reception) return
|
||||
const client = getClient()
|
||||
const me = getCurrentEmployee()
|
||||
const inviteIds: Array<Ref<Invite>> = []
|
||||
for (const person of persons) {
|
||||
inviteIds.push(
|
||||
await client.createDoc(love.class.Invite, core.space.Workspace, {
|
||||
target: person,
|
||||
room,
|
||||
status: RequestStatus.Pending,
|
||||
from: me
|
||||
})
|
||||
)
|
||||
}
|
||||
invitesQuery.query(love.class.Invite, { from: me, _id: { $in: inviteIds } }, (res) => {
|
||||
for (const invite of res) {
|
||||
if (invite === undefined || invite.status !== RequestStatus.Approved) {
|
||||
continue
|
||||
}
|
||||
invitesQuery.unsubscribe()
|
||||
const room = getRoomById(invite.room)
|
||||
if (room === undefined) return
|
||||
void connectToMeeting(room)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function acceptJoinRequest (request: JoinRequest): Promise<void> {
|
||||
const client = getClient()
|
||||
const room = getRoomById(request.room)
|
||||
|
||||
if (room === undefined) return
|
||||
|
||||
const meeting = await client.findOne(love.class.MeetingMinutes, {
|
||||
attachedTo: room._id,
|
||||
status: MeetingStatus.Active
|
||||
})
|
||||
if (meeting === undefined) {
|
||||
await createMeetingDocument(room)
|
||||
}
|
||||
|
||||
await client.update(request, { status: RequestStatus.Approved })
|
||||
await connectToMeeting(room)
|
||||
}
|
||||
|
||||
export async function rejectJoinRequest (request: JoinRequest): Promise<void> {
|
||||
const client = getClient()
|
||||
await client.update(request, { status: RequestStatus.Rejected })
|
||||
}
|
||||
|
||||
export async function cancelJoinRequest (request: JoinRequest): Promise<void> {
|
||||
if (request.status === RequestStatus.Pending) {
|
||||
const client = getClient()
|
||||
await client.remove(request)
|
||||
}
|
||||
}
|
||||
|
||||
async function sendJoinRequest (room: Room): Promise<void> {
|
||||
if (room.access === RoomAccess.DND) return
|
||||
|
||||
const me = getCurrentEmployee()
|
||||
const currentPerson = await getPersonByPersonRef(me)
|
||||
|
||||
if (currentPerson == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isOffice(room) && room.person === currentPerson._id) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
const _id = await client.createDoc(love.class.JoinRequest, core.space.Workspace, {
|
||||
person: currentPerson._id,
|
||||
room: room._id,
|
||||
status: RequestStatus.Pending
|
||||
})
|
||||
requestsQuery.query(love.class.JoinRequest, { person: me, _id }, (res) => {
|
||||
const req = res[0]
|
||||
if (req === undefined) return
|
||||
if (req.status === RequestStatus.Pending) return
|
||||
requestsQuery.unsubscribe()
|
||||
if (req.status === RequestStatus.Approved) {
|
||||
void connectToMeeting(room)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function kick (person: Ref<Person>): Promise<void> {
|
||||
const client = getClient()
|
||||
const allRooms = get(rooms)
|
||||
const participantsInfo = get(infos)
|
||||
const participantInfo = participantsInfo.find((p) => p.person === person)
|
||||
if (participantInfo === undefined) return
|
||||
const participantOffice = allRooms.find((r) => isOffice(r) && r.person === person)
|
||||
await client.update(participantInfo, { room: participantOffice?._id ?? love.ids.Reception, x: 0, y: 0 })
|
||||
}
|
||||
|
||||
async function connectToMeeting (room: Room): Promise<void> {
|
||||
if (getCurrentAccount().role === AccountRole.ReadOnlyGuest) return
|
||||
if (currentMeetingRoom === room._id) return
|
||||
|
||||
if (currentMeetingRoom !== undefined) {
|
||||
await leaveMeeting()
|
||||
}
|
||||
|
||||
currentMeetingRoom = room._id
|
||||
|
||||
await navigateToOfficeDoc(room)
|
||||
await moveToMeetingRoom(room)
|
||||
|
||||
try {
|
||||
const token = await loveClient.getRoomToken(room)
|
||||
const wsURL = getLiveKitEndpoint()
|
||||
await liveKitClient.connect(wsURL, token, room.type === RoomType.Video)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
await leaveMeeting()
|
||||
}
|
||||
}
|
||||
|
||||
async function moveToMeetingRoom (room: Room): Promise<void> {
|
||||
const me = getCurrentEmployee()
|
||||
const currentPerson = await getPersonByPersonRef(me)
|
||||
const client = getClient()
|
||||
const myParticipation = get(myInfo)
|
||||
if (myParticipation?.room === room._id) return
|
||||
if (room === undefined || currentPerson == null) return
|
||||
const roomParticipants = get(infos).filter((p) => p.room === room._id)
|
||||
const place = getFreeRoomPlace(room, roomParticipants, me)
|
||||
const sessionId = getMetadata(presentation.metadata.SessionId) ?? null
|
||||
|
||||
const currentInfo = get(myInfo)
|
||||
if (currentInfo !== undefined) {
|
||||
await client.diffUpdate(currentInfo, {
|
||||
x: place.x,
|
||||
y: place.y,
|
||||
room: room._id,
|
||||
sessionId
|
||||
})
|
||||
} else {
|
||||
await client.createDoc(love.class.ParticipantInfo, core.space.Workspace, {
|
||||
x: place.x,
|
||||
y: place.y,
|
||||
room: room._id,
|
||||
person: currentPerson._id,
|
||||
name: currentPerson.name,
|
||||
account: getCurrentAccount().uuid,
|
||||
sessionId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function createMeetingDocument (room: Room): Promise<void> {
|
||||
const client = getClient()
|
||||
await client.createDoc(love.class.MeetingMinutes, core.space.Workspace, {
|
||||
description: null,
|
||||
attachedTo: room._id,
|
||||
status: MeetingStatus.Active,
|
||||
title: await getNewMeetingTitle(room),
|
||||
attachedToClass: love.class.Room,
|
||||
collection: 'meetings'
|
||||
})
|
||||
}
|
||||
|
||||
async function getNewMeetingTitle (room: Room): Promise<string> {
|
||||
const date = new Date()
|
||||
.toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
.replace(',', ' at')
|
||||
return `${await getRoomName(room)} ${date}`
|
||||
}
|
||||
|
||||
function getRoomById (roomId: Ref<Room>): Room | undefined {
|
||||
const allRooms = get(rooms)
|
||||
return allRooms.find((p) => p._id === roomId)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import aiBot from '@hcengineering/ai-bot'
|
||||
import { connectMeeting, disconnectMeeting } from '@hcengineering/ai-bot-resources'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import calendar, { type Event, getAllEvents, type Schedule } from '@hcengineering/calendar'
|
||||
import calendar, { type Event, type Schedule } from '@hcengineering/calendar'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import contact, { getCurrentEmployee, getName, type Person } from '@hcengineering/contact'
|
||||
import { getName } from '@hcengineering/contact'
|
||||
import core, {
|
||||
AccountRole,
|
||||
type Client,
|
||||
@@ -12,7 +12,6 @@ import core, {
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
getCurrentAccount,
|
||||
type Hierarchy,
|
||||
type Ref,
|
||||
type RelatedDocument,
|
||||
type Space,
|
||||
@@ -21,8 +20,6 @@ import core, {
|
||||
} from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import {
|
||||
getFreeRoomPlace,
|
||||
type Invite,
|
||||
isOffice,
|
||||
type JoinRequest,
|
||||
LoveEvents,
|
||||
@@ -30,20 +27,13 @@ import {
|
||||
type Meeting,
|
||||
type MeetingMinutes,
|
||||
type MeetingSchedule,
|
||||
MeetingStatus,
|
||||
type Office,
|
||||
type ParticipantInfo,
|
||||
RequestStatus,
|
||||
type Room,
|
||||
RoomAccess,
|
||||
type RoomMetadata,
|
||||
RoomType,
|
||||
TranscriptionStatus
|
||||
} from '@hcengineering/love'
|
||||
import { getEmbeddedLabel, getMetadata, getResource, type IntlString } from '@hcengineering/platform'
|
||||
import presentation, {
|
||||
copyTextToClipboard,
|
||||
createQuery,
|
||||
type DocCreatePhase,
|
||||
getClient,
|
||||
type ObjectSearchResult
|
||||
@@ -60,9 +50,6 @@ import {
|
||||
type LocalTrack,
|
||||
type LocalTrackPublication,
|
||||
LocalVideoTrack,
|
||||
type RemoteParticipant,
|
||||
type RemoteTrack,
|
||||
type RemoteTrackPublication,
|
||||
type Room as LKRoom,
|
||||
RoomEvent,
|
||||
Track
|
||||
@@ -73,141 +60,26 @@ import { getPersonByPersonRef } from '@hcengineering/contact-resources'
|
||||
import MeetingMinutesSearchItem from './components/MeetingMinutesSearchItem.svelte'
|
||||
import RoomSettingsPopup from './components/RoomSettingsPopup.svelte'
|
||||
import love from './plugin'
|
||||
import { $myPreferences, currentMeetingMinutes, currentRoom, myOffice, selectedRoomPlace } from './stores'
|
||||
import { $myPreferences, currentMeetingMinutes, currentRoom } from './stores'
|
||||
import { getLiveKitClient } from './liveKitClient'
|
||||
|
||||
export async function getToken (
|
||||
roomName: string,
|
||||
roomId: Ref<Room>,
|
||||
userId: string,
|
||||
participantName: string
|
||||
): Promise<string> {
|
||||
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
|
||||
if (endpoint === undefined) {
|
||||
throw new Error('Love service endpoint not found')
|
||||
}
|
||||
const token = getPlatformToken()
|
||||
const res = await fetch(concatLink(endpoint, '/getToken'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName: getTokenRoomName(roomName, roomId), _id: userId, participantName })
|
||||
})
|
||||
return await res.text()
|
||||
}
|
||||
|
||||
function getTokenRoomName (roomName: string, roomId: Ref<Room>): string {
|
||||
const currentWorkspaceUuid = getMetadata(presentation.metadata.WorkspaceUuid)
|
||||
if (currentWorkspaceUuid === undefined) {
|
||||
throw new Error('Current workspace not found')
|
||||
}
|
||||
return `${currentWorkspaceUuid}_${roomName}_${roomId}`
|
||||
}
|
||||
import { getLoveClient } from './loveClient'
|
||||
import { getClient as getAccountClientRaw } from '@hcengineering/account-client'
|
||||
|
||||
export const liveKitClient = getLiveKitClient()
|
||||
export const lk: LKRoom = liveKitClient.liveKitRoom
|
||||
|
||||
export const loveClient = getLoveClient()
|
||||
|
||||
export function setCustomCreateScreenTracks (value: () => Promise<Array<LocalTrack<Track.Kind>>>): void {
|
||||
lk.localParticipant.createScreenTracks = value
|
||||
}
|
||||
|
||||
export const screenSharing = writable<boolean>(false)
|
||||
export const isRecording = writable<boolean>(false)
|
||||
export const isTranscription = writable<boolean>(false)
|
||||
export const isRecordingAvailable = writable<boolean>(false)
|
||||
export const isSharingEnabled = writable<boolean>(false)
|
||||
export const isFullScreen = writable<boolean>(false)
|
||||
export const isShareWithSound = writable<boolean>(false)
|
||||
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video && track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
function handleTrackUnsubscribed (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
): void {
|
||||
if (track.kind === Track.Kind.Video && track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.on(RoomEvent.LocalTrackPublished, (pub, part) => {
|
||||
const session = liveKitClient.currentMediaSession
|
||||
const track = pub.track?.mediaStreamTrack
|
||||
const deviceId = track?.getSettings().deviceId
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
session?.setFeature('sharing', { enabled: true, track, deviceId })
|
||||
screenSharing.set(true)
|
||||
isSharingEnabled.set(true)
|
||||
} else {
|
||||
session?.setCamera({ enabled: true, track, deviceId })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: true, track, deviceId })
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.LocalTrackUnpublished, (pub, part) => {
|
||||
const session = liveKitClient.currentMediaSession
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
session?.setFeature('sharing', { enabled: false })
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
} else {
|
||||
session?.setCamera({ enabled: false })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: false })
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
lk.on(RoomEvent.TrackMuted, (pub, participant) => {
|
||||
if (participant.isLocal) {
|
||||
const session = liveKitClient.currentMediaSession
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
session?.setFeature('sharing', { enabled: false })
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
} else {
|
||||
session?.setCamera({ enabled: false })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.TrackUnmuted, (pub, participant) => {
|
||||
if (participant.isLocal) {
|
||||
const session = liveKitClient.currentMediaSession
|
||||
const track = pub.track?.mediaStreamTrack
|
||||
const deviceId = track?.getSettings().deviceId
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
session?.setFeature('sharing', { enabled: true })
|
||||
screenSharing.set(true)
|
||||
isSharingEnabled.set(true)
|
||||
} else {
|
||||
session?.setCamera({ enabled: true, track, deviceId })
|
||||
}
|
||||
} else if (pub.track?.kind === Track.Kind.Audio) {
|
||||
session?.setMicrophone({ enabled: true, track, deviceId })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const krispProcessor = KrispNoiseFilter()
|
||||
export let blurProcessor: ProcessorWrapper<BackgroundOptions> | undefined
|
||||
let localVideo: LocalVideoTrack | undefined
|
||||
@@ -291,11 +163,6 @@ export async function updateBlurRadius (value: number): Promise<void> {
|
||||
}
|
||||
|
||||
lk.on(RoomEvent.LocalTrackPublished, (pub) => {
|
||||
if (pub.track?.kind === Track.Kind.Video && pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(true)
|
||||
isSharingEnabled.set(true)
|
||||
}
|
||||
|
||||
if (pub.source === Track.Source.Microphone) {
|
||||
void setKrispProcessor(pub)
|
||||
}
|
||||
@@ -305,14 +172,9 @@ lk.on(RoomEvent.LocalTrackPublished, (pub) => {
|
||||
}
|
||||
})
|
||||
lk.on(RoomEvent.LocalTrackUnpublished, (pub) => {
|
||||
if (pub.track?.kind === Track.Kind.Video) {
|
||||
if (pub.track.source === Track.Source.ScreenShare) {
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
} else if (pub.track.source === Track.Source.Camera) {
|
||||
if (localVideo !== undefined) {
|
||||
localVideo = undefined
|
||||
}
|
||||
if (pub.track?.kind === Track.Kind.Video && pub.track.source === Track.Source.Camera) {
|
||||
if (localVideo !== undefined) {
|
||||
localVideo = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -331,18 +193,10 @@ lk.on(RoomEvent.RoomMetadataChanged, (metadata) => {
|
||||
|
||||
lk.on(RoomEvent.Connected, () => {
|
||||
isRecording.set(lk.isRecording)
|
||||
void initRoom()
|
||||
void initRoomMetadata(lk.metadata)
|
||||
Analytics.handleEvent(LoveEvents.ConnectedToRoom)
|
||||
})
|
||||
|
||||
async function initRoom (): Promise<void> {
|
||||
const room = get(currentRoom)
|
||||
if (room !== undefined) {
|
||||
await initMeetingMinutes(room)
|
||||
}
|
||||
await initRoomMetadata(lk.metadata)
|
||||
}
|
||||
|
||||
async function initRoomMetadata (metadata: string | undefined): Promise<void> {
|
||||
const room = get(currentRoom)
|
||||
const data: RoomMetadata = parseMetadata(metadata)
|
||||
@@ -357,7 +211,7 @@ async function initRoomMetadata (metadata: string | undefined): Promise<void> {
|
||||
}
|
||||
|
||||
if (get(isRecordingAvailable) && data.recording == null && room?.startWithRecording === true && !get(isRecording)) {
|
||||
await record(room)
|
||||
await loveClient.record(room)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,32 +224,7 @@ function parseMetadata (metadata: string | undefined): RoomMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnect (): Promise<void> {
|
||||
await liveKitClient.disconnect()
|
||||
screenSharing.set(false)
|
||||
isSharingEnabled.set(false)
|
||||
}
|
||||
|
||||
export async function leaveRoom (ownInfo: ParticipantInfo | undefined, ownOffice: Office | undefined): Promise<void> {
|
||||
const me = lk.localParticipant
|
||||
try {
|
||||
await Promise.all([me.setScreenShareEnabled(false), me.setCameraEnabled(false), me.setMicrophoneEnabled(false)])
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
if (ownInfo !== undefined) {
|
||||
const client = getClient()
|
||||
if (ownOffice !== undefined && ownInfo.room !== ownOffice._id) {
|
||||
await client.update(ownInfo, { room: ownOffice._id, x: 0, y: 0 })
|
||||
} else if (ownOffice === undefined) {
|
||||
await client.update(ownInfo, { room: love.ids.Reception, x: 0, y: 0 })
|
||||
}
|
||||
}
|
||||
await disconnect()
|
||||
closeMeetingMinutes()
|
||||
}
|
||||
|
||||
function closeMeetingMinutes (): void {
|
||||
export function closeMeetingMinutes (): void {
|
||||
const loc = getCurrentLocation()
|
||||
|
||||
if (loc.path[2] === loveId) {
|
||||
@@ -410,20 +239,6 @@ function closeMeetingMinutes (): void {
|
||||
currentMeetingMinutes.set(undefined)
|
||||
}
|
||||
|
||||
function isRoomOpened (room: Room): boolean {
|
||||
const loc = getCurrentLocation()
|
||||
|
||||
if (loc.path[2] === loveId) {
|
||||
const panel = get(panelstore).panel
|
||||
const { _id } = panel ?? {}
|
||||
|
||||
if (_id !== undefined && room._id !== undefined && _id === room._id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function getRoomName (room: Room): Promise<string> {
|
||||
if (isOffice(room) && room.person !== null && room.name === '') {
|
||||
const employee = await getPersonByPersonRef(room.person)
|
||||
@@ -441,45 +256,8 @@ export async function getRoomLabel (room: Room): Promise<IntlString> {
|
||||
return isOffice(room) ? love.string.Office : love.string.Room
|
||||
}
|
||||
|
||||
async function moveToRoom (
|
||||
x: number,
|
||||
y: number,
|
||||
currentInfo: ParticipantInfo | undefined,
|
||||
currentPerson: Person,
|
||||
room: Room,
|
||||
sessionId: string | null
|
||||
): Promise<void> {
|
||||
const client = getClient()
|
||||
if (currentInfo !== undefined) {
|
||||
await client.diffUpdate(currentInfo, {
|
||||
x,
|
||||
y,
|
||||
room: room._id,
|
||||
sessionId
|
||||
})
|
||||
} else {
|
||||
await client.createDoc(love.class.ParticipantInfo, core.space.Workspace, {
|
||||
x,
|
||||
y,
|
||||
room: room._id,
|
||||
person: currentPerson._id,
|
||||
name: currentPerson.name,
|
||||
account: getCurrentAccount().uuid,
|
||||
sessionId
|
||||
})
|
||||
}
|
||||
|
||||
if (!isRoomOpened(room)) {
|
||||
await navigateToOfficeDoc(client.getHierarchy(), room)
|
||||
}
|
||||
}
|
||||
|
||||
async function connectLK (token: string, room: Room): Promise<void> {
|
||||
const wsURL = getLiveKitEndpoint()
|
||||
await liveKitClient.connect(wsURL, token, room.type === RoomType.Video)
|
||||
}
|
||||
|
||||
async function navigateToOfficeDoc (hierarchy: Hierarchy, object: Doc): Promise<void> {
|
||||
export async function navigateToOfficeDoc (object: Doc): Promise<void> {
|
||||
const hierarchy = getClient().getHierarchy()
|
||||
const panelComponent = hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectPanel)
|
||||
const comp = panelComponent?.component ?? view.component.EditDoc
|
||||
const loc = await getObjectLinkFragment(hierarchy, object, {}, comp)
|
||||
@@ -489,49 +267,9 @@ async function navigateToOfficeDoc (hierarchy: Hierarchy, object: Doc): Promise<
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
async function initMeetingMinutes (room: Room): Promise<void> {
|
||||
const client = getClient()
|
||||
const doc = await client.findOne(love.class.MeetingMinutes, {
|
||||
attachedTo: room._id,
|
||||
status: MeetingStatus.Active
|
||||
})
|
||||
|
||||
if (doc !== undefined) {
|
||||
currentMeetingMinutes.set(doc)
|
||||
const loc = getCurrentLocation()
|
||||
if (loc.path[2] === loveId || room.type === RoomType.Video) {
|
||||
await navigateToOfficeDoc(client.getHierarchy(), doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function connectRoom (
|
||||
x: number,
|
||||
y: number,
|
||||
currentInfo: ParticipantInfo | undefined,
|
||||
currentPerson: Person,
|
||||
room: Room
|
||||
): Promise<void> {
|
||||
liveKitClient.isConnecting = true
|
||||
await moveToRoom(x, y, currentInfo, currentPerson, room, getMetadata(presentation.metadata.SessionId) ?? null)
|
||||
selectedRoomPlace.set(undefined)
|
||||
if (getCurrentAccount().role === AccountRole.ReadOnlyGuest) {
|
||||
return
|
||||
}
|
||||
await disconnect()
|
||||
const token = await getToken(room.name, room._id, currentPerson._id, currentPerson.name)
|
||||
try {
|
||||
await connectLK(token, room)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
await leaveRoom(currentInfo, get(myOffice))
|
||||
}
|
||||
}
|
||||
|
||||
export const joinRequest: Ref<JoinRequest> | undefined = undefined
|
||||
const requestsQuery = createQuery(true)
|
||||
|
||||
export function calculateFloorSize (_rooms: Room[], preview?: boolean): number {
|
||||
export function calculateFloorSize (_rooms: Room[], _preview?: boolean): number {
|
||||
let fH: number = 5
|
||||
_rooms.forEach((room) => {
|
||||
if (room.y + room.height + 2 > fH) fH = room.y + room.height + 2
|
||||
@@ -539,177 +277,6 @@ export function calculateFloorSize (_rooms: Room[], preview?: boolean): number {
|
||||
return fH
|
||||
}
|
||||
|
||||
function checkPlace (room: Room, info: ParticipantInfo[], x: number, y: number): boolean {
|
||||
return !isOffice(room) && info.find((p) => p.x === x && p.y === y) === undefined
|
||||
}
|
||||
|
||||
export async function connectToMeeting (
|
||||
currentInfo: ParticipantInfo | undefined,
|
||||
info: ParticipantInfo[],
|
||||
currentRequests: JoinRequest[],
|
||||
currentInvites: Invite[],
|
||||
meetId: string
|
||||
): Promise<void> {
|
||||
const client = getClient()
|
||||
const meeting = await client.findOne(love.mixin.Meeting, { _id: meetId as Ref<Meeting> })
|
||||
if (meeting === undefined) return
|
||||
const room = await client.findOne(love.class.Room, { _id: meeting.room })
|
||||
if (room === undefined) return
|
||||
|
||||
// check time (it should be 10 minutes before the meeting or active in roomInfo)
|
||||
const now = new Date()
|
||||
const res = getAllEvents([meeting], now.setMinutes(now.getMinutes() - 10), new Date().getTime())
|
||||
if (res.length === 0) {
|
||||
console.log('Meeting is not active')
|
||||
return
|
||||
}
|
||||
|
||||
await tryConnect(
|
||||
currentInfo,
|
||||
room,
|
||||
info.filter((p) => p.room === room._id),
|
||||
currentRequests,
|
||||
currentInvites
|
||||
)
|
||||
}
|
||||
|
||||
export async function tryConnect (
|
||||
currentInfo: ParticipantInfo | undefined,
|
||||
room: Room,
|
||||
info: ParticipantInfo[],
|
||||
currentRequests: JoinRequest[],
|
||||
currentInvites: Invite[],
|
||||
place?: { x: number, y: number }
|
||||
): Promise<void> {
|
||||
if (liveKitClient.isConnecting) return
|
||||
const me = getCurrentEmployee()
|
||||
const currentPerson = await getPersonByPersonRef(me)
|
||||
if (currentPerson == null) return
|
||||
const client = getClient()
|
||||
|
||||
// guests can't join without invite
|
||||
if (!client.getHierarchy().hasMixin(currentPerson, contact.mixin.Employee)) return
|
||||
|
||||
if (room._id === currentInfo?.room) return
|
||||
if (room.access === RoomAccess.DND) return
|
||||
for (const req of currentRequests) {
|
||||
await client.remove(req)
|
||||
}
|
||||
if (place !== undefined && !checkPlace(room, info, place.x, place.y)) {
|
||||
place = undefined
|
||||
}
|
||||
if (place === undefined) {
|
||||
place = getFreeRoomPlace(room, info, me)
|
||||
}
|
||||
const x: number = place.x
|
||||
const y: number = place.y
|
||||
if (isOffice(room)) {
|
||||
if (room.person === null) return
|
||||
// we should check that office owner in office
|
||||
const owner = room.person
|
||||
if (owner === currentPerson._id) {
|
||||
// it's our office if it's empty let's disconnect
|
||||
if (info.length === 0) {
|
||||
await leaveRoom(currentInfo, room)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
const ownerInfo = info.find((p) => p.person === owner)
|
||||
if (ownerInfo?.room !== room._id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const invite of currentInvites) {
|
||||
await client.update(invite, { status: invite.room === room._id ? RequestStatus.Approved : RequestStatus.Rejected })
|
||||
}
|
||||
|
||||
const isGuest = getCurrentAccount().role === AccountRole.Guest
|
||||
if ((room.access === RoomAccess.Knock || isGuest) && (!isOffice(room) || room.person !== currentPerson._id)) {
|
||||
const _id = await client.createDoc(love.class.JoinRequest, core.space.Workspace, {
|
||||
person: currentPerson._id,
|
||||
room: room._id,
|
||||
status: RequestStatus.Pending
|
||||
})
|
||||
requestsQuery.query(love.class.JoinRequest, { person: me, _id }, (res) => {
|
||||
const req = res[0]
|
||||
if (req === undefined) return
|
||||
if (req.status === RequestStatus.Pending) return
|
||||
requestsQuery.unsubscribe()
|
||||
if (req.status === RequestStatus.Approved) {
|
||||
void connectRoom(x, y, currentInfo, currentPerson, room)
|
||||
}
|
||||
})
|
||||
// we should send request to room owner if it ouffice and all participants if not
|
||||
} else {
|
||||
await connectRoom(x, y, currentInfo, currentPerson, room)
|
||||
}
|
||||
}
|
||||
|
||||
export async function endMeeting (
|
||||
room: Office,
|
||||
rooms: Room[],
|
||||
infos: ParticipantInfo[],
|
||||
currentInfo: ParticipantInfo
|
||||
): Promise<void> {
|
||||
const roomInfos = infos.filter((p) => p.room === room._id && room.person !== p.person)
|
||||
for (const roomInfo of roomInfos) {
|
||||
await kick(roomInfo.person, rooms, infos)
|
||||
}
|
||||
await leaveRoom(currentInfo, room)
|
||||
}
|
||||
|
||||
export async function kick (person: Ref<Person>, rooms: Room[], infos: ParticipantInfo[]): Promise<void> {
|
||||
const personInfo = infos.find((p) => p.person === person)
|
||||
if (personInfo === undefined) return
|
||||
const personOffice = rooms.find((r) => isOffice(r) && r.person === personInfo.person)
|
||||
const client = getClient()
|
||||
await client.update(personInfo, { room: personOffice?._id ?? love.ids.Reception, x: 0, y: 0 })
|
||||
}
|
||||
|
||||
export async function invite (person: Ref<Person>, room: Ref<Room> | undefined): Promise<void> {
|
||||
if (room === undefined || room === love.ids.Reception) return
|
||||
const client = getClient()
|
||||
const me = getCurrentEmployee()
|
||||
await client.createDoc(love.class.Invite, core.space.Workspace, {
|
||||
target: person,
|
||||
room,
|
||||
status: RequestStatus.Pending,
|
||||
from: me
|
||||
})
|
||||
}
|
||||
|
||||
export async function record (room: Room): Promise<void> {
|
||||
try {
|
||||
const endpoint = getLoveEndpoint()
|
||||
const token = getPlatformToken()
|
||||
const roomName = getTokenRoomName(room.name, room._id)
|
||||
if (lk.isRecording) {
|
||||
await fetch(concatLink(endpoint, '/stopRecord'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName, room: room.name })
|
||||
})
|
||||
} else {
|
||||
await fetch(concatLink(endpoint, '/startRecord'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName, room: room.name, meetingMinutes: get(currentMeetingMinutes)?._id })
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkRecordAvailable (): Promise<void> {
|
||||
try {
|
||||
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
|
||||
@@ -736,7 +303,7 @@ export async function createMeeting (
|
||||
client: TxOperations,
|
||||
_id: Ref<Event>,
|
||||
space: Space,
|
||||
data: Data<Event>,
|
||||
_data: Data<Event>,
|
||||
store: Record<string, any>,
|
||||
phase: DocCreatePhase
|
||||
): Promise<void> {
|
||||
@@ -764,7 +331,7 @@ export async function createMeetingSchedule (
|
||||
client: TxOperations,
|
||||
_id: Ref<Schedule>,
|
||||
space: Space,
|
||||
data: Data<Schedule>,
|
||||
_data: Data<Schedule>,
|
||||
store: Record<string, any>,
|
||||
phase: DocCreatePhase
|
||||
): Promise<void> {
|
||||
@@ -792,15 +359,6 @@ export function getLiveKitEndpoint (): string {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
export function getLoveEndpoint (): string {
|
||||
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
|
||||
if (endpoint === undefined) {
|
||||
throw new Error('Love service endpoint not found')
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
|
||||
export function getPlatformToken (): string {
|
||||
const token = getMetadata(presentation.metadata.Token)
|
||||
if (token === undefined) {
|
||||
@@ -824,29 +382,6 @@ export async function stopTranscription (room: Room): Promise<void> {
|
||||
await disconnectMeeting(room._id)
|
||||
}
|
||||
|
||||
export async function updateSessionLanguage (room: Room): Promise<void> {
|
||||
const current = get(currentRoom)
|
||||
if (current === undefined || room._id !== current._id || !get(isTranscription)) return
|
||||
|
||||
try {
|
||||
const endpoint = getLoveEndpoint()
|
||||
const token = getPlatformToken()
|
||||
const roomName = getTokenRoomName(room.name, room._id)
|
||||
|
||||
await fetch(concatLink(endpoint, '/language'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName, room: room.name, language: room.language })
|
||||
})
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function showRoomSettings (room?: Room): Promise<void> {
|
||||
if (room === undefined) return
|
||||
|
||||
@@ -868,8 +403,14 @@ async function getRoomGuestLink (room: Room): Promise<string> {
|
||||
sessionId: roomInfo._id
|
||||
}
|
||||
|
||||
const func = await getResource(login.function.GetInviteLink)
|
||||
return await func(24, '', -1, AccountRole.Guest, encodeURIComponent(JSON.stringify(navigateUrl)))
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
const token = getMetadata(presentation.metadata.Token)
|
||||
|
||||
console.log('Create link')
|
||||
const accountClient = getAccountClientRaw(accountsUrl, token)
|
||||
return await accountClient.createAccessLink(AccountRole.Guest, {
|
||||
navigateUrl: encodeURIComponent(JSON.stringify(navigateUrl))
|
||||
})
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user