Scheduled meetings (#6206)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-07-31 20:33:57 +05:00 committed by GitHub
parent 31f6fcbcf8
commit 88e2df5885
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 512 additions and 97 deletions

View File

@ -46,6 +46,7 @@
"@hcengineering/love": "^0.6.0",
"@hcengineering/love-resources": "^0.6.0",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/model-notification": "^0.6.0"
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/model-calendar": "^0.6.0"
}
}

View File

@ -15,21 +15,13 @@
import contact, { type Employee, type Person } from '@hcengineering/contact'
import { AccountRole, DOMAIN_TRANSIENT, IndexKind, type Domain, type Ref } from '@hcengineering/core'
import { Index, Model, Prop, TypeRef, type Builder } from '@hcengineering/model'
import core, { TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import presentation from '@hcengineering/model-presentation'
import view, { createAction } from '@hcengineering/model-view'
import notification from '@hcengineering/notification'
import { getEmbeddedLabel } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import workbench from '@hcengineering/workbench'
import {
loveId,
type DevicesPreference,
type Floor,
type Invite,
type JoinRequest,
type Meeting,
type Office,
type ParticipantInfo,
type RequestStatus,
@ -38,6 +30,16 @@ import {
type RoomInfo,
type RoomType
} from '@hcengineering/love'
import { Index, Mixin, Model, Prop, TypeRef, type Builder } from '@hcengineering/model'
import calendar, { TEvent } from '@hcengineering/model-calendar'
import core, { TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import presentation from '@hcengineering/model-presentation'
import view, { createAction } from '@hcengineering/model-view'
import notification from '@hcengineering/notification'
import { getEmbeddedLabel } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import workbench from '@hcengineering/workbench'
import love from './plugin'
export { loveId } from '@hcengineering/love'
@ -127,10 +129,25 @@ export class TRoomInfo extends TDoc implements RoomInfo {
isOffice!: boolean
}
@Mixin(love.mixin.Meeting, calendar.class.Event)
export class TMeeting extends TEvent implements Meeting {
room!: Ref<Room>
}
export default love
export function createModel (builder: Builder): void {
builder.createModel(TRoom, TFloor, TOffice, TParticipantInfo, TJoinRequest, TDevicesPreference, TRoomInfo, TInvite)
builder.createModel(
TRoom,
TFloor,
TOffice,
TParticipantInfo,
TJoinRequest,
TDevicesPreference,
TRoomInfo,
TInvite,
TMeeting
)
builder.createDoc(
workbench.class.Application,
@ -151,6 +168,19 @@ export function createModel (builder: Builder): void {
component: love.component.WorkbenchExtension
})
builder.createDoc(presentation.class.DocCreateExtension, core.space.Model, {
ofClass: calendar.class.Event,
apply: love.function.CreateMeeting,
components: {
body: love.component.MeetingData
}
})
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: calendar.extensions.EditEventExtensions,
component: love.component.EditMeetingData
})
builder.createDoc(
setting.class.SettingsCategory,
core.space.Model,

View File

@ -7,7 +7,7 @@
export let manager: DocCreateExtensionManager
export let kind: CreateExtensionKind
export let props: Record<string, any> = {}
export let space: Space | undefined
export let space: Space | undefined = undefined
$: extensions = manager.extensions

View File

@ -13,10 +13,15 @@
// limitations under the License.
-->
<script lang="ts">
import { Calendar, RecurringRule, Visibility, generateEventId } from '@hcengineering/calendar'
import { Calendar, Event, ReccuringEvent, RecurringRule, Visibility, generateEventId } from '@hcengineering/calendar'
import { Person, PersonAccount } from '@hcengineering/contact'
import { Class, Doc, Markup, Ref, getCurrentAccount } from '@hcengineering/core'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import core, { Class, Doc, Markup, Ref, Space, generateId, getCurrentAccount } from '@hcengineering/core'
import presentation, {
createQuery,
DocCreateExtComponent,
DocCreateExtensionManager,
getClient
} from '@hcengineering/presentation'
import { EmptyMarkup } from '@hcengineering/text'
import { StyledTextBox } from '@hcengineering/text-editor-resources'
import {
@ -43,16 +48,21 @@
import ReccurancePopup from './ReccurancePopup.svelte'
import VisibilityEditor from './VisibilityEditor.svelte'
const currentUser = getCurrentAccount() as PersonAccount
export let attachedTo: Ref<Doc> = calendar.ids.NoAttached
export let attachedToClass: Ref<Class<Doc>> = calendar.class.Event
export let title: string = ''
export let date: Date | undefined = undefined
export let withTime = false
export let participants: Ref<Person>[] = [currentUser.person]
const now = new Date()
const defaultDuration = 60 * 60 * 1000
const allDayDuration = 24 * 60 * 60 * 1000 - 1
const docCreateManager = DocCreateExtensionManager.create(calendar.class.Event)
let startDate =
date === undefined ? now.getTime() : withTime ? date.getTime() : date.setHours(now.getHours(), now.getMinutes())
const duration = defaultDuration
@ -75,10 +85,14 @@
}
})
const spaceQ = createQuery()
let space: Space | undefined = undefined
spaceQ.query(core.class.Space, { _id: calendar.space.Calendar }, (res) => {
space = res[0]
})
let rules: RecurringRule[] = []
const currentUser = getCurrentAccount() as PersonAccount
let participants: Ref<Person>[] = [currentUser.person]
let externalParticipants: string[] = []
const dispatch = createEventDispatcher()
@ -93,6 +107,7 @@
if (startDate != null) date = startDate
if (date === undefined) return
if (title === '') return
const _id = generateId<Event>()
if (rules.length > 0) {
await client.addCollection(
calendar.class.ReccuringEvent,
@ -119,25 +134,37 @@
access: 'owner',
originalStartTime: allDay ? saveUTC(date) : date,
timeZone
}
},
_id as Ref<ReccuringEvent>
)
} else {
await client.addCollection(calendar.class.Event, calendar.space.Calendar, attachedTo, attachedToClass, 'events', {
calendar: _calendar,
eventId: generateEventId(),
date: allDay ? saveUTC(date) : date,
dueDate: allDay ? saveUTC(dueDate) : dueDate,
externalParticipants,
description,
visibility,
participants,
reminders,
title,
location,
allDay,
timeZone,
access: 'owner'
})
await client.addCollection(
calendar.class.Event,
calendar.space.Calendar,
attachedTo,
attachedToClass,
'events',
{
calendar: _calendar,
eventId: generateEventId(),
date: allDay ? saveUTC(date) : date,
dueDate: allDay ? saveUTC(dueDate) : dueDate,
externalParticipants,
description,
visibility,
participants,
reminders,
title,
location,
allDay,
timeZone,
access: 'owner'
},
_id
)
}
if (space !== undefined) {
await docCreateManager.commit(client, _id, space, {}, 'post')
}
dispatch('close')
}
@ -204,6 +231,9 @@
<LocationEditor focusIndex={10010} bind:value={location} />
<EventParticipants focusIndex={10011} bind:participants bind:externalParticipants />
</div>
<div class="block">
<DocCreateExtComponent manager={docCreateManager} kind={'body'} />
</div>
<div class="block row gap-1-5">
<div class="top-icon">
<Icon icon={calendar.icon.Description} size={'small'} />
@ -222,7 +252,7 @@
<CalendarSelector bind:value={_calendar} focusIndex={10101} />
<div class="flex-row-center flex-gap-1">
<Icon icon={calendar.icon.Hidden} size={'small'} />
<VisibilityEditor bind:value={visibility} kind={'tertiary'} focusIndex={10102} withoutIcon />
<VisibilityEditor bind:value={visibility} kind={'tertiary'} size={'small'} focusIndex={10102} withoutIcon />
</div>
<EventReminders bind:reminders focusIndex={10103} />
</div>

View File

@ -16,7 +16,7 @@
import { Event, ReccuringEvent, ReccuringInstance, RecurringRule } from '@hcengineering/calendar'
import { Person } from '@hcengineering/contact'
import { DocumentUpdate, Ref } from '@hcengineering/core'
import presentation, { getClient } from '@hcengineering/presentation'
import presentation, { ComponentExtensions, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor-resources'
import {
Button,
@ -199,6 +199,7 @@
<div class="block rightCropPadding">
<LocationEditor bind:value={location} focusIndex={10005} />
<EventParticipants bind:participants bind:externalParticipants disabled={readOnly} focusIndex={10006} />
<ComponentExtensions extension={calendar.extensions.EditEventExtensions} props={{ readOnly, value: object }} />
</div>
<div class="block row gap-1-5">
<div class="top-icon">

View File

@ -49,6 +49,8 @@
<Button
label={reminders.length > 0 ? calendar.string.AddReminder : calendar.string.Reminders}
{disabled}
padding={'0 .5rem'}
justify={'left'}
kind={'ghost'}
{focusIndex}
on:click={(e) => {

View File

@ -17,7 +17,7 @@ import { NotificationType } from '@hcengineering/notification'
import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Handler, IntegrationType } from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
import { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
/**
* @public
@ -212,6 +212,9 @@ const calendarPlugin = plugin(calendarId, {
metadata: {
CalendarServiceURL: '' as Metadata<string>
},
extensions: {
EditEventExtensions: '' as ComponentExtensionId
},
ids: {
ReminderNotification: '' as Ref<NotificationType>,
NoAttached: '' as Ref<Event>

View File

@ -521,7 +521,7 @@ export async function getInviteLinkId (
): Promise<string> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
const exp = expHours * 1000 * 60 * 60
const exp = expHours < 0 ? -1 : expHours * 1000 * 60 * 60
if (accountsUrl === undefined) {
throw new Error('accounts url not specified')

View File

@ -59,6 +59,7 @@
"FullscreenMode": "Full-screen mode",
"ExitingFullscreenMode": "Exiting fullscreen mode",
"Select": "Select",
"ChooseShare": "Choose what to share"
"ChooseShare": "Choose what to share",
"CreateMeeting": "Create meeting"
}
}

View File

@ -59,6 +59,7 @@
"FullscreenMode": "Modo de pantalla completa",
"ExitingFullscreenMode": "Salir del modo de pantalla completa",
"Select": "Seleccionar",
"ChooseShare": "Elija qué compartir"
"ChooseShare": "Elija qué compartir",
"CreateMeeting": "Crear reunión"
}
}

View File

@ -59,6 +59,7 @@
"FullscreenMode": "Mode plein écran",
"ExitingFullscreenMode": "Quitter le mode plein écran",
"Select": "Sélectionner",
"ChooseShare": "Choisissez ce que vous voulez partager"
"ChooseShare": "Choisissez ce que vous voulez partager",
"CreateMeeting": "Créer une réunion"
}
}

View File

@ -59,6 +59,7 @@
"FullscreenMode": "Modo de ecrã inteiro",
"ExitingFullscreenMode": "Saindo do modo de tela cheia",
"Select": "Seleccione",
"ChooseShare": "Escolha o que partilhar"
"ChooseShare": "Escolha o que partilhar",
"CreateMeeting": "Criar reunião"
}
}

View File

@ -59,6 +59,7 @@
"FullscreenMode": "Полноэкранный режим",
"ExitingFullscreenMode": "Выход из полноэкранного режима",
"Select": "Выбрать",
"ChooseShare": "Выберите, чем вы хотите поделиться"
"ChooseShare": "Выберите, чем вы хотите поделиться",
"CreateMeeting": "Создать встречу"
}
}

View File

@ -59,6 +59,7 @@
"FullscreenMode": "全屏模式",
"ExitingFullscreenMode": "退出全屏模式",
"Select": "选择",
"ChooseShare": "选择共享内容"
"ChooseShare": "选择共享内容",
"CreateMeeting": "创建会议"
}
}

View File

@ -40,6 +40,7 @@
"@hcengineering/platform": "^0.6.11",
"svelte": "^4.2.12",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/calendar": "^0.6.24",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/view-resources": "^0.6.0",

View File

@ -122,10 +122,11 @@
if (roomInfo !== undefined) {
const navigateUrl = getCurrentLocation()
navigateUrl.query = {
meetId: roomInfo._id
sessionId: roomInfo._id
}
const func = await getResource(login.function.GetInviteLink)
return await func(24 * 30, '', -1, AccountRole.Guest, JSON.stringify(navigateUrl))
return await func(24, '', -1, AccountRole.Guest, encodeURIComponent(JSON.stringify(navigateUrl)))
}
return ''
}

View File

@ -0,0 +1,37 @@
<!--
// 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 { Event } from '@hcengineering/calendar'
import love from '../plugin'
import RoomSelector from './RoomSelector.svelte'
import { getClient } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import { Room } from '@hcengineering/love'
export let value: Event
const client = getClient()
$: isMeeting = client.getHierarchy().hasMixin(value, love.mixin.Meeting)
$: meeting = isMeeting ? client.getHierarchy().as(value, love.mixin.Meeting) : null
async function changeRoom (val: Ref<Room>): Promise<void> {
await client.updateMixin(value._id, value._class, value.space, love.mixin.Meeting, { room: val })
}
</script>
{#if isMeeting && meeting}
<RoomSelector value={meeting?.room} on:change={(ev) => changeRoom(ev.detail)} />
{/if}

View File

@ -16,12 +16,12 @@
import { Contact, Person } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import love, { Floor as FloorType, Office, Room, RoomInfo, isOffice } from '@hcengineering/love'
import love, { Floor as FloorType, Meeting, Office, Room, RoomInfo, isOffice } from '@hcengineering/love'
import { getClient } from '@hcengineering/presentation'
import { deviceOptionsStore as deviceInfo, getCurrentLocation, navigate } from '@hcengineering/ui'
import { onMount, onDestroy } from 'svelte'
import { activeFloor, floors, infos, invites, myInfo, myRequests, rooms } from '../stores'
import { tryConnect } from '../utils'
import { connectToMeeting, tryConnect } from '../utils'
import Floor from './Floor.svelte'
import FloorConfigure from './FloorConfigure.svelte'
import Floors from './Floors.svelte'
@ -42,25 +42,31 @@
$: $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
tryConnect(
$personByIdStore,
$myInfo,
room,
$infos.filter((p) => p.room === room._id),
$myRequests,
$invites
)
}
onMount(async () => {
const loc = getCurrentLocation()
const { meetId, ...query } = loc.query ?? {}
if (meetId != null) {
loc.query = Object.keys(query).length === 0 ? undefined : query
navigate(loc, true)
const client = getClient()
const info = await client.findOne(love.class.RoomInfo, { _id: meetId as Ref<RoomInfo> })
if (info === undefined) return
const room = $rooms.find((p) => p._id === info.room)
if (room === undefined) return
tryConnect(
$personByIdStore,
$myInfo,
room,
$infos.filter((p) => p.room === room._id),
$myRequests,
$invites
)
const { sessionId, meetId, ...query } = loc.query ?? {}
loc.query = Object.keys(query).length === 0 ? undefined : query
navigate(loc, true)
if (sessionId != null) {
await connectToSession(sessionId)
} else if (meetId != null) {
await connectToMeeting($personByIdStore, $myInfo, $infos, $myRequests, $invites, $rooms, meetId)
}
})
</script>

View File

@ -0,0 +1,56 @@
<!--
// 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 { Ref } from '@hcengineering/core'
import { Room } from '@hcengineering/love'
import { Button, CheckBox } from '@hcengineering/ui'
import { Writable } from 'svelte/store'
import love from '../plugin'
import RoomSelector from './RoomSelector.svelte'
export let state: Writable<Record<string, any>>
function changeRoom (val: Ref<Room>) {
$state.room = val
}
function changeIsMeeting () {
$state.isMeeting = isMeeting
}
let isMeeting = false
</script>
<div class="flex-row-center gap-1-5 mt-1">
<CheckBox bind:checked={isMeeting} kind={'primary'} on:value={changeIsMeeting} />
<Button
label={love.string.CreateMeeting}
kind={'ghost'}
padding={'0 .5rem'}
justify={'left'}
on:click={() => {
isMeeting = !isMeeting
changeIsMeeting()
}}
/>
</div>
{#if isMeeting}
<RoomSelector
value={$state.room}
on:change={(ev) => {
changeRoom(ev.detail)
}}
/>
{/if}

View File

@ -0,0 +1,64 @@
<!--
// 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 { Ref } from '@hcengineering/core'
import love, { isOffice, Room } from '@hcengineering/love'
import { Dropdown, Icon } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { rooms } from '../stores'
export let value: Ref<Room> | undefined
export let disabled: boolean = false
export let focusIndex = -1
const dispatch = createEventDispatcher()
$: items = $rooms
.filter((p) => !isOffice(p))
.map((p) => {
return {
_id: p._id,
label: p.name
}
})
$: selected = value !== undefined ? items.find((p) => p._id === value) : undefined
function change (id: Ref<Room>) {
if (value !== id) {
dispatch('change', id)
value = id
}
}
</script>
{#if items.length > 0}
<div class="flex-row-center flex-gap-1">
<Icon icon={love.icon.Mic} size={'small'} />
<Dropdown
kind={'ghost'}
size={'medium'}
placeholder={love.string.Room}
{items}
withSearch={false}
{selected}
{disabled}
{focusIndex}
on:selected={(e) => {
change(e.detail._id)
}}
/>
</div>
{/if}

View File

@ -1,10 +1,12 @@
import { type Resources } from '@hcengineering/platform'
import ControlExt from './components/ControlExt.svelte'
import EditMeetingData from './components/EditMeetingData.svelte'
import Main from './components/Main.svelte'
import MeetingData from './components/MeetingData.svelte'
import SelectScreenSourcePopup from './components/SelectScreenSourcePopup.svelte'
import Settings from './components/Settings.svelte'
import WorkbenchExtension from './components/WorkbenchExtension.svelte'
import SelectScreenSourcePopup from './components/SelectScreenSourcePopup.svelte'
import { toggleMic, toggleVideo } from './utils'
import { createMeeting, toggleMic, toggleVideo } from './utils'
export { setCustomCreateScreenTracks } from './utils'
@ -14,7 +16,12 @@ export default async (): Promise<Resources> => ({
ControlExt,
Settings,
WorkbenchExtension,
SelectScreenSourcePopup
SelectScreenSourcePopup,
MeetingData,
EditMeetingData
},
function: {
CreateMeeting: createMeeting
},
actionImpl: {
ToggleMic: toggleMic,

View File

@ -13,15 +13,22 @@
// limitations under the License.
//
import { mergeIds, type IntlString } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui'
import love, { loveId } from '@hcengineering/love'
import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform'
import { type DocCreateFunction } from '@hcengineering/presentation'
import { type AnyComponent } from '@hcengineering/ui'
export default mergeIds(loveId, love, {
component: {
ControlExt: '' as AnyComponent
ControlExt: '' as AnyComponent,
MeetingData: '' as AnyComponent,
EditMeetingData: '' as AnyComponent
},
function: {
CreateMeeting: '' as Resource<DocCreateFunction>
},
string: {
CreateMeeting: '' as IntlString,
LeaveRoom: '' as IntlString,
LeaveRoomConfirmation: '' as IntlString,
Mute: '' as IntlString,

View File

@ -1,6 +1,17 @@
import { Analytics } from '@hcengineering/analytics'
import calendar, { type Event } from '@hcengineering/calendar'
import contact, { getName, type Person, type PersonAccount } from '@hcengineering/contact'
import core, { concatLink, getCurrentAccount, type IdMap, type Ref, type Space } from '@hcengineering/core'
import core, {
AccountRole,
concatLink,
getCurrentAccount,
type Data,
type IdMap,
type Ref,
type Space,
type TxOperations
} from '@hcengineering/core'
import login from '@hcengineering/login'
import {
RequestStatus,
RoomAccess,
@ -9,12 +20,13 @@ import {
loveId,
type Invite,
type JoinRequest,
type Meeting,
type Office,
type ParticipantInfo,
type Room
} from '@hcengineering/love'
import { getEmbeddedLabel, getMetadata, type IntlString } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { getEmbeddedLabel, getMetadata, getResource, type IntlString } from '@hcengineering/platform'
import presentation, { createQuery, getClient, type DocCreatePhase } from '@hcengineering/presentation'
import { getCurrentLocation, navigate, type DropdownTextItem } from '@hcengineering/ui'
import { KrispNoiseFilter, isKrispNoiseFilterSupported } from '@livekit/krisp-noise-filter'
import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors'
@ -555,6 +567,33 @@ function checkPlace (room: Room, info: ParticipantInfo[], x: number, y: number):
return !isOffice(room) && info.find((p) => p.x === x && p.y === y) === undefined
}
export async function connectToMeeting (
personByIdStore: IdMap<Person>,
currentInfo: ParticipantInfo | undefined,
info: ParticipantInfo[],
currentRequests: JoinRequest[],
currentInvites: Invite[],
rooms: Room[],
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 = rooms.find((p) => p._id === meeting.room)
if (room === undefined) return
// check time (it should be 10 minutes before the meeting or active in roomInfo)
await tryConnect(
personByIdStore,
currentInfo,
room,
info.filter((p) => p.room === room._id),
currentRequests,
currentInvites
)
}
export async function tryConnect (
personByIdStore: IdMap<Person>,
currentInfo: ParticipantInfo | undefined,
@ -720,3 +759,27 @@ async function checkRecordAvailable (): Promise<void> {
}
void checkRecordAvailable()
export async function createMeeting (
client: TxOperations,
_id: Ref<Event>,
space: Space,
data: Data<Event>,
store: Record<string, any>,
phase: DocCreatePhase
): Promise<void> {
if (phase === 'post' && store.room != null && store.isMeeting === true) {
await client.createMixin<Event, Meeting>(_id, calendar.class.Event, space._id, love.mixin.Meeting, {
room: store.room as Ref<Room>
})
const event = await client.findOne(calendar.class.Event, { _id })
if (event === undefined) return
const navigateUrl = getCurrentLocation()
navigateUrl.query = {
meetId: _id
}
const func = await getResource(login.function.GetInviteLink)
const link = await func(-1, '', -1, AccountRole.Guest, encodeURIComponent(JSON.stringify(navigateUrl)))
await client.update(event, { location: link })
}
}

View File

@ -43,6 +43,7 @@
"@hcengineering/preference": "^0.6.13",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/calendar": "^0.6.24",
"@hcengineering/drive": "^0.6.0",
"@hcengineering/core": "^0.6.32",
"@hcengineering/view": "^0.6.13"

View File

@ -1,5 +1,6 @@
import { Event } from '@hcengineering/calendar'
import { Person } from '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import { Class, Doc, Mixin, Ref } from '@hcengineering/core'
import { Drive } from '@hcengineering/drive'
import { NotificationType } from '@hcengineering/notification'
import { Asset, IntlString, Metadata, Plugin, plugin } from '@hcengineering/platform'
@ -58,6 +59,10 @@ export interface RoomInfo extends Doc {
isOffice: boolean
}
export interface Meeting extends Event {
room: Ref<Room>
}
export enum RequestStatus {
Pending,
Approved,
@ -97,6 +102,9 @@ const love = plugin(loveId, {
RoomInfo: '' as Ref<Class<RoomInfo>>,
Invite: '' as Ref<Class<Invite>>
},
mixin: {
Meeting: '' as Ref<Mixin<Meeting>>
},
action: {
ToggleMic: '' as Ref<Action>,
ToggleVideo: '' as Ref<Action>

View File

@ -24,7 +24,6 @@ import contact, {
Person,
PersonAccount
} from '@hcengineering/contact'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import core, {
AccountRole,
BaseWorkspaceInfo,
@ -40,30 +39,17 @@ import core, {
Ref,
roleOrder,
systemAccountEmail,
Timestamp,
Tx,
TxOperations,
Version,
versionToString,
WorkspaceId,
Timestamp,
WorkspaceIdWithUrl,
type Branding
} from '@hcengineering/core'
import { consoleModelLogger, MigrateOperation, ModelLogger } from '@hcengineering/model'
import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform'
import { decodeToken, generateToken } from '@hcengineering/server-token'
import toolPlugin, {
connect,
initializeWorkspace,
initModel,
updateModel,
prepareTools,
upgradeModel
} from '@hcengineering/server-tool'
import { pbkdf2Sync, randomBytes } from 'crypto'
import { Binary, Db, Filter, ObjectId, type MongoClient } from 'mongodb'
import fetch from 'node-fetch'
import otpGenerator from 'otp-generator'
import {
DummyFullTextAdapter,
Pipeline,
@ -78,6 +64,20 @@ import {
registerServerPlugins,
registerStringLoaders
} from '@hcengineering/server-pipeline'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import { decodeToken, generateToken } from '@hcengineering/server-token'
import toolPlugin, {
connect,
initializeWorkspace,
initModel,
prepareTools,
updateModel,
upgradeModel
} from '@hcengineering/server-tool'
import { pbkdf2Sync, randomBytes } from 'crypto'
import { Binary, Db, Filter, ObjectId, type MongoClient } from 'mongodb'
import fetch from 'node-fetch'
import otpGenerator from 'otp-generator'
import { accountPlugin } from './plugin'
@ -692,7 +692,7 @@ export async function checkInvite (ctx: MeasureContext, invite: Invite | null, e
Analytics.handleError(new Error(`no invite or invite limit exceed ${email}`))
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
if (invite.exp < Date.now()) {
if (invite.exp !== -1 && invite.exp < Date.now()) {
ctx.error('invite', { email, state: 'link expired' })
Analytics.handleError(new Error(`invite link expired ${invite._id.toString()} ${email}`))
throw new PlatformError(new Status(Severity.ERROR, platform.status.ExpiredLink, {}))
@ -1577,7 +1577,7 @@ export async function getInviteLink (
ctx.info('Getting invite link', { workspace: workspace.name, emailMask, limit })
const data: Omit<Invite, '_id'> = {
workspace,
exp: Date.now() + exp,
exp: exp < 0 ? -1 : Date.now() + exp,
emailMask,
limit,
role: role ?? AccountRole.User

View File

@ -27,7 +27,10 @@ import core, {
AttachedData,
Client,
Data,
Doc,
DocData,
DocumentUpdate,
Mixin,
Ref,
TxOperations,
TxUpdateDoc,
@ -42,10 +45,10 @@ import type { Collection, Db } from 'mongodb'
import { encode64 } from './base64'
import { CalendarController } from './calendarController'
import config from './config'
import { RateLimiter } from './rateLimiter'
import type { CalendarHistory, EventHistory, EventWatch, ProjectCredentials, State, Token, User, Watch } from './types'
import { encodeReccuring, isToken, parseRecurrenceStrings } from './utils'
import type { WorkspaceClient } from './workspaceClient'
import { RateLimiter } from './rateLimiter'
const SCOPES = [
'https://www.googleapis.com/auth/calendar.calendars.readonly',
@ -703,7 +706,7 @@ export class CalendarClient {
}
const data: Partial<AttachedData<Event>> = await this.parseUpdateData(event)
if (event.recurringEventId != null) {
const diff = this.getEventDiff<ReccuringInstance>(
const diff = this.getDiff<ReccuringInstance>(
{
...data,
recurringEventId: event.recurringEventId as Ref<ReccuringEvent>,
@ -719,7 +722,7 @@ export class CalendarClient {
} else {
if (event.recurrence != null) {
const parseRule = parseRecurrenceStrings(event.recurrence)
const diff = this.getEventDiff<ReccuringEvent>(
const diff = this.getDiff<ReccuringEvent>(
{
...data,
rules: parseRule.rules,
@ -733,13 +736,70 @@ export class CalendarClient {
await this.client.update(current, diff)
}
} else {
const diff = this.getEventDiff(data, current)
const diff = this.getDiff(data, current)
if (Object.keys(diff).length > 0) {
console.log('UPDATE EVENT DIFF', JSON.stringify(diff), JSON.stringify(current))
await this.client.update(current, diff)
}
}
}
await this.updateMixins(event, current)
}
private async updateMixins (event: calendar_v3.Schema$Event, current: Event): Promise<void> {
const mixins = this.parseMixins(event)
if (mixins !== undefined) {
for (const mixin in mixins) {
const attr = mixins[mixin]
if (typeof attr === 'object' && Object.keys(attr).length > 0) {
if (this.client.getHierarchy().hasMixin(current, mixin as Ref<Mixin<Doc>>)) {
const diff = this.getDiff(attr, this.client.getHierarchy().as(current, mixin as Ref<Mixin<Doc>>))
if (Object.keys(diff).length > 0) {
await this.client.updateMixin(
current._id,
current._class,
calendar.space.Calendar,
mixin as Ref<Mixin<Doc>>,
diff
)
}
} else {
await this.client.createMixin(
current._id,
current._class,
calendar.space.Calendar,
mixin as Ref<Mixin<Doc>>,
attr
)
}
}
}
}
}
private parseMixins (event: calendar_v3.Schema$Event): Record<string, any> | undefined {
if (event.extendedProperties?.shared?.mixins !== undefined) {
const mixins = JSON.parse(event.extendedProperties.shared.mixins)
return mixins
}
}
private async saveMixins (event: calendar_v3.Schema$Event, _id: Ref<Event>): Promise<void> {
const mixins = this.parseMixins(event)
if (mixins !== undefined) {
for (const mixin in mixins) {
const attr = mixins[mixin]
if (typeof attr === 'object' && Object.keys(attr).length > 0) {
await this.client.createMixin(
_id,
calendar.class.Event,
calendar.space.Calendar,
mixin as Ref<Mixin<Doc>>,
attr
)
}
}
}
}
private async saveExtEvent (
@ -767,6 +827,7 @@ export class CalendarClient {
timeZone: event.start?.timeZone ?? event.end?.timeZone ?? 'Etc/GMT'
}
)
await this.saveMixins(event, id)
console.log('SAVE INSTANCE', id, JSON.stringify(event))
} else if (event.status !== 'cancelled') {
if (event.recurrence != null) {
@ -786,6 +847,7 @@ export class CalendarClient {
timeZone: event.start?.timeZone ?? event.end?.timeZone ?? 'Etc/GMT'
}
)
await this.saveMixins(event, id)
console.log('SAVE REC EVENT', id, JSON.stringify(event))
} else {
const id = await this.client.addCollection(
@ -796,12 +858,13 @@ export class CalendarClient {
'events',
data
)
await this.saveMixins(event, id)
console.log('SAVE EVENT', id, JSON.stringify(event))
}
}
}
private getEventDiff<T extends Event>(data: Partial<AttachedData<T>>, current: T): Partial<AttachedData<T>> {
private getDiff<T extends Doc>(data: Partial<DocData<T>>, current: T): Partial<DocData<T>> {
const res = {}
for (const key in data) {
if (!deepEqual((data as any)[key], (current as any)[key])) {
@ -1120,6 +1183,24 @@ export class CalendarClient {
return false
}
private getMixinFields (event: Event): Record<string, any> {
const res = {}
const h = this.client.getHierarchy()
for (const [k, v] of Object.entries(event)) {
if (typeof v === 'object' && h.isMixin(k as Ref<Mixin<Doc>>)) {
for (const [key, value] of Object.entries(v)) {
if (value !== undefined) {
const obj = (res as any)[k] ?? {}
obj[key] = value
;(res as any)[k] = obj
}
}
}
}
return res
}
private convertBody (event: Event): calendar_v3.Schema$Event {
const res: calendar_v3.Schema$Event = {
start: convertDate(event.date, event.allDay, getTimezone(event)),
@ -1141,6 +1222,16 @@ export class CalendarClient {
}
}
}
const mixin = this.getMixinFields(event)
if (Object.keys(mixin).length > 0) {
res.extendedProperties = {
...res.extendedProperties,
shared: {
...res.extendedProperties?.shared,
mixin: JSON.stringify(mixin)
}
}
}
if (event.reminders !== undefined) {
res.reminders = {
useDefault: false,