mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-21 15:59:15 +00:00
524 lines
16 KiB
Svelte
524 lines
16 KiB
Svelte
<!--
|
|
// Copyright © 2024 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 { formatName } from '@hcengineering/contact'
|
|
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
|
|
import { IdMap, Ref, toIdMap } from '@hcengineering/core'
|
|
import {
|
|
Floor,
|
|
Invite,
|
|
isOffice,
|
|
JoinRequest,
|
|
loveId,
|
|
Office,
|
|
ParticipantInfo,
|
|
RequestStatus,
|
|
Room,
|
|
RoomType
|
|
} from '@hcengineering/love'
|
|
import { getEmbeddedLabel } from '@hcengineering/platform'
|
|
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
|
|
import {
|
|
closePopup,
|
|
eventToHTMLElement,
|
|
Location,
|
|
location,
|
|
PopupResult,
|
|
showPopup,
|
|
tooltip
|
|
} from '@hcengineering/ui'
|
|
import view from '@hcengineering/view'
|
|
import { onDestroy } from 'svelte'
|
|
import workbench from '@hcengineering/workbench'
|
|
import {
|
|
closeWidget,
|
|
minimizeSidebar,
|
|
openWidget,
|
|
sidebarStore,
|
|
SidebarVariant
|
|
} from '@hcengineering/workbench-resources'
|
|
|
|
import love from '../plugin'
|
|
import {
|
|
activeFloor,
|
|
activeInvites,
|
|
currentRoom,
|
|
floors,
|
|
infos,
|
|
myInfo,
|
|
myInvites,
|
|
myOffice,
|
|
myRequests,
|
|
rooms
|
|
} from '../stores'
|
|
import {
|
|
disconnect,
|
|
getRoomName,
|
|
isCameraEnabled,
|
|
isConnected,
|
|
isCurrentInstanceConnected,
|
|
isMicEnabled,
|
|
isSharingEnabled,
|
|
leaveRoom,
|
|
screenSharing,
|
|
setCam,
|
|
setMic,
|
|
setShare
|
|
} from '../utils'
|
|
import ActiveInvitesPopup from './ActiveInvitesPopup.svelte'
|
|
import CamSettingPopup from './CamSettingPopup.svelte'
|
|
import FloorPopup from './FloorPopup.svelte'
|
|
import InvitePopup from './InvitePopup.svelte'
|
|
import MicSettingPopup from './MicSettingPopup.svelte'
|
|
import PersonActionPopup from './PersonActionPopup.svelte'
|
|
import RequestPopup from './RequestPopup.svelte'
|
|
import RequestingPopup from './RequestingPopup.svelte'
|
|
import RoomPopup from './RoomPopup.svelte'
|
|
|
|
let allowCam: boolean = false
|
|
let allowLeave: boolean = false
|
|
|
|
$: allowCam = $currentRoom?.type === RoomType.Video
|
|
$: allowLeave = $myInfo !== undefined && $myInfo.room !== ($myOffice?._id ?? love.ids.Reception)
|
|
|
|
async function changeMute (): Promise<void> {
|
|
if (!$isConnected || $currentRoom?.type === RoomType.Reception) return
|
|
await setMic(!$isMicEnabled)
|
|
}
|
|
|
|
async function changeCam (): Promise<void> {
|
|
if (!$isConnected || !allowCam) return
|
|
await setCam(!$isCameraEnabled)
|
|
}
|
|
|
|
async function changeShare (): Promise<void> {
|
|
if (!$isConnected) return
|
|
await setShare(!$isSharingEnabled)
|
|
}
|
|
|
|
async function leave (): Promise<void> {
|
|
showPopup(MessageBox, {
|
|
label: love.string.LeaveRoom,
|
|
message: love.string.LeaveRoomConfirmation,
|
|
action: async () => {
|
|
await leaveRoom($myInfo, $myOffice)
|
|
}
|
|
})
|
|
}
|
|
|
|
interface ActiveRoom extends Room {
|
|
participants: ParticipantInfo[]
|
|
}
|
|
|
|
function getActiveRooms (rooms: Room[], infos: ParticipantInfo[]): ActiveRoom[] {
|
|
const roomMap = toIdMap(rooms)
|
|
const map: IdMap<ActiveRoom> = new Map()
|
|
for (const info of infos) {
|
|
if (info.room === love.ids.Reception) continue
|
|
const room = roomMap.get(info.room)
|
|
if (room === undefined) continue
|
|
// temprory disabled check for floor
|
|
// if (room.floor !== selectedFloor?._id) continue
|
|
const _id = room._id as Ref<ActiveRoom>
|
|
const active = map.get(_id) ?? { ...room, _id, participants: [] }
|
|
active.participants.push(info)
|
|
map.set(_id, active)
|
|
}
|
|
const arr = Array.from(map.values()).filter(
|
|
(r) => !isOffice(r) || r.participants.length > 1 || r.person !== r.participants[0]?.person
|
|
)
|
|
return arr
|
|
}
|
|
|
|
let selectedFloor: Floor | undefined = $floors.find((f) => f._id === $activeFloor)
|
|
$: selectedFloor = $floors.find((f) => f._id === $activeFloor)
|
|
|
|
$: activeRooms = getActiveRooms($rooms, $infos)
|
|
|
|
function selectFloor (): void {
|
|
showPopup(FloorPopup, { selectedFloor }, myOfficeElement, (res) => {
|
|
if (res === undefined) return
|
|
selectedFloor = $floors.find((p) => p._id === res)
|
|
})
|
|
}
|
|
|
|
const query = createQuery()
|
|
let requests: JoinRequest[] = []
|
|
query.query(love.class.JoinRequest, { status: RequestStatus.Pending }, (res) => {
|
|
requests = res
|
|
})
|
|
|
|
let activeRequest: JoinRequest | undefined = undefined
|
|
const joinRequestCategory = 'joinRequest'
|
|
function checkRequests (requests: JoinRequest[], $myInfo: ParticipantInfo | undefined): void {
|
|
if (activeRequest !== undefined) {
|
|
// try to find active request, if it not exists close popup
|
|
if (requests.find((r) => r._id === activeRequest?._id && r.room === $myInfo?.room) === undefined) {
|
|
closePopup(joinRequestCategory)
|
|
activeRequest = undefined
|
|
}
|
|
}
|
|
if (activeRequest === undefined) {
|
|
activeRequest = requests.find((r) => r.room === $myInfo?.room)
|
|
if (activeRequest !== undefined) {
|
|
showPopup(RequestPopup, { request: activeRequest }, myOfficeElement, undefined, undefined, {
|
|
category: joinRequestCategory,
|
|
overlay: false,
|
|
fixed: true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const myJoinRequestCategory = 'MyJoinRequest'
|
|
let myRequestsPopup: PopupResult | undefined = undefined
|
|
|
|
function checkMyRequests (requests: JoinRequest[]): void {
|
|
if (requests.length > 0) {
|
|
if (myRequestsPopup === undefined) {
|
|
myRequestsPopup = showPopup(RequestingPopup, { request: requests[0] }, myOfficeElement, undefined, undefined, {
|
|
category: myJoinRequestCategory,
|
|
overlay: false,
|
|
fixed: true
|
|
})
|
|
}
|
|
} else if (myRequestsPopup !== undefined) {
|
|
myRequestsPopup.close()
|
|
myRequestsPopup = undefined
|
|
}
|
|
}
|
|
|
|
$: checkMyRequests($myRequests)
|
|
|
|
let myOfficeElement: HTMLDivElement
|
|
|
|
$: checkRequests(requests, $myInfo)
|
|
|
|
function openRoom (ev: MouseEvent, room: Room): void {
|
|
showPopup(RoomPopup, { room }, ev.currentTarget as HTMLElement)
|
|
}
|
|
|
|
let activeInvite: Invite | undefined = undefined
|
|
const inviteCategory = 'inviteReq'
|
|
function checkInvites (invites: Invite[]): void {
|
|
if (activeInvite !== undefined) {
|
|
// try to find active request, if it not exists close popup
|
|
if (invites.find((r) => r._id === activeInvite?._id) === undefined) {
|
|
closePopup(inviteCategory)
|
|
activeInvite = undefined
|
|
}
|
|
}
|
|
if (activeInvite === undefined) {
|
|
activeInvite = invites[0]
|
|
if (activeInvite !== undefined) {
|
|
showPopup(InvitePopup, { invite: activeInvite }, myOfficeElement, undefined, undefined, {
|
|
category: inviteCategory,
|
|
overlay: false,
|
|
fixed: true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
$: checkInvites($myInvites)
|
|
|
|
async function checkOwnRoomConnection (
|
|
infos: ParticipantInfo[],
|
|
myInfo: ParticipantInfo | undefined,
|
|
myOffice: Office | undefined,
|
|
isConnected: boolean
|
|
): Promise<void> {
|
|
if (myOffice !== undefined) {
|
|
if (myInfo !== undefined && myInfo.room === myOffice._id) {
|
|
const filtered = infos.filter((p) => p.room === myOffice._id && p.person !== myInfo.person)
|
|
if (filtered.length === 0) {
|
|
if (isConnected) {
|
|
await disconnect()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$: checkOwnRoomConnection($infos, $myInfo, $myOffice, $isCurrentInstanceConnected)
|
|
|
|
const myInvitesCategory = 'myInvites'
|
|
|
|
let myInvitesPopup: PopupResult | undefined = undefined
|
|
|
|
function checkActiveInvites (invites: Invite[]): void {
|
|
if (invites.length > 0) {
|
|
if (myInvitesPopup === undefined) {
|
|
myInvitesPopup = showPopup(ActiveInvitesPopup, { invites }, myOfficeElement, undefined, undefined, {
|
|
category: myInvitesCategory,
|
|
overlay: false,
|
|
fixed: true
|
|
})
|
|
}
|
|
} else if (myInvitesPopup !== undefined) {
|
|
myInvitesPopup.close()
|
|
myInvitesPopup = undefined
|
|
}
|
|
}
|
|
|
|
$: reception = $rooms.find((f) => f._id === love.ids.Reception)
|
|
|
|
$: receptionParticipants = $infos.filter((p) => p.room === love.ids.Reception)
|
|
|
|
$: checkActiveInvites($activeInvites)
|
|
|
|
function micSettings (e: MouseEvent): void {
|
|
e.preventDefault()
|
|
showPopup(MicSettingPopup, {}, eventToHTMLElement(e))
|
|
}
|
|
|
|
function camSettings (e: MouseEvent): void {
|
|
e.preventDefault()
|
|
showPopup(CamSettingPopup, {}, eventToHTMLElement(e))
|
|
}
|
|
|
|
$: isVideoWidgetOpened = $sidebarStore.widgetsState.has(love.ids.VideoWidget)
|
|
|
|
$: if (
|
|
isVideoWidgetOpened &&
|
|
$sidebarStore.widget === undefined &&
|
|
$location.path[2] !== loveId &&
|
|
$sidebarStore.widgetsState.get(love.ids.VideoWidget)?.closedByUser !== true
|
|
) {
|
|
sidebarStore.update((s) => ({ ...s, widget: love.ids.VideoWidget, variant: SidebarVariant.EXPANDED }))
|
|
}
|
|
|
|
function checkActiveVideo (loc: Location, video: boolean, room: Ref<Room> | undefined): void {
|
|
const isOpened = $sidebarStore.widgetsState.has(love.ids.VideoWidget)
|
|
|
|
if (room === undefined) {
|
|
if (isOpened) {
|
|
closeWidget(love.ids.VideoWidget)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (video) {
|
|
if (!isOpened) {
|
|
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: love.ids.VideoWidget })[0]
|
|
if (widget === undefined) return
|
|
openWidget(
|
|
widget,
|
|
{
|
|
room
|
|
},
|
|
loc.path[2] !== loveId
|
|
)
|
|
}
|
|
|
|
if (loc.path[2] === loveId && $sidebarStore.widget === love.ids.VideoWidget) {
|
|
minimizeSidebar()
|
|
}
|
|
} else {
|
|
closeWidget(love.ids.VideoWidget)
|
|
}
|
|
}
|
|
|
|
$: checkActiveVideo(
|
|
$location,
|
|
$isCurrentInstanceConnected && ($currentRoom?.type === RoomType.Video || ($screenSharing && !isSharingEnabled)),
|
|
$currentRoom?._id
|
|
)
|
|
|
|
$: joined = activeRooms.filter((r) => $myInfo?.room === r._id)
|
|
|
|
onDestroy(() => {
|
|
closePopup(myInvitesCategory)
|
|
closePopup(inviteCategory)
|
|
closePopup(joinRequestCategory)
|
|
closePopup(myJoinRequestCategory)
|
|
closeWidget(love.ids.VideoWidget)
|
|
})
|
|
|
|
const client = getClient()
|
|
|
|
const camKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleVideo })?.[0]?.keyBinding
|
|
const micKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleMic })?.[0]?.keyBinding
|
|
|
|
function participantClickHandler (e: MouseEvent, participant: ParticipantInfo): void {
|
|
if ($myInfo !== undefined) {
|
|
showPopup(PersonActionPopup, { room: reception, person: participant.person }, eventToHTMLElement(e))
|
|
}
|
|
}
|
|
|
|
function getParticipantClickHandler (participant: ParticipantInfo): (e: MouseEvent) => void {
|
|
return (e: MouseEvent) => {
|
|
participantClickHandler(e, participant)
|
|
}
|
|
}
|
|
|
|
onDestroy(() => {
|
|
removeEventListener('beforeunload', beforeUnloadListener)
|
|
})
|
|
|
|
const beforeUnloadListener = () => {
|
|
if ($myInfo !== undefined && $isCurrentInstanceConnected) {
|
|
leaveRoom($myInfo, $myOffice)
|
|
}
|
|
}
|
|
|
|
window.addEventListener('beforeunload', beforeUnloadListener)
|
|
</script>
|
|
|
|
<div class="flex-row-center flex-gap-2">
|
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
<!-- <div class="container main flex-row-center flex-gap-2" bind:this={myOfficeElement} on:click={selectFloor}>-->
|
|
<!-- {#if selectedFloor}-->
|
|
<!-- <Label label={love.string.Floor} />-->
|
|
<!-- <span class="label overflow-label">-->
|
|
<!-- {selectedFloor?.name}-->
|
|
<!-- </span>-->
|
|
<!-- {/if}-->
|
|
<!-- <ActionIcon-->
|
|
<!-- icon={!$isConnected ? love.icon.Mic : $isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}-->
|
|
<!-- label={$isMicEnabled ? love.string.Mute : love.string.UnMute}-->
|
|
<!-- size={'small'}-->
|
|
<!-- action={changeMute}-->
|
|
<!-- on:contextmenu={micSettings}-->
|
|
<!-- disabled={!$isConnected}-->
|
|
<!-- keys={micKeys}-->
|
|
<!-- />-->
|
|
<!-- <ActionIcon-->
|
|
<!-- icon={!$isConnected || !allowCam-->
|
|
<!-- ? love.icon.Cam-->
|
|
<!-- : $isCameraEnabled-->
|
|
<!-- ? love.icon.CamEnabled-->
|
|
<!-- : love.icon.CamDisabled}-->
|
|
<!-- label={$isCameraEnabled ? love.string.StopVideo : love.string.StartVideo}-->
|
|
<!-- size={'small'}-->
|
|
<!-- action={changeCam}-->
|
|
<!-- on:contextmenu={camSettings}-->
|
|
<!-- disabled={!$isConnected || !allowCam}-->
|
|
<!-- keys={camKeys}-->
|
|
<!-- />-->
|
|
<!-- {#if $isConnected}-->
|
|
<!-- <ActionIcon-->
|
|
<!-- icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}-->
|
|
<!-- label={$isSharingEnabled ? love.string.StopShare : love.string.Share}-->
|
|
<!-- disabled={$screenSharing && !$isSharingEnabled}-->
|
|
<!-- size={'small'}-->
|
|
<!-- action={changeShare}-->
|
|
<!-- />-->
|
|
<!-- {/if}-->
|
|
<!-- {#if allowLeave}-->
|
|
<!-- <ActionIcon-->
|
|
<!-- icon={love.icon.LeaveRoom}-->
|
|
<!-- iconProps={{ color: '#FF6711' }}-->
|
|
<!-- label={love.string.LeaveRoom}-->
|
|
<!-- size={'small'}-->
|
|
<!-- action={leave}-->
|
|
<!-- />-->
|
|
<!-- {/if}-->
|
|
<!-- </div>-->
|
|
{#if activeRooms.length > 0}
|
|
<!-- <div class="divider" />-->
|
|
{#each activeRooms as active}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
<div
|
|
class="container flex-row-center"
|
|
class:active={joined.find((r) => r._id === active._id)}
|
|
on:click={(ev) => {
|
|
openRoom(ev, active)
|
|
}}
|
|
>
|
|
<div class="mr-2 overflow-label">{getRoomName(active, $personByIdStore)}</div>
|
|
<div class="flex-row-center avatars">
|
|
{#each active.participants as participant}
|
|
<div use:tooltip={{ label: getEmbeddedLabel(formatName(participant.name)) }}>
|
|
<Avatar name={participant.name} size={'card'} person={$personByIdStore.get(participant.person)} />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
{#if reception && receptionParticipants.length > 0}
|
|
{#if activeRooms.length > 0}
|
|
<div class="divider" />
|
|
{/if}
|
|
<div class="container flex-row-center flex-gap-2">
|
|
<div>{getRoomName(reception, $personByIdStore)}</div>
|
|
<div class="flex-row-center avatars">
|
|
{#each receptionParticipants as participant (participant._id)}
|
|
<div
|
|
use:tooltip={{ label: getEmbeddedLabel(formatName(participant.name)) }}
|
|
on:click={getParticipantClickHandler(participant)}
|
|
>
|
|
<Avatar name={participant.name} size={'card'} person={$personByIdStore.get(participant.person)} />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
.container {
|
|
padding: 0.125rem 0.125rem 0.125rem 0.5rem;
|
|
height: 1.625rem;
|
|
font-weight: 500;
|
|
background-color: var(--theme-button-pressed);
|
|
border: 1px solid transparent;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
|
|
.label {
|
|
font-weight: 700;
|
|
color: var(--theme-caption-color);
|
|
}
|
|
&.main {
|
|
order: -3;
|
|
padding-right: 0.25rem;
|
|
|
|
& + .divider {
|
|
order: -2;
|
|
}
|
|
}
|
|
&:hover {
|
|
background-color: var(--theme-button-hovered);
|
|
border-color: var(--theme-navpanel-divider);
|
|
}
|
|
&.active {
|
|
order: -1;
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.125rem 0.125rem 0.125rem 0.5rem;
|
|
background-color: var(--highlight-select);
|
|
border-color: var(--highlight-select-border);
|
|
|
|
&:hover {
|
|
background-color: var(--highlight-select-hover);
|
|
}
|
|
}
|
|
}
|
|
|
|
.avatars {
|
|
gap: 0.125rem;
|
|
}
|
|
|
|
.divider {
|
|
height: 1.5rem;
|
|
border: 1px solid var(--theme-divider-color);
|
|
}
|
|
</style>
|