diff --git a/models/love/package.json b/models/love/package.json index 75fc487f56..de93ddff59 100644 --- a/models/love/package.json +++ b/models/love/package.json @@ -28,6 +28,7 @@ "typescript": "^5.3.3" }, "dependencies": { + "@hcengineering/attachment": "^0.6.14", "@hcengineering/activity": "^0.6.0", "@hcengineering/chunter": "^0.6.20", "@hcengineering/contact": "^0.6.24", diff --git a/models/love/src/index.ts b/models/love/src/index.ts index abd60ac349..c468e07fc8 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -14,7 +14,14 @@ // import contact, { type Employee, type Person } from '@hcengineering/contact' -import { AccountRole, type Domain, DOMAIN_TRANSIENT, IndexKind, type Ref } from '@hcengineering/core' +import { + AccountRole, + type Domain, + DOMAIN_TRANSIENT, + IndexKind, + type Ref, + type CollaborativeDoc +} from '@hcengineering/core' import { type DevicesPreference, type Floor, @@ -34,18 +41,21 @@ import { } from '@hcengineering/love' import { type Builder, + Collection, Collection as PropCollection, Hidden, Index, Mixin, Model, Prop, + ReadOnly, + TypeCollaborativeDoc, TypeRef, TypeString, UX } from '@hcengineering/model' import calendar, { TEvent } from '@hcengineering/model-calendar' -import core, { TDoc } from '@hcengineering/model-core' +import core, { TAttachedDoc, 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' @@ -55,22 +65,32 @@ import setting from '@hcengineering/setting' import workbench, { WidgetType } from '@hcengineering/workbench' import activity from '@hcengineering/activity' import chunter from '@hcengineering/chunter' +import attachment from '@hcengineering/attachment' import love from './plugin' export { loveId } from '@hcengineering/love' export * from './migration' export const DOMAIN_LOVE = 'love' as Domain +export const DOMAIN_MEETING_MINUTES = 'meeting-minutes' as Domain @Model(love.class.Room, core.class.Doc, DOMAIN_LOVE) +@UX(love.string.Room, love.icon.Love) export class TRoom extends TDoc implements Room { - name!: string + @Prop(TypeString(), core.string.Name) + @Index(IndexKind.FullText) + name!: string + + @Prop(TypeCollaborativeDoc(), core.string.Description) + @Index(IndexKind.FullText) + description!: CollaborativeDoc type!: RoomType access!: RoomAccess @Prop(TypeRef(love.class.Floor), love.string.Floor) + @ReadOnly() // @Index(IndexKind.Indexed) floor!: Ref @@ -81,12 +101,20 @@ export class TRoom extends TDoc implements Room { language!: RoomLanguage startWithTranscription!: boolean + + @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) + attachments?: number + + @Prop(PropCollection(love.class.MeetingMinutes), love.string.MeetingMinutes) + meetings?: number } @Model(love.class.Office, love.class.Room) +@UX(love.string.Office, love.icon.Love) export class TOffice extends TRoom implements Office { @Prop(TypeRef(contact.mixin.Employee), contact.string.Employee) @Index(IndexKind.Indexed) + @ReadOnly() person!: Ref | null } @@ -155,22 +183,27 @@ export class TMeeting extends TEvent implements Meeting { room!: Ref } -@Model(love.class.MeetingMinutes, core.class.Doc, DOMAIN_LOVE) -@UX(love.string.Meeting) -export class TMeetingMinutes extends TDoc implements MeetingMinutes { +@Model(love.class.MeetingMinutes, core.class.Doc, DOMAIN_MEETING_MINUTES) +@UX(love.string.MeetingMinutes, love.icon.Cam) +export class TMeetingMinutes extends TAttachedDoc implements MeetingMinutes { @Hidden() sid!: string @Prop(TypeString(), view.string.Title) + @Index(IndexKind.FullText) title!: string - @Prop(TypeRef(love.class.Room), love.string.Room) - room!: Ref + @Prop(TypeCollaborativeDoc(), core.string.Description) + @Index(IndexKind.FullText) + description!: CollaborativeDoc - @Prop(PropCollection(activity.class.ActivityMessage), love.string.Transcription) + @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) + attachments?: number + + @Prop(PropCollection(chunter.class.ChatMessage), love.string.Transcription) transcription?: number - @Prop(PropCollection(activity.class.ActivityMessage), activity.string.Messages) + @Prop(PropCollection(chunter.class.ChatMessage), activity.string.Messages) messages?: number } @@ -383,15 +416,150 @@ export function createModel (builder: Builder): void { } }) + builder.createDoc(activity.class.ActivityExtension, core.space.Model, { + ofClass: love.class.Room, + components: { input: chunter.component.ChatMessageInput } + }) + + builder.createDoc(activity.class.ActivityExtension, core.space.Model, { + ofClass: love.class.Office, + components: { input: chunter.component.ChatMessageInput } + }) + builder.createDoc(activity.class.ActivityExtension, core.space.Model, { ofClass: love.class.MeetingMinutes, components: { input: chunter.component.ChatMessageInput } }) + builder.mixin(love.class.MeetingMinutes, core.class.Class, activity.mixin.ActivityDoc, {}) + + builder.mixin(love.class.Room, core.class.Class, activity.mixin.ActivityDoc, {}) + builder.mixin(love.class.MeetingMinutes, core.class.Class, view.mixin.ObjectPresenter, { presenter: love.component.MeetingMinutesPresenter }) + builder.mixin(love.class.MeetingMinutes, core.class.Class, view.mixin.CollectionEditor, { + editor: love.component.MeetingMinutesSection + }) + + builder.mixin(love.class.MeetingMinutes, core.class.Class, view.mixin.ObjectTitle, { + titleProvider: love.function.MeetingMinutesTitleProvider + }) + + builder.mixin(love.class.Room, core.class.Class, view.mixin.ObjectEditor, { + editor: love.component.EditRoom + }) + + builder.mixin(love.class.MeetingMinutes, core.class.Class, view.mixin.ObjectEditor, { + editor: love.component.EditMeetingMinutes + }) + + builder.mixin(love.class.Floor, core.class.Class, view.mixin.AttributeEditor, { + inlineEditor: love.component.FloorAttributePresenter + }) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: love.class.MeetingMinutes, + descriptor: view.viewlet.Table, + config: [ + '', + { key: 'messages', displayProps: { key: 'messages', suffix: true } }, + { key: 'transcription', displayProps: { key: 'transcription', suffix: true } }, + 'modifiedOn', + 'modifiedBy' + ], + configOptions: { + hiddenKeys: ['description'], + sortable: true + }, + options: {} + }, + love.viewlet.TableMeetingMinutes + ) + + builder.createDoc( + view.class.ViewletDescriptor, + core.space.Model, + { + label: love.string.Floor, + icon: love.icon.Love, + component: love.component.FloorView + }, + love.viewlet.FloorDescriptor + ) + + builder.createDoc( + view.class.ViewletDescriptor, + core.space.Model, + { + label: love.string.MeetingMinutes, + icon: view.icon.Table, + component: love.component.MeetingMinutesTable + }, + love.viewlet.MeetingMinutesDescriptor + ) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: love.class.Floor, + descriptor: love.viewlet.FloorDescriptor, + config: [] + }, + love.viewlet.Floor + ) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: love.class.Floor, + descriptor: love.viewlet.MeetingMinutesDescriptor, + config: [] + }, + love.viewlet.FloorMeetingMinutes + ) + + builder.createDoc( + notification.class.NotificationType, + core.space.Model, + { + label: chunter.string.Chat, + generated: false, + hidden: false, + txClasses: [core.class.TxCreateDoc], + objectClass: chunter.class.ChatMessage, + attachedToClass: love.class.MeetingMinutes, + txMatch: { + 'attributes.collection': 'messages' + }, + defaultEnabled: false, + group: love.ids.LoveNotificationGroup + }, + love.ids.MeetingMinutesChatNotification + ) + + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: [love.ids.MeetingMinutesChatNotification] + }) + + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.PushNotificationProvider, + ignoredTypes: [], + enabledTypes: [love.ids.MeetingMinutesChatNotification] + }) + + builder.mixin(love.class.MeetingMinutes, core.class.Class, notification.mixin.ClassCollaborators, { + fields: ['createdBy'] + }) + builder.mixin(love.class.Room, core.class.Class, core.mixin.IndexConfiguration, { indexes: [], searchDisabled: true diff --git a/models/love/src/migration.ts b/models/love/src/migration.ts index cdef35dfd5..779aa01052 100644 --- a/models/love/src/migration.ts +++ b/models/love/src/migration.ts @@ -14,9 +14,9 @@ // import contact from '@hcengineering/contact' -import { type Space, TxOperations, type Ref } from '@hcengineering/core' +import { type Space, TxOperations, type Ref, makeCollaborativeDoc } from '@hcengineering/core' import drive from '@hcengineering/drive' -import { RoomAccess, RoomType, createDefaultRooms, isOffice, loveId, type Floor } from '@hcengineering/love' +import { RoomAccess, RoomType, createDefaultRooms, isOffice, loveId, type Floor, type Room } from '@hcengineering/love' import { createDefaultSpace, migrateSpace, @@ -28,7 +28,7 @@ import { } from '@hcengineering/model' import core from '@hcengineering/model-core' import love from './plugin' -import { DOMAIN_LOVE } from '.' +import { DOMAIN_LOVE, DOMAIN_MEETING_MINUTES } from '.' async function createDefaultFloor (tx: TxOperations): Promise { const current = await tx.findOne(love.class.Floor, { @@ -56,7 +56,7 @@ async function createRooms (client: MigrationUpgradeClient): Promise { const data = createDefaultRooms(employees.map((p) => p._id)) for (const room of data) { const _class = isOffice(room) ? love.class.Office : love.class.Room - await tx.createDoc(_class, core.space.Workspace, room) + await tx.createDoc(_class, core.space.Workspace, room, room._id) } } @@ -79,7 +79,8 @@ async function createReception (client: MigrationUpgradeClient): Promise { x: 0, y: 0, language: 'en', - startWithTranscription: false + startWithTranscription: false, + description: makeCollaborativeDoc(love.ids.Reception, 'description') }, love.ids.Reception ) @@ -109,15 +110,38 @@ export const loveOperation: MigrateOperation = { ) await client.update( DOMAIN_LOVE, - { _class: love.class.Room, startWithTranscription: { $exists: false } }, + { _class: love.class.Room, type: RoomType.Video, startWithTranscription: { $exists: false } }, { startWithTranscription: true } ) + await client.update( + DOMAIN_LOVE, + { _class: love.class.Room, startWithTranscription: { $exists: false } }, + { startWithTranscription: false } + ) await client.update( DOMAIN_LOVE, { _class: love.class.Office, startWithTranscription: { $exists: false } }, { startWithTranscription: false } ) } + }, + { + state: 'move-meeting-minutes', + func: async (client) => { + await client.move(DOMAIN_LOVE, { _class: love.class.MeetingMinutes }, DOMAIN_MEETING_MINUTES) + } + }, + { + state: 'create-description-collaborative', + func: async (client) => { + const rooms = await client.find(DOMAIN_LOVE, { _class: { $in: [love.class.Room, love.class.Office] } }) + for (const room of rooms) { + const description = room.description + if (description == null) { + await client.update(DOMAIN_LOVE, room, { description: makeCollaborativeDoc(room._id, 'description') }) + } + } + } } ]) }, diff --git a/models/love/src/plugin.ts b/models/love/src/plugin.ts index f64f01ee9f..c7a5c088cf 100644 --- a/models/love/src/plugin.ts +++ b/models/love/src/plugin.ts @@ -13,9 +13,9 @@ // limitations under the License. // -import { type Doc, type Ref } from '@hcengineering/core' -import { type NotificationGroup } from '@hcengineering/notification' -import { mergeIds } from '@hcengineering/platform' +import { type Client, type Doc, type Ref } from '@hcengineering/core' +import { type NotificationType, type NotificationGroup } from '@hcengineering/notification' +import { type Resource, mergeIds } from '@hcengineering/platform' import { type AnyComponent } from '@hcengineering/ui' import { type ActionCategory, type ViewAction } from '@hcengineering/view' import { loveId } from '@hcengineering/love' @@ -43,6 +43,10 @@ export default mergeIds(loveId, love, { }, ids: { Settings: '' as Ref, - LoveNotificationGroup: '' as Ref + LoveNotificationGroup: '' as Ref, + MeetingMinutesChatNotification: '' as Ref + }, + function: { + MeetingMinutesTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise> } }) diff --git a/models/server-love/package.json b/models/server-love/package.json index 957bcc708c..dcc2d80d68 100644 --- a/models/server-love/package.json +++ b/models/server-love/package.json @@ -27,14 +27,15 @@ "typescript": "^5.3.3" }, "dependencies": { - "@hcengineering/core": "^0.6.32", "@hcengineering/contact": "^0.6.24", + "@hcengineering/core": "^0.6.32", + "@hcengineering/love": "^0.6.0", "@hcengineering/model": "^0.6.11", - "@hcengineering/platform": "^0.6.11", - "@hcengineering/server-core": "^0.6.1", "@hcengineering/model-core": "^0.6.0", "@hcengineering/model-love": "^0.6.0", - "@hcengineering/love": "^0.6.0", - "@hcengineering/server-love": "^0.6.0" + "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-love": "^0.6.0", + "@hcengineering/server-notification": "^0.6.1" } } diff --git a/models/server-love/src/index.ts b/models/server-love/src/index.ts index 384d288d98..ca252ccb46 100644 --- a/models/server-love/src/index.ts +++ b/models/server-love/src/index.ts @@ -19,10 +19,19 @@ import { type Builder } from '@hcengineering/model' import serverCore from '@hcengineering/server-core' import love from '@hcengineering/love' import serverLove from '@hcengineering/server-love' +import serverNotification from '@hcengineering/server-notification' export { serverLoveId } from '@hcengineering/server-love' export function createModel (builder: Builder): void { + builder.mixin(love.class.MeetingMinutes, core.class.Class, serverNotification.mixin.HTMLPresenter, { + presenter: serverLove.function.MeetingMinutesHTMLPresenter + }) + + builder.mixin(love.class.MeetingMinutes, core.class.Class, serverNotification.mixin.TextPresenter, { + presenter: serverLove.function.MeetingMinutesTextPresenter + }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverLove.trigger.OnEmployee, txMatch: { diff --git a/packages/ui/src/popups.ts b/packages/ui/src/popups.ts index d679c18598..8d475609fc 100644 --- a/packages/ui/src/popups.ts +++ b/packages/ui/src/popups.ts @@ -371,6 +371,12 @@ export function fitPopupElement ( newProps.left = '1px' newProps.right = '1px' show = true + } else if (element === 'full-centered') { + newProps.top = '20px' + newProps.bottom = '20px' + newProps.left = '20px' + newProps.right = '20px' + show = true } else if (element === 'content' && contentPanel !== undefined) { const rect = contentPanel.getBoundingClientRect() newProps.top = `${rect.top}px` diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 10e2f5b4cd..6293ffc0a7 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -231,7 +231,8 @@ export const posAlignment = [ 'centered', 'center', 'status', - 'movable' + 'movable', + 'full-centered' ] as const export type PopupPosAlignment = (typeof posAlignment)[number] diff --git a/plugins/attachment-resources/src/components/PreviewWidget.svelte b/plugins/attachment-resources/src/components/PreviewWidget.svelte index 66f4353caf..721d4820b0 100644 --- a/plugins/attachment-resources/src/components/PreviewWidget.svelte +++ b/plugins/attachment-resources/src/components/PreviewWidget.svelte @@ -15,24 +15,36 @@
@@ -26,9 +26,11 @@
- - + {#if label} + + + {/if}
+ + {#if popup && fullScreen} + + {/if} + + diff --git a/plugins/love-resources/src/components/ControlBarContainer.svelte b/plugins/love-resources/src/components/ControlBarContainer.svelte new file mode 100644 index 0000000000..bca46de7a5 --- /dev/null +++ b/plugins/love-resources/src/components/ControlBarContainer.svelte @@ -0,0 +1,84 @@ + + + +
+
+ +
+
+ +
+ +
+
+ +
+ + diff --git a/plugins/love-resources/src/components/ControlExt.svelte b/plugins/love-resources/src/components/ControlExt.svelte index 34437e2107..a3827a8b44 100644 --- a/plugins/love-resources/src/components/ControlExt.svelte +++ b/plugins/love-resources/src/components/ControlExt.svelte @@ -20,7 +20,6 @@ Invite, isOffice, JoinRequest, - loveId, Office, ParticipantInfo, RequestStatus, @@ -40,14 +39,7 @@ } from '@hcengineering/ui' import { onDestroy } from 'svelte' import workbench from '@hcengineering/workbench' - import { - closeWidget, - closeWidgetTab, - minimizeSidebar, - sidebarStore, - SidebarVariant, - updateWidgetState - } from '@hcengineering/workbench-resources' + import { closeWidget, closeWidgetTab, sidebarStore } from '@hcengineering/workbench-resources' import love from '../plugin' import { activeInvites, currentRoom, infos, myInfo, myInvites, myOffice, myRequests, rooms } from '../stores' @@ -69,37 +61,6 @@ const client = getClient() - // 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 { - // if (!$isConnected || $currentRoom?.type === RoomType.Reception) return - // await setMic(!$isMicEnabled) - // } - // - // async function changeCam (): Promise { - // if (!$isConnected || !allowCam) return - // await setCam(!$isCameraEnabled) - // } - // - // async function changeShare (): Promise { - // if (!$isConnected) return - // await setShare(!$isSharingEnabled) - // } - // - // async function leave (): Promise { - // showPopup(MessageBox, { - // label: love.string.LeaveRoom, - // message: love.string.LeaveRoomConfirmation, - // action: async () => { - // await leaveRoom($myInfo, $myOffice) - // } - // }) - // } - interface ActiveRoom extends Room { participants: ParticipantInfo[] } @@ -124,18 +85,8 @@ 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) => { @@ -261,32 +212,6 @@ $: checkActiveInvites($activeInvites) - // function micSettings (e: MouseEvent): void { - // e.preventDefault() - // showPopup(MicSettingPopup, {}, eventToHTMLElement(e)) - // } - // - // function camSettings (e: MouseEvent): void { - // e.preventDefault() - // showPopup(CamSettingPopup, {}, eventToHTMLElement(e)) - // } - - let prevLocation: Location = $location - $: isMeetingWidgetOpened = $sidebarStore.widgetsState.has(love.ids.MeetingWidget) - - $: widgetState = $sidebarStore.widgetsState.get(love.ids.MeetingWidget) - $: if ( - isMeetingWidgetOpened && - $sidebarStore.widget === undefined && - $location.path[2] !== loveId && - widgetState !== undefined && - widgetState.closedByUser !== true && - widgetState.tabs.some(({ id }) => id === 'video') - ) { - sidebarStore.update((s) => ({ ...s, widget: love.ids.MeetingWidget, variant: SidebarVariant.EXPANDED })) - updateWidgetState(love.ids.MeetingWidget, { openedByUser: false, tab: 'video' }) - } - function checkActiveVideo (loc: Location, video: boolean, room: Ref | undefined): void { const meetingWidgetState = $sidebarStore.widgetsState.get(love.ids.MeetingWidget) const isMeetingWidgetCreated = meetingWidgetState !== undefined @@ -302,44 +227,18 @@ const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: love.ids.MeetingWidget })[0] if (widget === undefined) return - // Create widget in sidebar if not created if (!isMeetingWidgetCreated) { - prevLocation = loc - createMeetingWidget(widget, room, loc, video) + createMeetingWidget(widget, room, video) } else if (video && !meetingWidgetState.tabs.some(({ id }) => id === 'video')) { - createMeetingVideoWidgetTab(widget, loc) + createMeetingVideoWidgetTab(widget) } else if (!video && meetingWidgetState.tabs.some(({ id }) => id === 'video')) { void closeWidgetTab(widget, 'video') } - - // Show video in sidebar when leave office - if ( - $sidebarStore.widget === love.ids.MeetingWidget && - prevLocation.path[2] === loveId && - loc.path[2] !== loveId && - widgetState !== undefined && - widgetState.tabs.some(({ id }) => id === 'video') - ) { - updateWidgetState(love.ids.MeetingWidget, { openedByUser: false, tab: 'video' }) - } - - // Hide video in sidebar when open office app - if ( - loc.path[2] === loveId && - prevLocation.path[2] !== loveId && - $sidebarStore.widget === love.ids.MeetingWidget && - widgetState !== undefined && - widgetState.tab === 'video' - ) { - minimizeSidebar() - } } else { if (isMeetingWidgetCreated) { closeWidget(love.ids.MeetingWidget) } } - - prevLocation = loc } $: checkActiveVideo( @@ -384,56 +283,6 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {#if activeRooms.length > 0} {#each activeRooms as active} diff --git a/plugins/love-resources/src/components/EditMeetingMinutes.svelte b/plugins/love-resources/src/components/EditMeetingMinutes.svelte new file mode 100644 index 0000000000..1036608521 --- /dev/null +++ b/plugins/love-resources/src/components/EditMeetingMinutes.svelte @@ -0,0 +1,52 @@ + + + +
+
+
+ +
+
+
+ + diff --git a/plugins/love-resources/src/components/EditRoom.svelte b/plugins/love-resources/src/components/EditRoom.svelte new file mode 100644 index 0000000000..d704e56a7a --- /dev/null +++ b/plugins/love-resources/src/components/EditRoom.svelte @@ -0,0 +1,104 @@ + + + +
+
+
+ +
+ +
+
+ + diff --git a/plugins/love-resources/src/components/Floor.svelte b/plugins/love-resources/src/components/Floor.svelte index f5e09adbac..d1c5e5c799 100644 --- a/plugins/love-resources/src/components/Floor.svelte +++ b/plugins/love-resources/src/components/Floor.svelte @@ -13,23 +13,25 @@ // limitations under the License. --> + +{#if floor} + {#if inline} + + {:else} +
+ {floor.name} +
+ {/if} +{/if} diff --git a/plugins/love-resources/src/components/FloorView.svelte b/plugins/love-resources/src/components/FloorView.svelte index d5df79fa5c..c75c55d28d 100644 --- a/plugins/love-resources/src/components/FloorView.svelte +++ b/plugins/love-resources/src/components/FloorView.svelte @@ -35,7 +35,7 @@ {#each rooms as room} - + {/each} diff --git a/plugins/love-resources/src/components/Hall.svelte b/plugins/love-resources/src/components/Hall.svelte index ab54f7836a..26808c0361 100644 --- a/plugins/love-resources/src/components/Hall.svelte +++ b/plugins/love-resources/src/components/Hall.svelte @@ -83,6 +83,11 @@ on:configure={() => (configure = false)} /> {:else} - (configure = true)} /> + (configure = true)} + on:open + /> {/if}
diff --git a/plugins/love-resources/src/components/Main.svelte b/plugins/love-resources/src/components/Main.svelte index eb242e22c9..27baf71f2b 100644 --- a/plugins/love-resources/src/components/Main.svelte +++ b/plugins/love-resources/src/components/Main.svelte @@ -13,13 +13,13 @@ // limitations under the License. --> -
- {#if ($currentRoom !== undefined && $screenSharing) || $currentRoom?.type === RoomType.Video} - +
+ {#if $currentRoom && $isConnected} + {:else} {/if} diff --git a/plugins/love-resources/src/components/MeetingMinutesSection.svelte b/plugins/love-resources/src/components/MeetingMinutesSection.svelte new file mode 100644 index 0000000000..3db2542a04 --- /dev/null +++ b/plugins/love-resources/src/components/MeetingMinutesSection.svelte @@ -0,0 +1,46 @@ + + + +
+ + {#if meetings > 0} + + {:else} +
+ + +
+ {/if} + + diff --git a/plugins/love-resources/src/components/MeetingMinutesTable.svelte b/plugins/love-resources/src/components/MeetingMinutesTable.svelte index 51ff93bd77..c5358d2ecc 100644 --- a/plugins/love-resources/src/components/MeetingMinutesTable.svelte +++ b/plugins/love-resources/src/components/MeetingMinutesTable.svelte @@ -1,21 +1,57 @@ - - +{#if viewlet?.$lookup?.descriptor?.component} + p._id) } }, + viewlet, + viewOptions, + viewOptionsConfig: viewlet.viewOptions?.other, + enableChecking: false + }} + /> +{/if} diff --git a/plugins/love-resources/src/components/Room.svelte b/plugins/love-resources/src/components/Room.svelte index 66a18590dc..5d2eb2904d 100644 --- a/plugins/love-resources/src/components/Room.svelte +++ b/plugins/love-resources/src/components/Room.svelte @@ -327,21 +327,57 @@ const handleFullScreen = () => ($isFullScreen = document.fullscreenElement != null) - function toggleFullscreen () { - if (!document.fullscreenElement) { + function checkFullscreen (): void { + const needFullScreen = $isFullScreen + if (document.fullscreenElement && !needFullScreen) { + document + .exitFullscreen() + .then(() => { + $isFullScreen = false + }) + .catch((err) => { + console.log(`Error exiting fullscreen mode: ${err.message} (${err.name})`) + $isFullScreen = false + }) + } else if (!document.fullscreenElement && needFullScreen) { roomEl .requestFullscreen() - .then(() => ($isFullScreen = true)) + .then(() => { + $isFullScreen = true + }) .catch((err) => { console.log(`Error attempting to enable fullscreen mode: ${err.message} (${err.name})`) $isFullScreen = false }) - } else { - document.exitFullscreen() - $isFullScreen = false } } - $: if (((document.fullscreenElement && !$isFullScreen) || $isFullScreen) && roomEl) toggleFullscreen() + + function onFullScreen (): void { + const needFullScreen = !$isFullScreen + if (!document.fullscreenElement && needFullScreen) { + roomEl + .requestFullscreen() + .then(() => { + $isFullScreen = true + }) + .catch((err) => { + console.log(`Error attempting to enable fullscreen mode: ${err.message} (${err.name})`) + $isFullScreen = false + }) + } else if (!needFullScreen) { + document + .exitFullscreen() + .then(() => { + $isFullScreen = false + }) + .catch((err) => { + console.log(`Error exiting fullscreen mode: ${err.message} (${err.name})`) + $isFullScreen = false + }) + } + } + + $: if (((document.fullscreenElement && !$isFullScreen) || $isFullScreen) && roomEl) checkFullscreen() function getActiveParticipants (participants: ParticipantData[]): ParticipantData[] { return participants.filter((p) => !p.isAgent || $infos.some(({ person }) => person === p._id)) @@ -396,7 +432,7 @@ {/if} {#if $currentRoom} - + {/if} diff --git a/plugins/love-resources/src/components/RoomModal.svelte b/plugins/love-resources/src/components/RoomModal.svelte new file mode 100644 index 0000000000..93ae8fcdc4 --- /dev/null +++ b/plugins/love-resources/src/components/RoomModal.svelte @@ -0,0 +1,43 @@ + + + +{#if ($currentRoom !== undefined && $screenSharing) || $currentRoom?.type === RoomType.Video} + dispatch('close')} + > + + +{/if} diff --git a/plugins/love-resources/src/components/RoomPreview.svelte b/plugins/love-resources/src/components/RoomPreview.svelte index 96abe0bdc5..819662b8cb 100644 --- a/plugins/love-resources/src/components/RoomPreview.svelte +++ b/plugins/love-resources/src/components/RoomPreview.svelte @@ -17,11 +17,15 @@ import { Avatar, personByIdStore } from '@hcengineering/contact-resources' import { IdMap, getCurrentAccount } from '@hcengineering/core' import { isOffice, ParticipantInfo, Room, RoomAccess, RoomType } from '@hcengineering/love' - import { Icon, Label, eventToHTMLElement, showPopup, DropdownIntlItem } from '@hcengineering/ui' + import { Icon, Label, eventToHTMLElement, showPopup } from '@hcengineering/ui' import { createEventDispatcher } from 'svelte' + import { getClient } from '@hcengineering/presentation' + import { openDoc } from '@hcengineering/view-resources' + import { get } from 'svelte/store' + import love from '../plugin' - import { invites, myInfo, myRequests } from '../stores' - import { getRoomLabel, tryConnect } from '../utils' + import { myInfo, selectedRoomPlace, currentRoom, meetingMinutesStore } from '../stores' + import { getRoomLabel, lk } from '../utils' import PersonActionPopup from './PersonActionPopup.svelte' import RoomLanguage from './RoomLanguage.svelte' @@ -36,7 +40,6 @@ const meName = $personByIdStore.get(me.person)?.name const meAvatar = $personByIdStore.get(me.person) - let container: HTMLDivElement let hoveredRoomX: number | undefined = undefined let hoveredRoomY: number | undefined = undefined @@ -61,12 +64,23 @@ hovered = false } - function clickHandler (e: MouseEvent, x: number, y: number, person: Person | undefined): void { + async function clickHandler (e: MouseEvent, x: number, y: number, person: Person | undefined): Promise { if (person !== undefined) { if (room._id === $myInfo?.room || $myInfo === undefined) return showPopup(PersonActionPopup, { room, person: person._id }, eventToHTMLElement(e)) } else { - void tryConnect($personByIdStore, $myInfo, room, info, $myRequests, $invites, { x, y }) + const client = getClient() + const hierarchy = client.getHierarchy() + if ($currentRoom?._id === room._id) { + const sid = await lk.getSid() + const meetingMinutes = + get(meetingMinutesStore) ?? (await client.findOne(love.class.MeetingMinutes, { sid, attachedTo: room._id })) + if (meetingMinutes === undefined) return + await openDoc(hierarchy, meetingMinutes) + } else { + selectedRoomPlace.set({ _id: room._id, x, y }) + await openDoc(hierarchy, room) + } } } @@ -112,7 +126,6 @@
@@ -57,8 +61,12 @@ muted: boolean mirror: boolean connecting: boolean + isAgent: boolean } + let aiPersonId: Ref | undefined = undefined + $: aiPersonId = $personIdByAccountId.get(aiBot.account.AIBot as Ref) + const dispatch = createEventDispatcher() let participants: ParticipantData[] = [] @@ -126,7 +134,8 @@ name: participant.name ?? '', muted: !participant.isMicrophoneEnabled, mirror: participant.isLocal, - connecting: false + connecting: false, + isAgent: participant.isAgent }) } participants = participants @@ -149,7 +158,8 @@ name: participant.name ?? '', muted: !participant.isMicrophoneEnabled, mirror: participant.isLocal, - connecting: false + connecting: false, + isAgent: participant.isAgent } participants.push(value) participants = participants @@ -227,7 +237,8 @@ name: info.name, muted: true, mirror: false, - connecting: true + connecting: true, + isAgent: info.person === aiPersonId } participants.push(value) } @@ -291,6 +302,12 @@ } }, 10) } + + function getActiveParticipants (participants: ParticipantData[]): ParticipantData[] { + return participants.filter((p) => !p.isAgent || $infos.some(({ person }) => person === p._id)) + } + + $: activeParticipants = getActiveParticipants(participants)
@@ -346,7 +363,7 @@
- {#each participants as participant, i (participant._id)} + {#each activeParticipants as participant, i (participant._id)}
diff --git a/plugins/love-resources/src/components/widget/ChatTab.svelte b/plugins/love-resources/src/components/widget/ChatTab.svelte index 269eabd0c0..5e7c2ecdff 100644 --- a/plugins/love-resources/src/components/widget/ChatTab.svelte +++ b/plugins/love-resources/src/components/widget/ChatTab.svelte @@ -13,13 +13,15 @@ // limitations under the License. --> @@ -42,4 +43,8 @@ on:channel={closeThread} onReply={replyToThread} on:close -/> +> + + + + diff --git a/plugins/love-resources/src/components/widget/MeetingWidget.svelte b/plugins/love-resources/src/components/widget/MeetingWidget.svelte index cc1c0a7b3b..2de0dfcb3c 100644 --- a/plugins/love-resources/src/components/widget/MeetingWidget.svelte +++ b/plugins/love-resources/src/components/widget/MeetingWidget.svelte @@ -14,15 +14,14 @@ --> + +
+ + + {#if ($currentRoom !== undefined && $screenSharing) || $currentRoom?.type === RoomType.Video} + + {/if} + +
diff --git a/plugins/love-resources/src/components/widget/TranscriptionTab.svelte b/plugins/love-resources/src/components/widget/TranscriptionTab.svelte index b7e4a3c385..00cb351401 100644 --- a/plugins/love-resources/src/components/widget/TranscriptionTab.svelte +++ b/plugins/love-resources/src/components/widget/TranscriptionTab.svelte @@ -13,31 +13,28 @@ // limitations under the License. --> tab.id === 'transcription')?.data?.thread} + threadId={undefined} collection="transcription" - on:channel={closeThread} - onReply={replyToThread} on:close -/> +> + + + + diff --git a/plugins/love-resources/src/components/widget/VideoTab.svelte b/plugins/love-resources/src/components/widget/VideoTab.svelte index 02f9d46fab..2d13fe9c42 100644 --- a/plugins/love-resources/src/components/widget/VideoTab.svelte +++ b/plugins/love-resources/src/components/widget/VideoTab.svelte @@ -13,13 +13,16 @@ // limitations under the License. --> +
diff --git a/plugins/love-resources/src/index.ts b/plugins/love-resources/src/index.ts index b3ffb54144..a7a197b485 100644 --- a/plugins/love-resources/src/index.ts +++ b/plugins/love-resources/src/index.ts @@ -12,6 +12,12 @@ import WorkbenchExtension from './components/WorkbenchExtension.svelte' import LoveWidget from './components/LoveWidget.svelte' import MeetingWidget from './components/widget/MeetingWidget.svelte' import MeetingMinutesPresenter from './components/MeetingMinutesPresenter.svelte' +import MeetingMinutesSection from './components/MeetingMinutesSection.svelte' +import EditMeetingMinutes from './components/EditMeetingMinutes.svelte' +import EditRoom from './components/EditRoom.svelte' +import FloorAttributePresenter from './components/FloorAttributePresenter.svelte' +import FloorView from './components/FloorView.svelte' +import MeetingMinutesTable from './components/MeetingMinutesTable.svelte' import { copyGuestLink, @@ -20,7 +26,8 @@ import { startTranscription, stopTranscription, toggleMic, - toggleVideo + toggleVideo, + getMeetingMinutesTitle } from './utils' export { setCustomCreateScreenTracks } from './utils' @@ -36,7 +43,13 @@ export default async (): Promise => ({ EditMeetingData, LoveWidget, MeetingWidget, - MeetingMinutesPresenter + MeetingMinutesPresenter, + MeetingMinutesSection, + EditMeetingMinutes, + EditRoom, + FloorAttributePresenter, + FloorView, + MeetingMinutesTable }, function: { CreateMeeting: createMeeting, @@ -50,7 +63,8 @@ export default async (): Promise => ({ }, CanCopyGuestLink: () => { return hasAccountRole(getCurrentAccount(), AccountRole.User) - } + }, + MeetingMinutesTitleProvider: getMeetingMinutesTitle }, actionImpl: { ToggleMic: toggleMic, diff --git a/plugins/love-resources/src/plugin.ts b/plugins/love-resources/src/plugin.ts index 9171775e23..0f89ecfc7e 100644 --- a/plugins/love-resources/src/plugin.ts +++ b/plugins/love-resources/src/plugin.ts @@ -24,7 +24,13 @@ export default mergeIds(loveId, love, { ControlExt: '' as AnyComponent, MeetingData: '' as AnyComponent, EditMeetingData: '' as AnyComponent, - MeetingMinutesPresenter: '' as AnyComponent + MeetingMinutesPresenter: '' as AnyComponent, + MeetingMinutesSection: '' as AnyComponent, + EditMeetingMinutes: '' as AnyComponent, + EditRoom: '' as AnyComponent, + FloorAttributePresenter: '' as AnyComponent, + MeetingMinutesTable: '' as AnyComponent, + FloorView: '' as AnyComponent }, function: { CreateMeeting: '' as Resource, diff --git a/plugins/love-resources/src/stores.ts b/plugins/love-resources/src/stores.ts index 856c79d910..6759bdc783 100644 --- a/plugins/love-resources/src/stores.ts +++ b/plugins/love-resources/src/stores.ts @@ -8,7 +8,8 @@ import { type JoinRequest, type Office, type ParticipantInfo, - type Room + type Room, + type MeetingMinutes } from '@hcengineering/love' import { createQuery, getClient } from '@hcengineering/presentation' import { derived, get, writable } from 'svelte/store' @@ -59,6 +60,9 @@ export const activeInvites = derived(invites, (val) => { export const myPreferences = writable() export let $myPreferences: DevicesPreference | undefined +export const meetingMinutesStore = writable(undefined) +export const selectedRoomPlace = writable<{ _id: Ref, x: number, y: number } | undefined>(undefined) + function filterParticipantInfo (value: ParticipantInfo[]): ParticipantInfo[] { const map = new Map() const aiPersonId = get(personIdByAccountId).get(aiBot.account.AIBot as Ref) diff --git a/plugins/love-resources/src/utils.ts b/plugins/love-resources/src/utils.ts index 29d4bb1abe..2b9482b5c7 100644 --- a/plugins/love-resources/src/utils.ts +++ b/plugins/love-resources/src/utils.ts @@ -5,8 +5,10 @@ import core, { AccountRole, concatLink, type Data, + generateId, getCurrentAccount, type IdMap, + makeCollaborativeDoc, type Ref, type Space, type TxOperations @@ -27,7 +29,8 @@ import { RoomAccess, RoomType, TranscriptionStatus, - type RoomMetadata + type RoomMetadata, + type MeetingMinutes } from '@hcengineering/love' import { getEmbeddedLabel, getMetadata, getResource, type IntlString } from '@hcengineering/platform' import presentation, { @@ -36,7 +39,7 @@ import presentation, { type DocCreatePhase, getClient } from '@hcengineering/presentation' -import { type DropdownTextItem, getCurrentLocation, type Location, navigate, showPopup } from '@hcengineering/ui' +import { type DropdownTextItem, getCurrentLocation, navigate, showPopup } from '@hcengineering/ui' import { isKrispNoiseFilterSupported, KrispNoiseFilter } from '@livekit/krisp-noise-filter' import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors' import { @@ -57,13 +60,20 @@ import { import { get, writable } from 'svelte/store' import aiBot from '@hcengineering/ai-bot' import { connectMeeting, disconnectMeeting } from '@hcengineering/ai-bot-resources' -import { openWidget, sidebarStore, updateWidgetState } from '@hcengineering/workbench-resources' +import { + openWidget, + sidebarStore, + updateWidgetState, + currentWorkspaceStore, + openWidgetTab +} from '@hcengineering/workbench-resources' import { type Widget, type WidgetTab } from '@hcengineering/workbench' import view from '@hcengineering/view' +import chunter from '@hcengineering/chunter' import { sendMessage } from './broadcast' import love from './plugin' -import { $myPreferences, currentRoom } from './stores' +import { $myPreferences, meetingMinutesStore, currentRoom } from './stores' import RoomSettingsPopup from './components/RoomSettingsPopup.svelte' export const selectedCamId = 'selectedDevice_cam' @@ -92,7 +102,9 @@ export async function getToken ( function getTokenRoomName (roomName: string, roomId: Ref): string { const loc = getCurrentLocation() - return `${loc.path[1]}_${roomName}_${roomId}` + const currentWorkspace = get(currentWorkspaceStore) + + return `${currentWorkspace?.workspaceId ?? loc.path[1]}_${roomName}_${roomId}` } export const lk: LKRoom = new LKRoom({ @@ -432,6 +444,7 @@ export async function disconnect (): Promise { isMicEnabled.set(false) isCameraEnabled.set(false) isSharingEnabled.set(false) + meetingMinutesStore.set(undefined) sendMessage({ type: 'mic', value: false }) sendMessage({ type: 'cam', value: false }) sendMessage({ type: 'share', value: false }) @@ -580,6 +593,46 @@ async function connectLK (currentPerson: Person, room: Room): Promise { ]) } +async function createMeetingMinutes (room: Room): Promise { + const client = getClient() + const sid = await lk.getSid() + + if (sid !== undefined) { + const doc = await client.findOne(love.class.MeetingMinutes, { sid }) + + if (doc === undefined) { + const dateStr = new Date().toISOString().replace('T', ' ').slice(0, 19) + const _id = generateId() + const newDoc: MeetingMinutes = { + _id, + _class: love.class.MeetingMinutes, + sid, + attachedTo: room._id, + attachedToClass: room._class, + collection: 'meetings', + space: core.space.Workspace, + title: room.name + ' ' + dateStr, + description: makeCollaborativeDoc(_id, 'description'), + modifiedBy: getCurrentAccount()._id, + modifiedOn: Date.now() + } + + await client.addCollection( + love.class.MeetingMinutes, + core.space.Workspace, + room._id, + room._class, + 'meetings', + { sid, title: newDoc.title, description: newDoc.description }, + _id + ) + meetingMinutesStore.set(newDoc) + } else { + meetingMinutesStore.set(doc) + } + } +} + export async function connectRoom ( x: number, y: number, @@ -590,6 +643,7 @@ export async function connectRoom ( await disconnect() await moveToRoom(x, y, currentInfo, currentPerson, room, getMetadata(presentation.metadata.SessionId) ?? null) await connectLK(currentPerson, room) + await createMeetingMinutes(room) } export const joinRequest: Ref | undefined = undefined @@ -910,13 +964,13 @@ export function isTranscriptionAllowed (): boolean { return url !== '' } -export function createMeetingWidget (widget: Widget, room: Ref, loc: Location, video: boolean): void { +export function createMeetingWidget (widget: Widget, room: Ref, video: boolean): void { const tabs: WidgetTab[] = [ ...(video ? [ { id: 'video', - name: 'Video', + label: love.string.Video, icon: love.icon.Cam, readonly: true } @@ -924,13 +978,13 @@ export function createMeetingWidget (widget: Widget, room: Ref, loc: Locat : []), { id: 'chat', - name: 'Chat', + label: chunter.string.Chat, icon: view.icon.Bubble, readonly: true }, { id: 'transcription', - name: 'Transcription', + label: love.string.Transcription, icon: view.icon.Feather, readonly: true } @@ -940,12 +994,12 @@ export function createMeetingWidget (widget: Widget, room: Ref, loc: Locat { room }, - { active: loc.path[2] !== loveId, openedByUser: false }, + { active: true, openedByUser: false }, tabs ) } -export function createMeetingVideoWidgetTab (widget: Widget, loc: Location): void { +export function createMeetingVideoWidgetTab (widget: Widget): void { const state = get(sidebarStore) const { widgetsState } = state const widgetState = widgetsState.get(widget._id) @@ -954,12 +1008,23 @@ export function createMeetingVideoWidgetTab (widget: Widget, loc: Location): voi const tab: WidgetTab = { id: 'video', - name: 'Video', + label: love.string.Video, icon: love.icon.Cam, readonly: true } updateWidgetState(widget._id, { tabs: [tab, ...widgetState.tabs], - tab: state.widget === widget._id && loc.path[2] === loveId ? widgetState.tab : 'video' + tab: 'video' }) + openWidgetTab(love.ids.MeetingWidget, 'video') +} + +export async function getMeetingMinutesTitle ( + client: TxOperations, + ref: Ref, + doc?: MeetingMinutes +): Promise { + const meeting = doc ?? (await client.findOne(love.class.MeetingMinutes, { _id: ref })) + + return meeting?.title ?? '' } diff --git a/plugins/love/src/index.ts b/plugins/love/src/index.ts index 34113039b3..fb59afb44d 100644 --- a/plugins/love/src/index.ts +++ b/plugins/love/src/index.ts @@ -1,12 +1,12 @@ import { Event } from '@hcengineering/calendar' import { Person } from '@hcengineering/contact' -import { Class, Doc, Mixin, Ref } from '@hcengineering/core' +import { AttachedDoc, Class, CollaborativeDoc, 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' import { Preference } from '@hcengineering/preference' import { AnyComponent } from '@hcengineering/ui/src/types' -import { Action } from '@hcengineering/view' +import { Action, Viewlet, ViewletDescriptor } from '@hcengineering/view' import { Widget } from '@hcengineering/workbench' export const loveId = 'love' as Plugin @@ -102,6 +102,9 @@ export interface Room extends Doc { y: number language: RoomLanguage startWithTranscription: boolean + description: CollaborativeDoc + attachments?: number + meetings?: number } export interface Office extends Room { @@ -155,12 +158,13 @@ export interface DevicesPreference extends Preference { camEnabled: boolean } -export interface MeetingMinutes extends Doc { +export interface MeetingMinutes extends AttachedDoc { sid: string title: string - room: Ref transcription?: number messages?: number + description: CollaborativeDoc + attachments?: number } export * from './utils' @@ -200,7 +204,11 @@ const love = plugin(loveId, { Meeting: '' as IntlString, Transcription: '' as IntlString, StartWithTranscription: '' as IntlString, - MeetingMinutes: '' as IntlString + MeetingMinutes: '' as IntlString, + StartMeeting: '' as IntlString, + Video: '' as IntlString, + NoMeetingMinutes: '' as IntlString, + JoinMeeting: '' as IntlString }, ids: { MainFloor: '' as Ref, @@ -243,6 +251,13 @@ const love = plugin(loveId, { }, component: { SelectScreenSourcePopup: '' as AnyComponent + }, + viewlet: { + TableMeetingMinutes: '' as Ref, + MeetingMinutesDescriptor: '' as Ref, + FloorDescriptor: '' as Ref, + Floor: '' as Ref, + FloorMeetingMinutes: '' as Ref } }) diff --git a/plugins/love/src/utils.ts b/plugins/love/src/utils.ts index d40a190ad1..9eaa2c2d40 100644 --- a/plugins/love/src/utils.ts +++ b/plugins/love/src/utils.ts @@ -1,5 +1,5 @@ import { Employee, Person } from '@hcengineering/contact' -import { Data, Ref } from '@hcengineering/core' +import { Data, generateId, makeCollaborativeDoc, Ref } from '@hcengineering/core' import love, { Office, Room, ParticipantInfo, RoomAccess, RoomType, GRID_WIDTH } from '.' @@ -15,11 +15,13 @@ export function isOffice (room: Data): room is Office { return (room as Office).person !== undefined } -export function createDefaultRooms (employees: Ref[]): Data[] { - const res: Data[] = [] +export function createDefaultRooms (employees: Ref[]): (Data & { _id: Ref })[] { + const res: (Data & { _id: Ref })[] = [] // create 12 offices for (let index = 0; index < 12; index++) { - const office: Data = { + const _id = generateId() + const office: Data & { _id: Ref } = { + _id, name: '', type: RoomType.Audio, access: RoomAccess.Knock, @@ -30,11 +32,15 @@ export function createDefaultRooms (employees: Ref[]): Data() + res.push({ + _id: allHands, name: 'All hands', type: RoomType.Video, access: RoomAccess.Open, @@ -44,9 +50,13 @@ export function createDefaultRooms (employees: Ref[]): Data() res.push({ + _id: meetingRoom1, name: 'Meeting Room 1', type: RoomType.Video, access: RoomAccess.Open, @@ -56,9 +66,12 @@ export function createDefaultRooms (employees: Ref[]): Data() res.push({ + _id: meetingRoom2, name: 'Meeting Room 2', type: RoomType.Video, access: RoomAccess.Open, @@ -68,9 +81,12 @@ export function createDefaultRooms (employees: Ref[]): Data() res.push({ + _id: voiceRoom1, name: 'Voice Room 1', type: RoomType.Audio, access: RoomAccess.Open, @@ -80,9 +96,12 @@ export function createDefaultRooms (employees: Ref[]): Data() res.push({ + _id: voiceRoom2, name: 'Voice Room 2', type: RoomType.Audio, access: RoomAccess.Open, @@ -92,7 +111,8 @@ export function createDefaultRooms (employees: Ref[]): Data>, docId: Ref, - op: OpWithMe + op: 'add' | 'remove', + doc?: Doc ): Promise { const me = getCurrentAccount()._id const hierarchy = client.getHierarchy() - const target = await client.findOne(docClass, { _id: docId }) - if (target !== undefined) { - if (hierarchy.hasMixin(target, notification.mixin.Collaborators)) { - const collab = hierarchy.as(target, notification.mixin.Collaborators) - let collabUpdate: DocumentUpdate | undefined - if (collab.collaborators.includes(me) && op === OpWithMe.Remove) { - collabUpdate = { - $pull: { - collaborators: me - } - } - } else if (!collab.collaborators.includes(me) && op === OpWithMe.Add) { - collabUpdate = { - $push: { - collaborators: me - } + if (hierarchy.classHierarchyMixin(docClass, notification.mixin.ClassCollaborators) === undefined) return + + const target = doc ?? (await client.findOne(docClass, { _id: docId })) + if (target === undefined) return + if (hierarchy.hasMixin(target, notification.mixin.Collaborators)) { + const collab = hierarchy.as(target, notification.mixin.Collaborators) + let collabUpdate: DocumentUpdate | undefined + + if (collab.collaborators.includes(me) && op === 'remove') { + collabUpdate = { + $pull: { + collaborators: me } } - - if (collabUpdate !== undefined) { - await client.updateMixin( - collab._id, - collab._class, - collab.space, - notification.mixin.Collaborators, - collabUpdate - ) + } else if (!collab.collaborators.includes(me) && op === 'add') { + collabUpdate = { + $push: { + collaborators: me + } } } + + if (collabUpdate !== undefined) { + await client.updateMixin(collab._id, collab._class, collab.space, notification.mixin.Collaborators, collabUpdate) + } + } else if (op === 'add') { + await client.createMixin(docId, docClass, target.space, notification.mixin.Collaborators, { + collaborators: [me] + }) } } @@ -277,7 +273,7 @@ async function updateMeInCollaborators ( */ export async function unsubscribe (context: DocNotifyContext): Promise { const client = getClient() - await updateMeInCollaborators(client, context.objectClass, context.objectId, OpWithMe.Remove) + await subscribeDoc(client, context.objectClass, context.objectId, 'remove') } /** @@ -285,7 +281,7 @@ export async function unsubscribe (context: DocNotifyContext): Promise { */ export async function subscribe (docClass: Ref>, docId: Ref): Promise { const client = getClient() - await updateMeInCollaborators(client, docClass, docId, OpWithMe.Add) + await subscribeDoc(client, docClass, docId, 'add') } export async function pinDocNotifyContext (object: DocNotifyContext): Promise { diff --git a/plugins/view-resources/src/components/EditDoc.svelte b/plugins/view-resources/src/components/EditDoc.svelte index 91f1b8e753..6b180bfce0 100644 --- a/plugins/view-resources/src/components/EditDoc.svelte +++ b/plugins/view-resources/src/components/EditDoc.svelte @@ -39,6 +39,7 @@ export let _class: Ref> export let embedded: boolean = false export let readonly: boolean = false + export let selectedAside: boolean | undefined = undefined let realObjectClass: Ref> = _class let lastId: Ref | undefined @@ -225,6 +226,7 @@ allowClose={!embedded} isAside={true} {embedded} + {selectedAside} bind:content bind:panelWidth bind:innerWidth diff --git a/plugins/workbench-resources/src/components/sidebar/SidebarTab.svelte b/plugins/workbench-resources/src/components/sidebar/SidebarTab.svelte index aeda43b35a..7e756f2ce5 100644 --- a/plugins/workbench-resources/src/components/sidebar/SidebarTab.svelte +++ b/plugins/workbench-resources/src/components/sidebar/SidebarTab.svelte @@ -43,7 +43,7 @@ { return spaces.find((sp) => sp.archived) !== undefined @@ -59,6 +59,7 @@ export default async (): Promise => ({ CanCloseTab: canCloseTab, CreateWidgetTab: createWidgetTab, CloseWidgetTab: closeWidgetTab, + CloseWidget: closeWidget, GetSidebarObject: getSidebarObject }, actionImpl: { diff --git a/plugins/workbench-resources/src/sidebar.ts b/plugins/workbench-resources/src/sidebar.ts index e0d852b78b..5a7900ab35 100644 --- a/plugins/workbench-resources/src/sidebar.ts +++ b/plugins/workbench-resources/src/sidebar.ts @@ -18,7 +18,7 @@ import { get, writable } from 'svelte/store' import { getCurrentLocation, deviceOptionsStore as deviceInfo } from '@hcengineering/ui' import { getResource } from '@hcengineering/platform' -import { workspaceStore } from './utils' +import { locationWorkspaceStore } from './utils' import { Analytics } from '@hcengineering/analytics' export enum SidebarVariant { @@ -50,14 +50,14 @@ export const defaultSidebarState: SidebarState = { export const sidebarStore = writable(defaultSidebarState) -workspaceStore.subscribe((workspace) => { +locationWorkspaceStore.subscribe((workspace) => { sidebarStore.set(getSidebarStateFromLocalStorage(workspace ?? '')) }) sidebarStore.subscribe(setSidebarStateToLocalStorage) export function syncSidebarState (): void { - const workspace = get(workspaceStore) + const workspace = get(locationWorkspaceStore) sidebarStore.set(getSidebarStateFromLocalStorage(workspace ?? '')) } function getSideBarLocalStorageKey (workspace: string): string | undefined { @@ -89,7 +89,7 @@ function getSidebarStateFromLocalStorage (workspace: string): SidebarState { } function setSidebarStateToLocalStorage (state: SidebarState): void { - const workspace = get(workspaceStore) + const workspace = get(locationWorkspaceStore) if (workspace == null || workspace === '') return const sidebarStateLocalStorageKey = getSideBarLocalStorageKey(workspace) @@ -214,6 +214,8 @@ export function openWidgetTab (widget: Ref, tab: string): void { Analytics.handleEvent(WorkbenchEvents.SidebarOpenWidget, { widget, tab: newTab?.name }) sidebarStore.set({ ...state, + widget, + variant: SidebarVariant.EXPANDED, widgetsState }) } diff --git a/plugins/workbench-resources/src/utils.ts b/plugins/workbench-resources/src/utils.ts index d00a88a0d4..dffc7b52d1 100644 --- a/plugins/workbench-resources/src/utils.ts +++ b/plugins/workbench-resources/src/utils.ts @@ -157,7 +157,13 @@ export async function showApplication (app: Application): Promise { } export const workspacesStore = writable([]) -export const workspaceStore = derived(location, (loc: Location) => loc.path[1]) +export const locationWorkspaceStore = derived(location, (loc: Location) => loc.path[1]) +export const currentWorkspaceStore = derived( + [workspacesStore, locationWorkspaceStore], + ([$workspaces, $locationWorkspace]) => { + return $workspaces.find((it) => it.workspace === $locationWorkspace) + } +) /** * @public diff --git a/plugins/workbench-resources/src/workbench.ts b/plugins/workbench-resources/src/workbench.ts index ecf38d225c..c5e1eaba60 100644 --- a/plugins/workbench-resources/src/workbench.ts +++ b/plugins/workbench-resources/src/workbench.ts @@ -40,7 +40,7 @@ import { type Asset, type IntlString, getMetadata, getResource, translate } from import { parseLinkId } from '@hcengineering/view-resources' import notification, { notificationId } from '@hcengineering/notification' -import { workspaceStore } from './utils' +import { locationWorkspaceStore } from './utils' import workbench from './plugin' export const tabIdStore = writable | undefined>() @@ -56,7 +56,7 @@ tabIdStore.subscribe((value) => { prevTabId = value }) -workspaceStore.subscribe((workspace) => { +locationWorkspaceStore.subscribe((workspace) => { tabIdStore.set(getTabFromLocalStorage(workspace ?? '')) }) @@ -64,7 +64,7 @@ tabIdStore.subscribe(saveTabToLocalStorage) const syncTabLoc = reduceCalls(async (): Promise => { const loc = getCurrentLocation() - const workspace = get(workspaceStore) + const workspace = get(locationWorkspaceStore) if (workspace == null || workspace === '') return const tab = get(currentTabStore) if (tab == null) return @@ -132,7 +132,7 @@ locationStore.subscribe((l: Location) => { }) export function syncWorkbenchTab (): void { - const workspace = get(workspaceStore) + const workspace = get(locationWorkspaceStore) tabIdStore.set(getTabFromLocalStorage(workspace ?? '')) } @@ -153,7 +153,7 @@ function getTabFromLocalStorage (workspace: string): Ref | undefin } function saveTabToLocalStorage (_id: Ref | undefined): void { - const workspace = get(workspaceStore) + const workspace = get(locationWorkspaceStore) if (workspace == null || workspace === '') return const localStorageKey = getTabIdLocalStorageKey(workspace) diff --git a/plugins/workbench/src/index.ts b/plugins/workbench/src/index.ts index cb7374fd2f..e511a7f78e 100644 --- a/plugins/workbench/src/index.ts +++ b/plugins/workbench/src/index.ts @@ -89,6 +89,7 @@ export interface WidgetPreference extends Preference { export interface WidgetTab { id: string name?: string + label?: IntlString icon?: Asset | AnySvelteComponent iconComponent?: AnyComponent iconProps?: Record @@ -280,6 +281,7 @@ export default plugin(workbenchId, { function: { CreateWidgetTab: '' as Resource<(widget: Widget, tab: WidgetTab, newTab: boolean) => Promise>, CloseWidgetTab: '' as Resource<(widget: Widget, tab: string) => Promise>, + CloseWidget: '' as Resource<(widget: Ref) => Promise>, GetSidebarObject: '' as Resource<() => Partial>> }, actionImpl: { diff --git a/server-plugins/ai-bot-resources/src/utils.ts b/server-plugins/ai-bot-resources/src/utils.ts index a3f6366b18..f603d89c32 100644 --- a/server-plugins/ai-bot-resources/src/utils.ts +++ b/server-plugins/ai-bot-resources/src/utils.ts @@ -62,6 +62,7 @@ export async function createAccountRequest (workspace: WorkspaceId, ctx: Measure } try { + ctx.info('Requesting AI account creation', { url, workspace }) await fetch(concatLink(url, '/connect'), { method: 'POST', headers: { diff --git a/server-plugins/love-resources/package.json b/server-plugins/love-resources/package.json index dde0f521f0..d01be96df2 100644 --- a/server-plugins/love-resources/package.json +++ b/server-plugins/love-resources/package.json @@ -37,6 +37,7 @@ "prettier-plugin-svelte": "^3.2.2" }, "dependencies": { + "@hcengineering/view": "^0.6.13", "@hcengineering/core": "^0.6.32", "@hcengineering/contact": "^0.6.24", "@hcengineering/notification": "^0.6.23", diff --git a/server-plugins/love-resources/src/index.ts b/server-plugins/love-resources/src/index.ts index 37cca9ba13..f989c81669 100644 --- a/server-plugins/love-resources/src/index.ts +++ b/server-plugins/love-resources/src/index.ts @@ -23,11 +23,14 @@ import core, { TxMixin, TxProcessor, TxUpdateDoc, - UserStatus + UserStatus, + Doc, + concatLink } from '@hcengineering/core' import love, { Invite, JoinRequest, + MeetingMinutes, ParticipantInfo, RequestStatus, RoomAccess, @@ -35,14 +38,15 @@ import love, { loveId } from '@hcengineering/love' import notification from '@hcengineering/notification' -import { translate } from '@hcengineering/platform' -import { TriggerControl } from '@hcengineering/server-core' +import { getMetadata, translate } from '@hcengineering/platform' +import serverCore, { TriggerControl } from '@hcengineering/server-core' import { createPushNotification, getNotificationProviderControl, isAllowed } from '@hcengineering/server-notification-resources' import { workbenchId } from '@hcengineering/workbench' +import view from '@hcengineering/view' export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] @@ -370,8 +374,31 @@ export async function OnInvite (txes: Tx[], control: TriggerControl): Promise { + const meetingMinutes = doc as MeetingMinutes + const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? '' + + const panelProps = [view.component.EditDoc, meetingMinutes._id, meetingMinutes._class] + const fragment = encodeURIComponent(panelProps.join('|')) + const path = `${workbenchId}/${control.workspace.workspaceUrl}/${loveId}#${fragment}` + const link = concatLink(front, path) + return `${meetingMinutes.title}` +} + +/** + * @public + */ +export async function meetingMinutesTextPresenter (doc: Doc): Promise { + const meetingMinutes = doc as MeetingMinutes + return meetingMinutes.title +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ + function: { + MeetingMinutesHTMLPresenter: meetingMinutesHTMLPresenter, + MeetingMinutesTextPresenter: meetingMinutesTextPresenter + }, trigger: { OnEmployee, OnUserStatus, diff --git a/server-plugins/love/package.json b/server-plugins/love/package.json index e376ec38d4..85084a0fd9 100644 --- a/server-plugins/love/package.json +++ b/server-plugins/love/package.json @@ -38,7 +38,9 @@ "prettier-plugin-svelte": "^3.2.2" }, "dependencies": { + "@hcengineering/core": "^0.6.32", "@hcengineering/platform": "^0.6.11", - "@hcengineering/server-core": "^0.6.1" + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-notification": "^0.6.1" } } diff --git a/server-plugins/love/src/index.ts b/server-plugins/love/src/index.ts index e67d1c1250..9b645774be 100644 --- a/server-plugins/love/src/index.ts +++ b/server-plugins/love/src/index.ts @@ -1,6 +1,7 @@ import type { Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { TriggerFunc } from '@hcengineering/server-core' +import { Presenter } from '@hcengineering/server-notification' /** * @public @@ -11,6 +12,10 @@ export const serverLoveId = 'server-love' as Plugin * @public */ export default plugin(serverLoveId, { + function: { + MeetingMinutesHTMLPresenter: '' as Resource, + MeetingMinutesTextPresenter: '' as Resource + }, trigger: { OnEmployee: '' as Resource, OnUserStatus: '' as Resource, diff --git a/services/ai-bot/pod-ai-bot/src/controller.ts b/services/ai-bot/pod-ai-bot/src/controller.ts index fdc7b74905..90b2a0b73c 100644 --- a/services/ai-bot/pod-ai-bot/src/controller.ts +++ b/services/ai-bot/pod-ai-bot/src/controller.ts @@ -290,7 +290,10 @@ export class AIControl { if (workspace === null) return const wsClient = await this.getWorkspaceClient(workspace) - if (wsClient === undefined) return + if (wsClient === undefined) { + this.ctx.error('Workspace not found', { workspace }) + return + } return await wsClient.getLoveIdentity() } diff --git a/services/ai-bot/pod-ai-bot/src/server/server.ts b/services/ai-bot/pod-ai-bot/src/server/server.ts index 9cbda42522..45c9765b58 100644 --- a/services/ai-bot/pod-ai-bot/src/server/server.ts +++ b/services/ai-bot/pod-ai-bot/src/server/server.ts @@ -27,6 +27,7 @@ import { aiBotAccountEmail } from '@hcengineering/ai-bot' import { extractToken } from '@hcengineering/server-client' +import { MeasureContext } from '@hcengineering/core' import { ApiError } from './error' import { AIControl } from '../controller' @@ -54,7 +55,7 @@ const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, n void handleRequest(fn, req, res, next) } -export function createServer (controller: AIControl): Express { +export function createServer (controller: AIControl, ctx: MeasureContext): Express { const app = express() app.use(cors()) app.use(express.json()) @@ -78,6 +79,7 @@ export function createServer (controller: AIControl): Express { app.post( '/connect', wrapRequest(async (_, res, token) => { + ctx.info('Request to connect to workspace', { workspace: token.workspace.name }) await controller.connect(token.workspace.name) res.status(200) diff --git a/services/ai-bot/pod-ai-bot/src/start.ts b/services/ai-bot/pod-ai-bot/src/start.ts index 2d22b623b4..daba37d419 100644 --- a/services/ai-bot/pod-ai-bot/src/start.ts +++ b/services/ai-bot/pod-ai-bot/src/start.ts @@ -51,7 +51,7 @@ export const start = async (): Promise => { } const aiControl = new AIControl(storage, ctx) - const app = createServer(aiControl) + const app = createServer(aiControl, ctx) const server = listen(app, config.Port) const onClose = (): void => { diff --git a/services/ai-bot/pod-ai-bot/src/utils/account.ts b/services/ai-bot/pod-ai-bot/src/utils/account.ts index 7ca9e54a8a..12f0e91b95 100644 --- a/services/ai-bot/pod-ai-bot/src/utils/account.ts +++ b/services/ai-bot/pod-ai-bot/src/utils/account.ts @@ -64,6 +64,7 @@ export async function tryAssignToWorkspace ( const info = await tryGetWorkspaceInfo(workspace, ctx) if (info === undefined) { + ctx.error('Workspace not found', { workspace }) return false } diff --git a/services/ai-bot/pod-ai-bot/src/workspace/love.ts b/services/ai-bot/pod-ai-bot/src/workspace/love.ts index 5ff30aaac6..6b2f5e1cea 100644 --- a/services/ai-bot/pod-ai-bot/src/workspace/love.ts +++ b/services/ai-bot/pod-ai-bot/src/workspace/love.ts @@ -9,8 +9,7 @@ import core, { TxCreateDoc, TxUpdateDoc, MeasureContext, - Markup, - generateId + Markup } from '@hcengineering/core' import { Person } from '@hcengineering/contact' import love, { @@ -22,27 +21,11 @@ import love, { TranscriptionStatus } from '@hcengineering/love' import { ConnectMeetingRequest } from '@hcengineering/ai-bot' -import chunter, { ChatMessage } from '@hcengineering/chunter' +import chunter from '@hcengineering/chunter' import { jsonToMarkup, MarkupNodeType } from '@hcengineering/text' import config from '../config' -class Transcriptions { - private readonly transcriptionByPerson = new Map, { _id: Ref, text: string }>() - - get (person: Ref): { _id: Ref, text: string } | undefined { - return this.transcriptionByPerson.get(person) - } - - set (person: Ref, value: { _id: Ref, text: string }): void { - this.transcriptionByPerson.set(person, value) - } - - delete (person: Ref): void { - this.transcriptionByPerson.delete(person) - } -} - export class LoveController { private readonly roomSidById = new Map, string>() private readonly connectedRooms = new Set>() @@ -50,7 +33,6 @@ export class LoveController { private participantsInfo: ParticipantInfo[] = [] private rooms: Room[] = [] private readonly meetingMinutes: MeetingMinutes[] = [] - private readonly activeTranscriptions = new Map, Transcriptions>() constructor ( private readonly workspace: string, @@ -142,6 +124,7 @@ export class LoveController { const room = await this.getRoom(request.roomId) if (room === undefined) { + this.ctx.error('Room not found', request) this.roomSidById.delete(request.roomId) this.connectedRooms.delete(request.roomId) return @@ -166,8 +149,6 @@ export class LoveController { async disconnect (roomId: Ref): Promise { this.ctx.info('Disconnecting', { roomId }) - this.activeTranscriptions.delete(roomId) - const participant = await this.getRoomParticipant(roomId, this.currentPerson._id) if (participant !== undefined) { await this.client.remove(participant) @@ -191,44 +172,32 @@ export class LoveController { return } - const doc = await this.getMeetingMinutes(roomId, this.roomSidById.get(roomId) ?? '') - const personAccount = this.client.getModel().getAccountByPersonId(participant.person)[0] - if (doc === undefined) return + const sid = this.roomSidById.get(roomId) - const transcriptions = this.activeTranscriptions.get(roomId) ?? new Transcriptions() - const activeTranscription = transcriptions.get(participant.person) - - if (activeTranscription === undefined) { - const _id = generateId() - if (!final) { - transcriptions.set(participant.person, { _id, text }) - this.activeTranscriptions.set(roomId, transcriptions) - } - - await this.client.addCollection( - chunter.class.ChatMessage, - core.space.Workspace, - doc._id, - doc._class, - 'transcription', - { - message: this.transcriptToMarkup(text) - }, - _id, - undefined, - personAccount._id - ) - } else { - const mergedText = activeTranscription.text + ' ' + text - if (!final) { - transcriptions.set(participant.person, { _id: activeTranscription._id, text: mergedText }) - } else { - transcriptions.delete(participant.person) - } - await this.client.updateDoc(chunter.class.ChatMessage, core.space.Workspace, activeTranscription._id, { - message: this.transcriptToMarkup(mergedText) - }) + if (sid === undefined) { + return } + + const personAccount = this.client.getModel().getAccountByPersonId(participant.person)[0] + const doc = await this.getMeetingMinutes(room, sid) + + if (doc === undefined) return + const op = this.client.apply(undefined, undefined, true) + + await op.addCollection( + chunter.class.ChatMessage, + core.space.Workspace, + doc._id, + doc._class, + 'transcription', + { + message: this.transcriptToMarkup(text) + }, + undefined, + undefined, + personAccount._id + ) + await op.commit() } hasActiveConnections (): boolean { @@ -246,18 +215,15 @@ export class LoveController { ) } - async getMeetingMinutes (room: Ref, sid: string): Promise { + async getMeetingMinutes (room: Room, sid: string): Promise { if (sid === '') return undefined - const existing = this.meetingMinutes.find((m) => m.room === room && m.sid === sid) - if (existing !== undefined) return existing + const doc = + this.meetingMinutes.find((m) => m.sid === sid) ?? (await this.client.findOne(love.class.MeetingMinutes, { sid })) - const doc = await this.client.findOne(love.class.MeetingMinutes, { - room, - sid - }) - - if (doc === undefined) return + if (doc === undefined) { + return undefined + } this.meetingMinutes.push(doc) return doc diff --git a/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts b/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts index 7498f327d7..94ac84fb19 100644 --- a/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts +++ b/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts @@ -727,7 +727,10 @@ export class WorkspaceClient { // Just wait initialization await this.opClient - if (this.love === undefined) return + if (this.love === undefined) { + this.ctx.error('Love is not initialized') + return + } return this.love.getIdentity() } diff --git a/services/love/src/main.ts b/services/love/src/main.ts index b57b0af9ce..6cd98dc85b 100644 --- a/services/love/src/main.ts +++ b/services/love/src/main.ts @@ -180,7 +180,7 @@ export const main = async (): Promise => { const metadata = language != null ? { transcription, language } : { transcription } try { await updateMetadata(roomClient, roomName, metadata) - res.send() + res.status(200).send() } catch (e) { console.error(e) res.status(500).send()