Initial meeting minutes (#7111)
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-11-07 14:08:15 +04:00 committed by GitHub
parent 1ce18226ab
commit e34f59ca37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
111 changed files with 2543 additions and 589 deletions

14
.vscode/launch.json vendored
View File

@ -86,7 +86,7 @@
// "WS_LIVENESS_DAYS": "1",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost",
"MINIO_ENDPOINT": "localhost"
// "DISABLE_SIGNUP": "true",
// "INIT_SCRIPT_URL": "https://raw.githubusercontent.com/hcengineering/init/main/script.yaml",
// "INIT_WORKSPACE": "onboarding",
@ -105,7 +105,7 @@
"args": ["src/__start.ts"],
"env": {
"PORT": "4900",
"SERVER_SECRET": "secret",
"SERVER_SECRET": "secret"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
@ -241,7 +241,7 @@
"protocol": "inspector",
"outputCapture": "std",
"runtimeVersion": "20",
"showAsyncStacks": true,
"showAsyncStacks": true
},
{
"name": "Debug tool upgrade",
@ -304,7 +304,7 @@
"SECRET": "secret",
"REGION": "pg",
"BUCKET_NAME":"backups",
"INTERVAL":"30",
"INTERVAL":"30"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"showAsyncStacks": true,
@ -472,7 +472,7 @@
"PASSWORD": "password",
"AVATAR_PATH": "./assets/avatar.png",
"AVATAR_CONTENT_TYPE": ".png",
"OPENAI_API_KEY": "token"
"LOVE_ENDPOINT": "http://localhost:8096"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
@ -519,7 +519,7 @@
"args": ["src/__start.ts", "import-notion-to-teamspace", "/home/anna/work/notion/natalya/Export-fad9ecb4-a1a5-4623-920d-df32dd423743", "-u", "user1", "-pw", "1234", "-ws", "ws5", "-ts", "notion", ],
// "args": ["src/__start.ts", "import-notion-with-teamspaces", "/home/anna/work/notion/natalya/Export-fad9ecb4-a1a5-4623-920d-df32dd423743", "-u", "user1", "-pw", "1234", "-ws", "ws1"],
"env": {
"FRONT_URL": "http://localhost:8087",
"FRONT_URL": "http://localhost:8087"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
@ -533,7 +533,7 @@
"request": "launch",
"args": ["src/__start.ts", "import-clickup-tasks", "/home/anna/work/clickup/aleksandr/debug/mentions.csv", "-u", "user1", "-pw", "1234", "-ws", "ws5"],
"env": {
"FRONT_URL": "http://localhost:8087",
"FRONT_URL": "http://localhost:8087"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],

View File

@ -385,6 +385,7 @@ services:
- AVATAR_PATH=./avatar.png
- AVATAR_CONTENT_TYPE=.png
- STATS_URL=http://host.docker.internal:4900
# - LOVE_ENDPOINT=http://host.docker.internal:8096
# - OPENAI_API_KEY=token
deploy:
resources:

View File

@ -77,7 +77,7 @@ function defineMessageActions (builder: Builder): void {
group: 'edit'
}
},
chunter.action.ReplyToThreadAction
activity.action.Reply
)
createAction(

View File

@ -45,8 +45,7 @@ export default mergeIds(chunterId, chunter, {
ArchiveChannel: '' as Ref<Action>,
UnarchiveChannel: '' as Ref<Action>,
ConvertToPrivate: '' as Ref<Action>,
CopyChatMessageLink: '' as Ref<Action<Doc, any>>,
ReplyToThreadAction: '' as Ref<Action>
CopyChatMessageLink: '' as Ref<Action<Doc, any>>
},
actionImpl: {
ArchiveChannel: '' as ViewAction,

View File

@ -28,24 +28,25 @@
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/platform": "^0.6.11",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/core": "^0.6.32",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/model": "^0.6.11",
"@hcengineering/setting": "^0.6.17",
"@hcengineering/workbench": "^0.6.16",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/model-preference": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/drive": "^0.6.0",
"@hcengineering/view": "^0.6.13",
"@hcengineering/love": "^0.6.0",
"@hcengineering/love-resources": "^0.6.0",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/model": "^0.6.11",
"@hcengineering/model-calendar": "^0.6.0",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/model-calendar": "^0.6.0"
"@hcengineering/model-preference": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/setting": "^0.6.17",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13",
"@hcengineering/workbench": "^0.6.16"
}
}

View File

@ -28,9 +28,22 @@ import {
type Room,
type RoomAccess,
type RoomInfo,
type RoomType
type RoomType,
type RoomLanguage,
type MeetingMinutes
} from '@hcengineering/love'
import { type Builder, Index, Mixin, Model, Prop, TypeRef } from '@hcengineering/model'
import {
type Builder,
Collection as PropCollection,
Hidden,
Index,
Mixin,
Model,
Prop,
TypeRef,
TypeString,
UX
} from '@hcengineering/model'
import calendar, { TEvent } from '@hcengineering/model-calendar'
import core, { TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
@ -40,6 +53,9 @@ import notification from '@hcengineering/notification'
import { getEmbeddedLabel } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import workbench, { WidgetType } from '@hcengineering/workbench'
import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import love from './plugin'
export { loveId } from '@hcengineering/love'
@ -54,7 +70,7 @@ export class TRoom extends TDoc implements Room {
access!: RoomAccess
@Prop(TypeRef(love.class.Floor), getEmbeddedLabel('Floor'))
@Prop(TypeRef(love.class.Floor), love.string.Floor)
// @Index(IndexKind.Indexed)
floor!: Ref<Floor>
@ -62,6 +78,9 @@ export class TRoom extends TDoc implements Room {
height!: number
x!: number
y!: number
language!: RoomLanguage
startWithTranscription!: boolean
}
@Model(love.class.Office, love.class.Room)
@ -82,7 +101,7 @@ export class TParticipantInfo extends TDoc implements ParticipantInfo {
@Prop(TypeRef(contact.class.Person), getEmbeddedLabel('Person'))
person!: Ref<Person>
@Prop(TypeRef(love.class.Room), getEmbeddedLabel('Room'))
@Prop(TypeRef(love.class.Room), love.string.Room)
room!: Ref<Room>
x!: number
@ -96,7 +115,7 @@ export class TJoinRequest extends TDoc implements JoinRequest {
@Prop(TypeRef(contact.class.Person), getEmbeddedLabel('From'))
person!: Ref<Person>
@Prop(TypeRef(love.class.Room), getEmbeddedLabel('Room'))
@Prop(TypeRef(love.class.Room), love.string.Room)
room!: Ref<Room>
status!: RequestStatus
@ -110,7 +129,7 @@ export class TInvite extends TDoc implements Invite {
@Prop(TypeRef(contact.class.Person), getEmbeddedLabel('Target'))
target!: Ref<Person>
@Prop(TypeRef(love.class.Room), getEmbeddedLabel('Room'))
@Prop(TypeRef(love.class.Room), love.string.Room)
room!: Ref<Room>
status!: RequestStatus
@ -136,6 +155,25 @@ export class TMeeting extends TEvent implements Meeting {
room!: Ref<Room>
}
@Model(love.class.MeetingMinutes, core.class.Doc, DOMAIN_LOVE)
@UX(love.string.Meeting)
export class TMeetingMinutes extends TDoc implements MeetingMinutes {
@Hidden()
sid!: string
@Prop(TypeString(), view.string.Title)
title!: string
@Prop(TypeRef(love.class.Room), love.string.Room)
room!: Ref<Room>
@Prop(PropCollection(activity.class.ActivityMessage), love.string.Transcription)
transcription?: number
@Prop(PropCollection(activity.class.ActivityMessage), activity.string.Messages)
messages?: number
}
export default love
export function createModel (builder: Builder): void {
@ -148,7 +186,8 @@ export function createModel (builder: Builder): void {
TDevicesPreference,
TRoomInfo,
TInvite,
TMeeting
TMeeting,
TMeetingMinutes
)
builder.createDoc(
@ -182,12 +221,12 @@ export function createModel (builder: Builder): void {
workbench.class.Widget,
core.space.Model,
{
label: love.string.MeetingRoom,
label: love.string.Meeting,
type: WidgetType.Flexible,
icon: love.icon.Cam,
component: love.component.VideoWidget
component: love.component.MeetingWidget
},
love.ids.VideoWidget
love.ids.MeetingWidget
)
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
@ -317,4 +356,39 @@ export function createModel (builder: Builder): void {
},
love.action.ToggleVideo
)
createAction(builder, {
action: love.actionImpl.CopyGuestLink,
label: love.string.CopyGuestLink,
icon: view.icon.Copy,
category: love.category.Office,
input: 'focus',
target: love.class.Room,
visibilityTester: love.function.CanCopyGuestLink,
context: {
mode: 'context'
}
})
createAction(builder, {
action: love.actionImpl.ShowRoomSettings,
label: love.string.Settings,
icon: view.icon.Setting,
category: love.category.Office,
input: 'focus',
target: love.class.Room,
visibilityTester: love.function.CanShowRoomSettings,
context: {
mode: 'context'
}
})
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, view.mixin.ObjectPresenter, {
presenter: love.component.MeetingMinutesPresenter
})
}

View File

@ -77,7 +77,9 @@ async function createReception (client: MigrationUpgradeClient): Promise<void> {
width: 100,
height: 0,
x: 0,
y: 0
y: 0,
language: 'en',
startWithTranscription: false
},
love.ids.Reception
)
@ -91,6 +93,31 @@ export const loveOperation: MigrateOperation = {
func: async (client: MigrationClient) => {
await migrateSpace(client, 'love:space:Rooms' as Ref<Space>, core.space.Workspace, [DOMAIN_LOVE])
}
},
{
state: 'setup-defaults-settings',
func: async (client: MigrationClient) => {
await client.update(
DOMAIN_LOVE,
{ _class: love.class.Room, language: { $exists: false } },
{ language: 'en' }
)
await client.update(
DOMAIN_LOVE,
{ _class: love.class.Office, language: { $exists: false } },
{ language: 'en' }
)
await client.update(
DOMAIN_LOVE,
{ _class: love.class.Room, startWithTranscription: { $exists: false } },
{ startWithTranscription: true }
)
await client.update(
DOMAIN_LOVE,
{ _class: love.class.Office, startWithTranscription: { $exists: false } },
{ startWithTranscription: false }
)
}
}
])
},

View File

@ -27,7 +27,7 @@ export default mergeIds(loveId, love, {
WorkbenchExtension: '' as AnyComponent,
Settings: '' as AnyComponent,
LoveWidget: '' as AnyComponent,
VideoWidget: '' as AnyComponent
MeetingWidget: '' as AnyComponent
},
app: {
Love: '' as Ref<Doc>
@ -37,7 +37,9 @@ export default mergeIds(loveId, love, {
},
actionImpl: {
ToggleMic: '' as ViewAction,
ToggleVideo: '' as ViewAction
ToggleVideo: '' as ViewAction,
ShowRoomSettings: '' as ViewAction,
CopyGuestLink: '' as ViewAction
},
ids: {
Settings: '' as Ref<Doc>,

View File

@ -94,6 +94,7 @@
"TypeHere": "Type here...",
"FullSize": "Full size",
"UseMaxWidth": "Max width",
"Sidebar": "Sidebar"
"Sidebar": "Sidebar",
"Language": "Language"
}
}

View File

@ -94,6 +94,7 @@
"TypeHere": "Escribe aquí...",
"FullSize": "Tamaño completo",
"UseMaxWidth": "Ancho máximo",
"Sidebar": "Barra lateral"
"Sidebar": "Barra lateral",
"Language": "Idioma"
}
}

View File

@ -94,6 +94,7 @@
"TypeHere": "Tapez ici...",
"FullSize": "Taille réelle",
"UseMaxWidth": "Largeur maximale",
"Sidebar": "Barre latérale"
"Sidebar": "Barre latérale",
"Language": "Langue"
}
}

View File

@ -94,6 +94,7 @@
"TypeHere": "Scrivi qui...",
"FullSize": "Dimensione intera",
"UseMaxWidth": "Larghezza massima",
"Sidebar": "Barra laterale"
"Sidebar": "Barra laterale",
"Language": "Lingua"
}
}

View File

@ -94,6 +94,7 @@
"TypeHere": "Escreva aqui...",
"FullSize": "Tamanho completo",
"UseMaxWidth": "Largura máxima",
"Sidebar": "Barra lateral"
"Sidebar": "Barra lateral",
"Language": "Idioma"
}
}

View File

@ -94,6 +94,7 @@
"TypeHere": "Вводите здесь...",
"FullSize": "Полный размер",
"UseMaxWidth": "Максимальная ширина",
"Sidebar": "Боковая панель"
"Sidebar": "Боковая панель",
"Language": "Язык"
}
}

View File

@ -94,6 +94,7 @@
"TypeHere": "在此输入...",
"FullSize": "全尺寸",
"UseMaxWidth": "使用最大宽度",
"Sidebar": "侧边栏"
"Sidebar": "侧边栏",
"Language": "语言"
}
}

View File

@ -26,6 +26,7 @@
import DropdownIcon from './icons/Dropdown.svelte'
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> = {}
export let label: IntlString = ui.string.DropdownDefaultLabel
export let params: Record<string, any> = {}
export let items: DropdownIntlItem[]
@ -78,6 +79,7 @@
<div bind:this={container} class:min-w-0={minW0}>
<Button
{icon}
{iconProps}
width={width ?? 'min-content'}
{size}
{kind}

View File

@ -17,7 +17,7 @@
import type { DropdownIntlItem } from '../types'
import IconCheck from './icons/Check.svelte'
import Label from './Label.svelte'
import { resizeObserver } from '..'
import { Icon, resizeObserver } from '..'
export let items: DropdownIntlItem[]
export let selected: DropdownIntlItem['id'] | undefined = undefined
@ -55,7 +55,12 @@
dispatch('close', item.id)
}}
>
<div class="flex-grow caption-color nowrap"><Label label={item.label} params={item.params ?? params} /></div>
<div class="flex-grow caption-color nowrap flex-presenter flex-gap-2">
{#if item.icon}
<Icon size="small" icon={item.icon} iconProps={item.iconProps} />
{/if}
<Label label={item.label} params={item.params ?? params} />
</div>
<div class="check">
{#if item.id === selected}<IconCheck size={'small'} />{/if}
</div>

View File

@ -29,8 +29,16 @@
export let orientation: 'horizontal' | 'vertical' = 'horizontal'
export let kind: 'primary' | 'secondary' = 'primary'
export let canClose = true
export let readonly = false
const dispatch = createEventDispatcher()
function handleContextMenu (e: MouseEvent): void {
if (readonly) return
e.preventDefault()
e.stopPropagation()
dispatch('contextmenu', e)
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@ -42,7 +50,7 @@
class:active={highlighted}
use:tooltip={{ label: label ? getEmbeddedLabel(label) : labelIntl }}
on:click
on:contextmenu
on:contextmenu={handleContextMenu}
>
<slot name="prefix" />
@ -69,7 +77,7 @@
{/if}
</span>
{#if canClose}
{#if canClose && !readonly}
<div class="close-button {orientation}">
<ButtonIcon icon={IconClose} size="min" on:click={() => dispatch('close')} />
</div>

View File

@ -121,7 +121,8 @@ export const uis = plugin(uiId, {
FullSize: '' as IntlString,
UseMaxWidth: '' as IntlString,
Sidebar: '' as IntlString
Sidebar: '' as IntlString,
Language: '' as IntlString
},
metadata: {
DefaultApplication: '' as Metadata<AnyComponent>,

View File

@ -327,6 +327,7 @@ export interface DropdownIntlItem {
id: string | number
label: IntlString
icon?: Asset | AnySvelteComponent | ComponentType
iconProps?: Record<string, any>
params?: Record<string, any>
description?: IntlString
paramsDescription?: Record<string, any>

View File

@ -30,6 +30,7 @@
export let withActionMenu = true
export let onOpen: () => void
export let onClose: () => void
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const client = getClient()
@ -76,22 +77,33 @@
.filter((action) => action.inline)
.filter((action) => !excludedAction.includes(action._id))
}
async function handleAction (action: ViewAction, ev?: Event): Promise<void> {
if (message === undefined) return
if (onReply !== undefined && action._id === activity.action.Reply) {
onReply(message)
handleActionMenuClosed()
return
}
const fn = await getResource(action.action)
await fn(message, ev, { onOpen, onClose })
}
</script>
{#if message}
<div class="activityMessage-actionPopup">
{#each inlineActions as inline}
{#if inline.icon}
{#await getResource(inline.action) then action}
<ActivityMessageAction
label={inline.label}
size={inline.actionProps?.size ?? 'small'}
icon={inline.icon}
iconProps={inline.actionProps?.iconProps}
dataId={inline._id}
action={(ev) => action(message, ev, { onOpen, onClose })}
action={(ev) => handleAction(inline, ev)}
/>
{/await}
{/if}
{/each}

View File

@ -29,6 +29,7 @@
export let object: ActivityMessage
export let embedded = false
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const client = getClient()
const maxDisplayPersons = 5
@ -82,6 +83,10 @@
e.stopPropagation()
e.preventDefault()
if (onReply) {
onReply(object)
}
if (replyProvider) {
const fn = await getResource(replyProvider.function)
await fn(object, e)

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { DisplayActivityMessage, ActivityMessageViewType } from '@hcengineering/activity'
import { DisplayActivityMessage, ActivityMessageViewType, ActivityMessage } from '@hcengineering/activity'
import view from '@hcengineering/view'
import { getClient } from '@hcengineering/presentation'
import { Action, Component } from '@hcengineering/ui'
@ -41,6 +41,7 @@
export let compact = false
export let readonly = false
export let onClick: (() => void) | undefined = undefined
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -74,7 +75,8 @@
type,
compact,
readonly,
onClick
onClick,
onReply
}}
/>
{/if}

View File

@ -16,7 +16,8 @@
import activity, {
ActivityMessageViewlet,
DisplayActivityMessage,
ActivityMessageViewType
ActivityMessageViewType,
ActivityMessage
} from '@hcengineering/activity'
import { Person } from '@hcengineering/contact'
import { Avatar, SystemAvatar } from '@hcengineering/contact-resources'
@ -63,6 +64,7 @@
export let excludedActions: Ref<ViewAction>[] = []
export let readonly: boolean = false
export let onClick: (() => void) | undefined = undefined
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
export let socialIcon: Asset | undefined = undefined
@ -146,9 +148,14 @@
if (readonly) return
const showCustomPopup = !isTextClicked(event.target as HTMLElement, event.clientX, event.clientY)
if (showCustomPopup) {
showMenu(event, { object: message, baseMenuClass: activity.class.ActivityMessage, excludedActions }, () => {
const overrides = onReply ? new Map([[activity.action.Reply, onReply]]) : []
showMenu(
event,
{ object: message, baseMenuClass: activity.class.ActivityMessage, excludedActions, overrides },
() => {
isActionsOpened = false
})
}
)
isActionsOpened = true
}
}
@ -247,7 +254,7 @@
<slot name="content" {readonly} />
{#if !hideFooter}
<Replies {embedded} object={message} />
<Replies {embedded} object={message} {onReply} />
{/if}
<ReactionsPresenter object={message} {readonly} />
{#if parentMessage && showEmbedded}
@ -263,6 +270,7 @@
{actions}
{withActionMenu}
{excludedActions}
{onReply}
onOpen={handleActionsOpened}
onClose={handleActionsClosed}
/>

View File

@ -31,6 +31,7 @@ import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platfor
import { plugin } from '@hcengineering/platform'
import { Preference } from '@hcengineering/preference'
import type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
import type { Action } from '@hcengineering/view'
/**
* @public
@ -343,5 +344,8 @@ export default plugin(activityId, {
backreference: {
// Update list of back references
Update: '' as Resource<(source: Doc, key: string, target: RelatedDocument[], label: IntlString) => Promise<void>>
},
action: {
Reply: '' as Ref<Action>
}
})

View File

@ -40,11 +40,12 @@
"dependencies": {
"@hcengineering/ai-bot": "^0.6.0",
"@hcengineering/analytics-collector": "^0.6.0",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/core": "^0.6.32",
"@hcengineering/love": "^0.6.0",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/presentation": "^0.6.3",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/chunter": "^0.6.20",
"svelte": "^4.2.12"
}
}

View File

@ -16,7 +16,7 @@
import { type Resources } from '@hcengineering/platform'
import OnboardingChannelPanelExtension from './components/OnboardingChannelAsideExtension.svelte'
export * from './utils'
export * from './requests'
export default async (): Promise<Resources> => ({
component: {

View File

@ -0,0 +1,108 @@
//
// 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.
//
import { concatLink, type Markup, type Ref } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import {
type ConnectMeetingRequest,
type DisconnectMeetingRequest,
type TranslateRequest,
type TranslateResponse
} from '@hcengineering/ai-bot'
import { type Room, type RoomLanguage } from '@hcengineering/love'
import aiBot from './plugin'
export async function translate (text: Markup, lang: string): Promise<TranslateResponse | undefined> {
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
const token = getMetadata(presentation.metadata.Token) ?? ''
if (url === '' || token === '') {
return undefined
}
try {
const req: TranslateRequest = { text, lang }
const resp = await fetch(concatLink(url, '/translate'), {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
})
if (!resp.ok) {
return undefined
}
return (await resp.json()) as TranslateResponse
} catch (error) {
console.error(error)
return undefined
}
}
export async function connectMeeting (
roomId: Ref<Room>,
sid: string,
language: RoomLanguage,
options: Partial<ConnectMeetingRequest>
): Promise<void> {
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
const token = getMetadata(presentation.metadata.Token) ?? ''
if (url === '' || token === '') {
return undefined
}
try {
const req: ConnectMeetingRequest = { roomId, roomSid: sid, transcription: options.transcription ?? false, language }
await fetch(concatLink(url, 'love/connect'), {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
})
} catch (error) {
console.error(error)
return undefined
}
}
export async function disconnectMeeting (roomId: Ref<Room>): Promise<void> {
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
const token = getMetadata(presentation.metadata.Token) ?? ''
if (url === '' || token === '') {
return undefined
}
try {
const req: DisconnectMeetingRequest = { roomId }
await fetch(concatLink(url, 'love/disconnect'), {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
})
} catch (error) {
console.error(error)
return undefined
}
}

View File

@ -1,49 +0,0 @@
//
// 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.
//
import { concatLink, type Markup } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { type TranslateRequest, type TranslateResponse } from '@hcengineering/ai-bot'
import aiBot from './plugin'
export async function translate (text: Markup, lang: string): Promise<TranslateResponse | undefined> {
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
const token = getMetadata(presentation.metadata.Token) ?? ''
if (url === '' || token === '') {
return undefined
}
try {
const req: TranslateRequest = { text, lang }
const resp = await fetch(concatLink(url, '/translate'), {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
})
if (!resp.ok) {
return undefined
}
return (await resp.json()) as TranslateResponse
} catch (error) {
console.error(error)
return undefined
}
}

View File

@ -40,6 +40,7 @@
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/core": "^0.6.32",
"@hcengineering/love": "^0.6.0",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/view": "^0.6.13"

View File

@ -15,6 +15,8 @@
import { Account, Class, Doc, Markup, Ref, Space, Timestamp } from '@hcengineering/core'
import { ChatMessage } from '@hcengineering/chunter'
import { Room, RoomLanguage } from '@hcengineering/love'
import { Person } from '@hcengineering/contact'
export enum AIEventType {
Message = 'message',
@ -56,3 +58,20 @@ export interface TranslateResponse {
text: Markup
lang: string
}
export interface ConnectMeetingRequest {
roomId: Ref<Room>
roomSid: string
language: RoomLanguage
transcription: boolean
}
export interface DisconnectMeetingRequest {
roomId: Ref<Room>
}
export interface PostTranscriptRequest {
transcript: string
participant: Ref<Person>
roomName: string
}

View File

@ -130,7 +130,6 @@ export async function openFilePreviewInSidebar (
id: file,
icon,
name,
widget: attachment.ids.PreviewWidget,
data: { file, name, contentType, metadata }
}
await createFn(widget, tab, true)

View File

@ -126,7 +126,8 @@ export class ChannelDataProvider implements IChannelDataProvider {
_class: Ref<Class<ActivityMessage>>,
selectedMsgId: Ref<ActivityMessage> | undefined,
loadAll = false,
withRefs = false
withRefs = false,
private readonly collection: string | undefined = undefined
) {
this.chatId = chatId
this.msgClass = _class
@ -197,7 +198,11 @@ export class ChannelDataProvider implements IChannelDataProvider {
this.metadataQuery.query(
this.msgClass,
{ attachedTo: this.chatId, space: this.space },
{
attachedTo: this.chatId,
space: this.space,
...(this.collection != null ? { collection: this.collection } : {})
},
(res) => {
this.updatesDates(res)
this.metadataStore.set(res)
@ -281,6 +286,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
{
attachedTo: this.chatId,
space: this.space,
...(this.collection != null ? { collection: this.collection } : {}),
...query,
...(this.tailStart !== undefined ? { createdOn: { $gte: this.tailStart } } : {})
},
@ -335,6 +341,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
{
attachedTo: this.chatId,
space: this.space,
...(this.collection != null ? { collection: this.collection } : {}),
createdOn: equal
? isBackward
? { $lte: loadAfter }

View File

@ -18,6 +18,7 @@
export let scroller: Scroller | undefined | null = undefined
export let scrollDiv: HTMLDivElement | undefined | null = undefined
export let contentDiv: HTMLDivElement | undefined | null = undefined
export let bottomStart: boolean = true
export let loadingOverlay: boolean = false
export let onScroll: () => void = () => {}
export let onResize: () => void = () => {}
@ -33,8 +34,8 @@
bind:divScroll={scrollDiv}
bind:divBox={contentDiv}
scrollDirection="vertical-reverse"
noStretch
bottomStart
noStretch={bottomStart}
{bottomStart}
disableOverscroll
disablePointerEventsOnScroll
{onScroll}

View File

@ -27,12 +27,13 @@
export let object: Doc
export let context: DocNotifyContext | undefined
export let filters: Ref<ActivityMessagesFilter>[] = []
export let isAsideOpened = false
export let syncLocation = true
export let autofocus = true
export let freeze = false
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let collection: string | undefined = undefined
export let withInput: boolean = true
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -65,7 +66,6 @@
let refsLoaded = false
$: isDocChannel = !hierarchy.isDerived(object._class, chunter.class.ChunterSpace)
$: collection = isDocChannel ? 'comments' : 'messages'
$: void updateDataProvider(object._id, selectedMessageId)
@ -90,7 +90,8 @@
activity.class.ActivityMessage,
selectedMessageId,
loadAll,
hasRefs
hasRefs,
collection
)
}
}
@ -106,10 +107,12 @@
channel={object}
bind:selectedMessageId
{object}
{collection}
collection={collection ?? (isDocChannel ? 'comments' : 'messages')}
provider={dataProvider}
{freeze}
{autofocus}
loadMoreAllowed={!isDocChannel}
{withInput}
{onReply}
/>
{/if}

View File

@ -0,0 +1,91 @@
<!--
// 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 { Doc, Ref } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { ActivityMessage } from '@hcengineering/activity'
import Channel from './Channel.svelte'
import { ThreadView } from '../index'
export let height: string
export let width: string
export let object: Doc
export let threadId: Ref<ActivityMessage> | undefined
export let collection: string | undefined = undefined
export let withInput: boolean = true
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const notificationsClient = InboxNotificationsClientImpl.getClient()
const contextByDocStore = notificationsClient.contextByDoc
let context: DocNotifyContext | undefined = undefined
$: context = object ? $contextByDocStore.get(object._id) : undefined
$: renderChannel = threadId === undefined
$: visible = height !== '0px' && width !== '0px'
</script>
{#if renderChannel && visible}
<div class="channel" class:invisible={threadId !== undefined} style:height style:width>
{#key object._id}
<Channel
{object}
{context}
syncLocation={false}
freeze={threadId !== undefined}
{collection}
{withInput}
{onReply}
/>
{/key}
</div>
{/if}
{#if threadId && visible}
<div class="thread" style:height style:width>
<ThreadView _id={threadId} syncLocation={false} {onReply} on:channel on:close />
</div>
{/if}
<style lang="scss">
.channel {
display: inline-flex;
flex-direction: column;
flex: 1;
min-width: 0;
min-height: 0;
position: absolute;
top: 0;
left: 0;
&.invisible {
visibility: hidden;
}
}
.thread {
position: absolute;
display: inline-flex;
flex-direction: column;
flex: 1;
min-width: 0;
min-height: 0;
top: 0;
left: 0;
background-color: var(--theme-panel-color);
}
</style>

View File

@ -25,12 +25,11 @@
Separator
} from '@hcengineering/ui'
import { DocNotifyContext } from '@hcengineering/notification'
import { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity'
import { ActivityMessage } from '@hcengineering/activity'
import { getClient } from '@hcengineering/presentation'
import { Channel, ObjectChatPanel } from '@hcengineering/chunter'
import view from '@hcengineering/view'
import { messageInFocus } from '@hcengineering/activity-resources'
import { onMount } from 'svelte'
import ChannelComponent from './Channel.svelte'
import ChannelHeader from './ChannelHeader.svelte'
@ -51,8 +50,6 @@
let isThreadOpened = false
let isAsideShown = false
let filters: Ref<ActivityMessagesFilter>[] = []
locationStore.subscribe((newLocation) => {
isThreadOpened = newLocation.path[4] != null
})
@ -111,7 +108,6 @@
_class={object._class}
{object}
{withAside}
bind:filters
canOpen={isDocChat}
allowClose={embedded}
{isAsideShown}
@ -140,13 +136,7 @@
</div>
</div>
{:else}
<ChannelComponent
{context}
{object}
{filters}
{autofocus}
isAsideOpened={(withAside && isAsideShown) || isThreadOpened}
/>
<ChannelComponent {context} {object} {autofocus} />
{/if}
{/key}
</div>

View File

@ -74,14 +74,14 @@
unsubscribe()
})
function handleMenu (event: MouseEvent): void {
function handleMenu (event: CustomEvent<MouseEvent>): void {
if (actions.length === 0) {
return
}
event.preventDefault()
event.stopPropagation()
showPopup(Menu, { actions }, event.target as HTMLElement)
showPopup(Menu, { actions }, event.detail.target as HTMLElement)
}
</script>

View File

@ -53,6 +53,8 @@
export let freeze = false
export let loadMoreAllowed = true
export let autofocus = true
export let withInput: boolean = true
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const minMsgHeightRem = 2
const loadMoreThreshold = 200
@ -572,6 +574,8 @@
window.removeEventListener('blur', handleWindowBlur)
removeTxListener(newMessageTxListener)
})
$: showBlankView = !$isLoadingStore && messages.length === 0 && !isThread && !readonly
</script>
<div class="flex-col relative" class:h-full={fullHeight}>
@ -584,11 +588,12 @@
bind:scroller
bind:scrollDiv
bind:contentDiv
bottomStart={!showBlankView}
loadingOverlay={$isLoadingStore || !isScrollInitialized}
onScroll={handleScroll}
onResize={handleResize}
>
{#if !$isLoadingStore && messages.length === 0 && !isThread && !readonly}
{#if showBlankView}
<BlankView
icon={chunter.icon.Thread}
header={chunter.string.NoMessagesInChannel}
@ -628,6 +633,7 @@
isHighlighted={isSelected}
shouldScroll={false}
{readonly}
{onReply}
/>
{/each}
@ -638,7 +644,7 @@
{#if loadMoreAllowed && $canLoadNextForwardStore}
<HistoryLoading isLoading={$isLoadingMoreStore} />
{/if}
{#if !fixedInput}
{#if !fixedInput && withInput}
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} {autofocus} />
{/if}
</BaseChatScroller>
@ -655,7 +661,7 @@
{/if}
</div>
{#if fixedInput}
{#if fixedInput && withInput}
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} {autofocus} />
{/if}

View File

@ -55,6 +55,7 @@
export let readonly = false
export let type: ActivityMessageViewType = 'default'
export let onClick: (() => void) | undefined = undefined
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const client = getClient()
const { pendingCreatedDocs } = client
@ -255,6 +256,7 @@
{inlineActions}
{type}
{onClick}
{onReply}
>
<svelte:fragment slot="header">
<ChatMessageHeader label={viewlet?.label} />

View File

@ -14,6 +14,7 @@
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let message: ActivityMessage
export let autofocus = true
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -69,10 +70,11 @@
{autofocus}
fullHeight={false}
fixedInput={false}
{onReply}
>
<svelte:fragment slot="header">
<div class="mt-3">
<ThreadParentMessage {message} {readonly} />
<ThreadParentMessage {message} {readonly} {onReply} />
</div>
{#if (message.replies ?? $messagesStore?.length ?? 0) > 0}

View File

@ -18,6 +18,7 @@
export let message: ActivityMessage
export let readonly = false
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
</script>
<ActivityMessagePresenter
@ -28,4 +29,5 @@
attachmentImageSize="x-large"
skipLabel
{readonly}
{onReply}
/>

View File

@ -32,6 +32,7 @@
export let showHeader: boolean = true
export let syncLocation = true
export let autofocus = true
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -144,7 +145,7 @@
{#if message}
{#key _id}
<ThreadContent bind:selectedMessageId {message} {autofocus} />
<ThreadContent bind:selectedMessageId {message} {autofocus} {onReply} />
{/key}
{:else if isLoading}
<Loading />

View File

@ -86,6 +86,7 @@ import {
startConversationAction
} from './utils'
export { default as ChannelEmbeddedContent } from './components/ChannelEmbeddedContent.svelte'
export { default as ChatMessageInput } from './components/chat-message/ChatMessageInput.svelte'
export { default as ChatMessagePopup } from './components/chat-message/ChatMessagePopup.svelte'
export { default as ChatMessagesPresenter } from './components/chat-message/ChatMessagesPresenter.svelte'

View File

@ -60,6 +60,13 @@
"ExitingFullscreenMode": "Exiting fullscreen mode",
"Select": "Select",
"ChooseShare": "Choose what to share",
"CreateMeeting": "Create meeting"
"CreateMeeting": "Create meeting",
"MoreOptions": "More options",
"StartTranscription": "Start transcription",
"StopTranscription": "Stop transcription",
"Meeting": "Meeting",
"Transcription": "Transcription",
"StartWithTranscription": "Start with transcription",
"MeetingMinutes": "Meeting minutes"
}
}

View File

@ -60,6 +60,13 @@
"ExitingFullscreenMode": "Salir del modo de pantalla completa",
"Select": "Seleccionar",
"ChooseShare": "Elija qué compartir",
"CreateMeeting": "Crear reunión"
"CreateMeeting": "Crear reunión",
"MoreOptions": "Más opciones",
"StartTranscription": "Iniciar transcripción",
"StopTranscription": "Detener transcripción",
"Meeting": "Reunión",
"Transcription": "Transcripción",
"StartWithTranscription": "Iniciar con transcripción",
"MeetingMinutes": "Minutos de la reunión"
}
}

View File

@ -60,6 +60,13 @@
"ExitingFullscreenMode": "Quitter le mode plein écran",
"Select": "Sélectionner",
"ChooseShare": "Choisissez ce que vous voulez partager",
"CreateMeeting": "Créer une réunion"
"CreateMeeting": "Créer une réunion",
"MoreOptions": "Plus d'options",
"StartTranscription": "Démarrer la transcription",
"StopTranscription": "Arrêter la transcription",
"Meeting": "Réunion",
"Transcription": "Transcription",
"StartWithTranscription": "Démarrer avec la transcription",
"MeetingMinutes": "Minutes de la réunion"
}
}

View File

@ -60,6 +60,13 @@
"ExitingFullscreenMode": "Uscita dalla modalità a schermo intero",
"Select": "Seleziona",
"ChooseShare": "Scegli cosa condividere",
"CreateMeeting": "Crea riunione"
"CreateMeeting": "Crea riunione",
"MoreOptions": "Altre opzioni",
"StartTranscription": "Avvia trascrizione",
"StopTranscription": "Interrompi trascrizione",
"Meeting": "Riunione",
"Transcription": "Trascrizione",
"StartWithTranscription": "Inizia con la trascrizione",
"MeetingMinutes": "Verbale della riunione"
}
}

View File

@ -60,6 +60,13 @@
"ExitingFullscreenMode": "Saindo do modo de tela cheia",
"Select": "Seleccione",
"ChooseShare": "Escolha o que partilhar",
"CreateMeeting": "Criar reunião"
"CreateMeeting": "Criar reunião",
"MoreOptions": "Mais opções",
"StartTranscription": "Iniciar transcrição",
"StopTranscription": "Parar transcrição",
"Meeting": "Reunião",
"Transcription": "Transcrição",
"StartWithTranscription": "Começar com transcrição",
"MeetingMinutes": "Minutos da reunião"
}
}

View File

@ -60,6 +60,13 @@
"ExitingFullscreenMode": "Выход из полноэкранного режима",
"Select": "Выбрать",
"ChooseShare": "Выберите, чем вы хотите поделиться",
"CreateMeeting": "Создать встречу"
"CreateMeeting": "Создать встречу",
"MoreOptions": "Дополнительные опции",
"StartTranscription": "Начать транскрипцию",
"StopTranscription": "Остановить транскрипцию",
"Meeting": "Встреча",
"Transcription": "Транскрипция",
"StartWithTranscription": "Начинать с транскрипцией",
"MeetingMinutes": "Протоколы встреч"
}
}

View File

@ -60,6 +60,13 @@
"ExitingFullscreenMode": "退出全屏模式",
"Select": "选择",
"ChooseShare": "选择共享内容",
"CreateMeeting": "创建会议"
"CreateMeeting": "创建会议",
"MoreOptions": "更多选项",
"StartTranscription": "开始转录",
"StopTranscription": "停止转录",
"Meeting": "会议",
"Transcription": "转录",
"StartWithTranscription": "开始转录",
"MeetingMinutes": "会议记录"
}
}

View File

@ -37,8 +37,13 @@
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/ai-bot": "^0.6.0",
"@hcengineering/ai-bot-resources": "^0.6.0",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/calendar": "^0.6.24",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/chunter-resources": "^0.6.0",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/core": "^0.6.32",

View File

@ -59,7 +59,9 @@
width: 2,
height: 1,
type: val.type,
access: val.access
access: val.access,
language: 'en',
startWithTranscription: val._class !== love.class.Office
}
if (val._class === love.class.Office) {
;(data as Data<Office>).person = null

View File

@ -15,31 +15,37 @@
<script lang="ts">
import { PersonAccount } from '@hcengineering/contact'
import { AccountRole, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import login from '@hcengineering/login'
import love, { Room, RoomType, isOffice, roomAccessIcon } from '@hcengineering/love'
import { Room, RoomType, isOffice, roomAccessIcon } from '@hcengineering/love'
import { getResource } from '@hcengineering/platform'
import { copyTextToClipboard, getClient } from '@hcengineering/presentation'
import { getClient } from '@hcengineering/presentation'
import {
IconUpOutline,
ModernButton,
PopupInstance,
SplitButton,
eventToHTMLElement,
getCurrentLocation,
showPopup,
type AnySvelteComponent,
type CompAndProps,
resizeObserver
resizeObserver,
IconMoreV,
ButtonMenu,
DropdownIntlItem
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import plugin from '../plugin'
import view, { Action } from '@hcengineering/view'
import { getActions } from '@hcengineering/view-resources'
import { afterUpdate } from 'svelte'
import love from '../plugin'
import { currentRoom, myInfo, myOffice } from '../stores'
import {
isTranscriptionAllowed,
isCameraEnabled,
isConnected,
isFullScreen,
isMicEnabled,
isRecording,
isTranscription,
isRecordingAvailable,
isSharingEnabled,
leaveRoom,
@ -47,12 +53,14 @@
screenSharing,
setCam,
setMic,
setShare
setShare,
stopTranscription,
startTranscription
} from '../utils'
import CamSettingPopup from './CamSettingPopup.svelte'
import MicSettingPopup from './MicSettingPopup.svelte'
import RoomAccessPopup from './RoomAccessPopup.svelte'
import { afterUpdate } from 'svelte'
import RoomLanguageSelector from './RoomLanguageSelector.svelte'
export let room: Room
export let fullScreen: boolean = false
@ -68,7 +76,7 @@
let combinePanel: boolean = false
$: allowCam = $currentRoom?.type === RoomType.Video
$: allowLeave = $myInfo?.room !== ($myOffice?._id ?? plugin.ids.Reception)
$: allowLeave = $myInfo?.room !== ($myOffice?._id ?? love.ids.Reception)
async function changeMute (): Promise<void> {
await setMic(!$isMicEnabled)
@ -124,38 +132,11 @@
}
}
async function getLink (): Promise<string> {
const roomInfo = await client.findOne(love.class.RoomInfo, { room: room._id })
if (roomInfo !== undefined) {
const navigateUrl = getCurrentLocation()
navigateUrl.query = {
sessionId: roomInfo._id
}
const func = await getResource(login.function.GetInviteLink)
return await func(24, '', -1, AccountRole.Guest, encodeURIComponent(JSON.stringify(navigateUrl)))
}
return ''
}
async function copyGuestLink (): Promise<void> {
await copyTextToClipboard(getLink())
linkCopied = true
clearTimeout(linkTimeout)
linkTimeout = setTimeout(() => {
linkCopied = false
}, 3000)
}
let linkCopied: boolean = false
let linkTimeout: any | undefined = undefined
const me = (getCurrentAccount() as PersonAccount).person
const client = getClient()
const camKeys = client.getModel().findAllSync(view.class.Action, { _id: plugin.action.ToggleVideo })?.[0]?.keyBinding
const micKeys = client.getModel().findAllSync(view.class.Action, { _id: plugin.action.ToggleMic })?.[0]?.keyBinding
const camKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleVideo })?.[0]?.keyBinding
const micKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleMic })?.[0]?.keyBinding
const checkBar = (): void => {
if (grow === undefined || leftPanel === undefined) return
@ -168,14 +149,43 @@
afterUpdate(() => {
checkBar()
})
let actions: Action[] = []
let moreItems: DropdownIntlItem[] = []
$: void getActions(client, room, love.class.Room).then((res) => {
actions = res
})
$: moreItems = actions.map((action) => ({
id: action._id,
label: action.label,
icon: action.icon
}))
async function handleMenuOption (e: CustomEvent<DropdownIntlItem['id']>): Promise<void> {
const action = actions.find((action) => action._id === e.detail)
if (action !== undefined) {
await handleAction(action)
}
}
async function handleAction (action: Action): Promise<void> {
const fn = await getResource(action.action)
await fn(room)
}
</script>
<div class="bar w-full flex-center flex-gap-2 flex-no-shrink" class:combinePanel use:resizeObserver={checkBar}>
<div class="bar__right-panel flex-gap-2 flex-center">
{#if $isConnected && isTranscriptionAllowed() && $isTranscription}
<RoomLanguageSelector {room} kind="icon" />
{/if}
</div>
<div bind:this={grow} class="flex-grow" />
{#if room._id !== plugin.ids.Reception}
{#if room._id !== love.ids.Reception}
<ModernButton
icon={roomAccessIcon[room.access]}
tooltip={{ label: plugin.string.ChangeAccess }}
tooltip={{ label: love.string.ChangeAccess }}
kind={'secondary'}
size={'large'}
disabled={isOffice(room) && room.person !== me}
@ -185,8 +195,8 @@
{#if $isConnected}
<SplitButton
size={'large'}
icon={$isMicEnabled ? plugin.icon.MicEnabled : plugin.icon.MicDisabled}
showTooltip={{ label: $isMicEnabled ? plugin.string.Mute : plugin.string.UnMute, keys: micKeys }}
icon={$isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
showTooltip={{ label: $isMicEnabled ? love.string.Mute : love.string.UnMute, keys: micKeys }}
action={changeMute}
secondIcon={IconUpOutline}
secondAction={micSettings}
@ -195,8 +205,8 @@
{#if allowCam}
<SplitButton
size={'large'}
icon={$isCameraEnabled ? plugin.icon.CamEnabled : plugin.icon.CamDisabled}
showTooltip={{ label: $isCameraEnabled ? plugin.string.StopVideo : plugin.string.StartVideo, keys: camKeys }}
icon={$isCameraEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
showTooltip={{ label: $isCameraEnabled ? love.string.StopVideo : love.string.StartVideo, keys: camKeys }}
disabled={!$isConnected}
action={changeCam}
secondIcon={IconUpOutline}
@ -206,8 +216,8 @@
{/if}
{#if allowShare}
<ModernButton
icon={$isSharingEnabled ? plugin.icon.SharingEnabled : plugin.icon.SharingDisabled}
tooltip={{ label: $isSharingEnabled ? plugin.string.StopShare : plugin.string.Share }}
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
tooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
disabled={($screenSharing && !$isSharingEnabled) || !$isConnected}
kind={'secondary'}
size={'large'}
@ -216,21 +226,37 @@
{/if}
{#if hasAccountRole(getCurrentAccount(), AccountRole.User) && $isRecordingAvailable}
<ModernButton
icon={$isRecording ? plugin.icon.StopRecord : plugin.icon.Record}
tooltip={{ label: $isRecording ? plugin.string.StopRecord : plugin.string.Record }}
icon={$isRecording ? love.icon.StopRecord : love.icon.Record}
tooltip={{ label: $isRecording ? love.string.StopRecord : love.string.Record }}
disabled={!$isConnected}
kind={'secondary'}
size={'large'}
on:click={() => record(room)}
/>
{/if}
{#if hasAccountRole(getCurrentAccount(), AccountRole.User) && isTranscriptionAllowed() && $isConnected}
<ModernButton
icon={view.icon.Feather}
iconProps={$isTranscription ? { fill: 'var(--button-negative-BackgroundColor)' } : {}}
tooltip={{ label: $isTranscription ? love.string.StopTranscription : love.string.StartTranscription }}
kind="secondary"
size="large"
on:click={() => {
if ($isTranscription) {
void stopTranscription(room)
} else {
void startTranscription(room)
}
}}
/>
{/if}
{/if}
<div bind:this={leftPanel} class="bar__left-panel flex-gap-2 flex-center">
{#if $isConnected}
<ModernButton
icon={$isFullScreen ? love.icon.ExitFullScreen : love.icon.FullScreen}
tooltip={{
label: $isFullScreen ? plugin.string.ExitingFullscreenMode : plugin.string.FullscreenMode,
label: $isFullScreen ? love.string.ExitingFullscreenMode : love.string.FullscreenMode,
direction: 'top'
}}
kind={'secondary'}
@ -240,20 +266,22 @@
}}
/>
{/if}
{#if hasAccountRole(getCurrentAccount(), AccountRole.User) && $isConnected}
<ModernButton
icon={view.icon.Copy}
tooltip={{ label: !linkCopied ? plugin.string.CopyGuestLink : view.string.Copied, direction: 'top' }}
kind={'secondary'}
size={'large'}
on:click={copyGuestLink}
{#if $isConnected && moreItems.length > 0}
<ButtonMenu
items={moreItems}
icon={IconMoreV}
tooltip={{ label: love.string.MoreOptions, direction: 'top' }}
kind="secondary"
size="large"
noSelection
on:selected={handleMenuOption}
/>
{/if}
{#if allowLeave}
<ModernButton
icon={plugin.icon.LeaveRoom}
label={noLabel ? undefined : plugin.string.LeaveRoom}
tooltip={{ label: plugin.string.LeaveRoom, direction: 'top' }}
icon={love.icon.LeaveRoom}
label={noLabel ? undefined : love.string.LeaveRoom}
tooltip={{ label: love.string.LeaveRoom, direction: 'top' }}
kind={'negative'}
size={'large'}
on:click={leave}
@ -293,8 +321,20 @@
height: 100%;
}
&__right-panel {
position: absolute;
top: 0;
bottom: 0;
left: 1rem;
height: 100%;
}
&.combinePanel .bar__left-panel {
position: static;
}
&.combinePanel .bar__right-panel {
position: static;
}
}
</style>

View File

@ -17,7 +17,6 @@
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref, toIdMap } from '@hcengineering/core'
import {
Floor,
Invite,
isOffice,
JoinRequest,
@ -29,7 +28,7 @@
RoomType
} from '@hcengineering/love'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
closePopup,
eventToHTMLElement,
@ -39,84 +38,67 @@
showPopup,
tooltip
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { onDestroy } from 'svelte'
import workbench from '@hcengineering/workbench'
import {
closeWidget,
closeWidgetTab,
minimizeSidebar,
openWidget,
sidebarStore,
SidebarVariant
SidebarVariant,
updateWidgetState
} from '@hcengineering/workbench-resources'
import love from '../plugin'
import { activeInvites, currentRoom, infos, myInfo, myInvites, myOffice, myRequests, rooms } from '../stores'
import {
activeFloor,
activeInvites,
currentRoom,
floors,
infos,
myInfo,
myInvites,
myOffice,
myRequests,
rooms
} from '../stores'
import {
createMeetingVideoWidgetTab,
createMeetingWidget,
disconnect,
getRoomName,
isCameraEnabled,
isConnected,
isCurrentInstanceConnected,
isMicEnabled,
isSharingEnabled,
leaveRoom,
screenSharing,
setCam,
setMic,
setShare
screenSharing
} from '../utils'
import ActiveInvitesPopup from './ActiveInvitesPopup.svelte'
import CamSettingPopup from './CamSettingPopup.svelte'
import FloorPopup from './FloorPopup.svelte'
import InvitePopup from './InvitePopup.svelte'
import MicSettingPopup from './MicSettingPopup.svelte'
import PersonActionPopup from './PersonActionPopup.svelte'
import RequestPopup from './RequestPopup.svelte'
import RequestingPopup from './RequestingPopup.svelte'
import RoomPopup from './RoomPopup.svelte'
let allowCam: boolean = false
let allowLeave: boolean = false
const client = getClient()
$: allowCam = $currentRoom?.type === RoomType.Video
$: allowLeave = $myInfo !== undefined && $myInfo.room !== ($myOffice?._id ?? love.ids.Reception)
// let allowCam: boolean = false
// let allowLeave: boolean = false
//
// $: allowCam = $currentRoom?.type === RoomType.Video
// $: allowLeave = $myInfo !== undefined && $myInfo.room !== ($myOffice?._id ?? love.ids.Reception)
async function changeMute (): Promise<void> {
if (!$isConnected || $currentRoom?.type === RoomType.Reception) return
await setMic(!$isMicEnabled)
}
async function changeCam (): Promise<void> {
if (!$isConnected || !allowCam) return
await setCam(!$isCameraEnabled)
}
async function changeShare (): Promise<void> {
if (!$isConnected) return
await setShare(!$isSharingEnabled)
}
async function leave (): Promise<void> {
showPopup(MessageBox, {
label: love.string.LeaveRoom,
message: love.string.LeaveRoomConfirmation,
action: async () => {
await leaveRoom($myInfo, $myOffice)
}
})
}
// async function changeMute (): Promise<void> {
// if (!$isConnected || $currentRoom?.type === RoomType.Reception) return
// await setMic(!$isMicEnabled)
// }
//
// async function changeCam (): Promise<void> {
// if (!$isConnected || !allowCam) return
// await setCam(!$isCameraEnabled)
// }
//
// async function changeShare (): Promise<void> {
// if (!$isConnected) return
// await setShare(!$isSharingEnabled)
// }
//
// async function leave (): Promise<void> {
// showPopup(MessageBox, {
// label: love.string.LeaveRoom,
// message: love.string.LeaveRoomConfirmation,
// action: async () => {
// await leaveRoom($myInfo, $myOffice)
// }
// })
// }
interface ActiveRoom extends Room {
participants: ParticipantInfo[]
@ -142,17 +124,17 @@
return arr
}
let selectedFloor: Floor | undefined = $floors.find((f) => f._id === $activeFloor)
$: selectedFloor = $floors.find((f) => f._id === $activeFloor)
// 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)
})
}
// 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[] = []
@ -279,68 +261,90 @@
$: checkActiveInvites($activeInvites)
function micSettings (e: MouseEvent): void {
e.preventDefault()
showPopup(MicSettingPopup, {}, eventToHTMLElement(e))
}
// function micSettings (e: MouseEvent): void {
// e.preventDefault()
// showPopup(MicSettingPopup, {}, eventToHTMLElement(e))
// }
//
// function camSettings (e: MouseEvent): void {
// e.preventDefault()
// showPopup(CamSettingPopup, {}, eventToHTMLElement(e))
// }
function camSettings (e: MouseEvent): void {
e.preventDefault()
showPopup(CamSettingPopup, {}, eventToHTMLElement(e))
}
$: isVideoWidgetOpened = $sidebarStore.widgetsState.has(love.ids.VideoWidget)
let prevLocation: Location = $location
$: isMeetingWidgetOpened = $sidebarStore.widgetsState.has(love.ids.MeetingWidget)
$: widgetState = $sidebarStore.widgetsState.get(love.ids.MeetingWidget)
$: if (
isVideoWidgetOpened &&
isMeetingWidgetOpened &&
$sidebarStore.widget === undefined &&
$location.path[2] !== loveId &&
$sidebarStore.widgetsState.get(love.ids.VideoWidget)?.closedByUser !== true
widgetState !== undefined &&
widgetState.closedByUser !== true &&
widgetState.tabs.some(({ id }) => id === 'video')
) {
sidebarStore.update((s) => ({ ...s, widget: love.ids.VideoWidget, variant: SidebarVariant.EXPANDED }))
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<Room> | undefined): void {
const widgetState = $sidebarStore.widgetsState.get(love.ids.VideoWidget)
const isOpened = widgetState !== undefined
const meetingWidgetState = $sidebarStore.widgetsState.get(love.ids.MeetingWidget)
const isMeetingWidgetCreated = meetingWidgetState !== undefined
if (room === undefined) {
if (isOpened) {
closeWidget(love.ids.VideoWidget)
if (isMeetingWidgetCreated) {
closeWidget(love.ids.MeetingWidget)
}
return
}
if (video) {
if (!isOpened) {
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: love.ids.VideoWidget })[0]
if ($isCurrentInstanceConnected) {
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: love.ids.MeetingWidget })[0]
if (widget === undefined) return
openWidget(
widget,
{
room
},
{ active: loc.path[2] !== loveId, openedByUser: false }
)
// Create widget in sidebar if not created
if (!isMeetingWidgetCreated) {
prevLocation = loc
createMeetingWidget(widget, room, loc, video)
} else if (video && !meetingWidgetState.tabs.some(({ id }) => id === 'video')) {
createMeetingVideoWidgetTab(widget, loc)
} 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 &&
$sidebarStore.widget === love.ids.VideoWidget &&
widgetState?.openedByUser !== true
prevLocation.path[2] !== loveId &&
$sidebarStore.widget === love.ids.MeetingWidget &&
widgetState !== undefined &&
widgetState.tab === 'video'
) {
minimizeSidebar()
}
} else {
if (isOpened) {
closeWidget(love.ids.VideoWidget)
if (isMeetingWidgetCreated) {
closeWidget(love.ids.MeetingWidget)
}
}
prevLocation = loc
}
$: checkActiveVideo(
$location,
$isCurrentInstanceConnected && ($currentRoom?.type === RoomType.Video || ($screenSharing && !isSharingEnabled)),
$isCurrentInstanceConnected && ($currentRoom?.type === RoomType.Video || $screenSharing),
$currentRoom?._id
)
@ -351,14 +355,9 @@
closePopup(inviteCategory)
closePopup(joinRequestCategory)
closePopup(myJoinRequestCategory)
closeWidget(love.ids.VideoWidget)
closeWidget(love.ids.MeetingWidget)
})
const client = getClient()
const camKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleVideo })?.[0]?.keyBinding
const micKeys = client.getModel().findAllSync(view.class.Action, { _id: love.action.ToggleMic })?.[0]?.keyBinding
function participantClickHandler (e: MouseEvent, participant: ParticipantInfo): void {
if ($myInfo !== undefined) {
showPopup(PersonActionPopup, { room: reception, person: participant.person }, eventToHTMLElement(e))

View File

@ -14,38 +14,48 @@
-->
<script lang="ts">
import { AccountRole, Ref, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import { Breadcrumb, Header, IconEdit, ModernButton, Scroller } from '@hcengineering/ui'
import { Floor, ParticipantInfo, Room } from '@hcengineering/love'
import { Breadcrumb, Header, IconEdit, ModernButton, Switcher } from '@hcengineering/ui'
import { Floor, Room } from '@hcengineering/love'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import lovePlg from '../plugin'
import { currentRoom, floors, infos } from '../stores'
import { calculateFloorSize } from '../utils'
import { currentRoom, floors } from '../stores'
import ControlBar from './ControlBar.svelte'
import FloorGrid from './FloorGrid.svelte'
import RoomPreview from './RoomPreview.svelte'
import MeetingsTable from './MeetingMinutesTable.svelte'
import FloorView from './FloorView.svelte'
export let rooms: Room[] = []
export let floor: Ref<Floor>
const dispatch = createEventDispatcher()
let floorContainer: HTMLDivElement
let selectedViewlet: 'meetingMinutes' | 'floor' = 'floor'
$: selectedFloor = $floors.filter((fl) => fl._id === floor)[0]
function getInfo (room: Ref<Room>, info: ParticipantInfo[]): ParticipantInfo[] {
return info.filter((p) => p.room === room)
}
const me = getCurrentAccount()
let editable: boolean = false
$: editable = hasAccountRole(me, AccountRole.Maintainer)
$: rows = calculateFloorSize(rooms) - 1
</script>
<div class="hulyComponent">
<Header allowFullsize adaptive={'disabled'}>
<Breadcrumb title={selectedFloor?.name ?? ''} size={'large'} isCurrent />
<svelte:fragment slot="beforeTitle">
<Switcher
selected={selectedViewlet}
items={[
{ id: 'floor', icon: lovePlg.icon.Love, tooltip: lovePlg.string.Floor },
{ id: 'meetingMinutes', icon: view.icon.Table, tooltip: lovePlg.string.MeetingMinutes }
]}
kind="subtle"
name="selector"
on:select={(e) => {
selectedViewlet = e.detail.id
}}
/>
</svelte:fragment>
<svelte:fragment slot="actions">
{#if editable}
<ModernButton
@ -58,13 +68,11 @@
</svelte:fragment>
</Header>
<div class="hulyComponent-content__column content">
<Scroller padding={'1rem'} bottomPadding={'4rem'} horizontal>
<FloorGrid bind:floorContainer {rows} preview>
{#each rooms as room}
<RoomPreview {room} info={getInfo(room._id, $infos)} />
{/each}
</FloorGrid>
</Scroller>
{#if selectedViewlet === 'meetingMinutes'}
<MeetingsTable />
{:else}
<FloorView {rooms} />
{/if}
</div>
{#if $currentRoom}
<ControlBar room={$currentRoom} />

View File

@ -0,0 +1,41 @@
<!--
// 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 { Scroller } from '@hcengineering/ui'
import { ParticipantInfo, Room } from '@hcengineering/love'
import { infos } from '../stores'
import { calculateFloorSize } from '../utils'
import FloorGrid from './FloorGrid.svelte'
import RoomPreview from './RoomPreview.svelte'
export let rooms: Room[] = []
let floorContainer: HTMLDivElement
function getInfo (room: Ref<Room>, info: ParticipantInfo[]): ParticipantInfo[] {
return info.filter((p) => p.room === room)
}
$: rows = calculateFloorSize(rooms) - 1
</script>
<Scroller padding="1rem" bottomPadding="4rem" horizontal>
<FloorGrid bind:floorContainer {rows} preview>
{#each rooms as room}
<RoomPreview {room} info={getInfo(room._id, $infos)} />
{/each}
</FloorGrid>
</Scroller>

View File

@ -17,10 +17,10 @@
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { getClient } from '@hcengineering/presentation'
import { Button, Label } from '@hcengineering/ui'
import { Invite, RequestStatus } from '@hcengineering/love'
import { Invite, RequestStatus, getFreeRoomPlace } from '@hcengineering/love'
import love from '../plugin'
import { infos, myInfo, rooms } from '../stores'
import { connectRoom, getFreePlace } from '../utils'
import { connectRoom } from '../utils'
export let invite: Invite
@ -35,9 +35,10 @@
if (myPerson === undefined) return
if ($myInfo === undefined) return
await client.update(invite, { status: RequestStatus.Approved })
const place = getFreePlace(
const place = getFreeRoomPlace(
room,
$infos.filter((p) => p.room === room?._id)
$infos.filter((p) => p.room === room?._id),
myPerson._id
)
await connectRoom(place.x, place.y, $myInfo, myPerson, room)
}
@ -78,12 +79,12 @@
.title {
color: var(--caption-color);
font-size: 700;
font-weight: 700;
}
.roomTitle {
color: var(--caption-color);
font-size: 700;
font-weight: 700;
font-size: 1rem;
}
</style>

View File

@ -0,0 +1,36 @@
<!--
// 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 { RoomLanguage } from '@hcengineering/love'
import { languagesDisplayData } from '../types'
export let lang: RoomLanguage
export let size: 'small' | 'medium' = 'small'
</script>
<div class={size}>
{languagesDisplayData[lang]?.emoji}
</div>
<style lang="scss">
.small {
font-size: 1rem;
}
.medium {
font-size: 2rem;
}
</style>

View File

@ -0,0 +1,53 @@
<!--
// 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 love, { MeetingMinutes } from '@hcengineering/love'
import { WithLookup } from '@hcengineering/core'
import { ObjectPresenterType } from '@hcengineering/view'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
import { tooltip, Icon } from '@hcengineering/ui'
export let value: WithLookup<MeetingMinutes>
export let inline: boolean = false
export let disabled: boolean = false
export let accent: boolean = false
export let noUnderline: boolean = false
export let shouldShowAvatar = true
export let type: ObjectPresenterType = 'link'
</script>
{#if value}
{#if inline}
<ObjectMention object={value} {disabled} {accent} {noUnderline} />
{:else if type === 'link'}
<DocNavLink object={value} {disabled} {accent} {noUnderline}>
<div class="flex-presenter" use:tooltip={{ label: getEmbeddedLabel(value.title) }}>
{#if shouldShowAvatar}
<div class="icon">
<Icon icon={love.icon.Love} size={'small'} />
</div>
{/if}
<div class="label nowrap flex flex-gap-2" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
<span>{value.title}</span>
</div>
</div>
</DocNavLink>
{:else if type === 'text'}
<span class="overflow-label" use:tooltip={{ label: getEmbeddedLabel(value.title) }}>
{value.title}
</span>
{/if}
{/if}

View File

@ -0,0 +1,21 @@
<!--
// 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 { TableBrowser } from '@hcengineering/view-resources'
import love from '../plugin'
</script>
<TableBrowser _class={love.class.MeetingMinutes} query={{}} config={['', 'modifiedOn']} />

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { personByIdStore } from '@hcengineering/contact-resources'
import { personByIdStore, personIdByAccountId } from '@hcengineering/contact-resources'
import { Room as TypeRoom } from '@hcengineering/love'
import { getMetadata } from '@hcengineering/platform'
import { Label, Loading, resizeObserver, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
@ -30,6 +30,11 @@
TrackPublication
} from 'livekit-client'
import { onDestroy, onMount, tick } from 'svelte'
import presentation from '@hcengineering/presentation'
import aiBot from '@hcengineering/ai-bot'
import { Ref } from '@hcengineering/core'
import { Person, PersonAccount } from '@hcengineering/contact'
import love from '../plugin'
import { storePromise, currentRoom, infos, invites, myInfo, myRequests } from '../stores'
import {
@ -43,7 +48,6 @@
} from '../utils'
import ControlBar from './ControlBar.svelte'
import ParticipantView from './ParticipantView.svelte'
import presentation from '@hcengineering/presentation'
export let withVideo: boolean
export let room: TypeRoom
@ -61,6 +65,9 @@
let screen: HTMLVideoElement
let roomEl: HTMLDivElement
let aiPersonId: Ref<Person> | undefined = undefined
$: aiPersonId = $personIdByAccountId.get(aiBot.account.AIBot as Ref<PersonAccount>)
function handleTrackSubscribed (
track: RemoteTrack,
publication: RemoteTrackPublication,
@ -278,6 +285,10 @@
onDestroy(
infos.subscribe((data) => {
const aiParticipant = aiPersonId !== undefined ? participants.find(({ _id }) => _id === aiPersonId) : undefined
if (aiParticipant && !data.some((it) => it.room === room._id && it.person === aiParticipant._id)) {
participants = participants.filter(({ _id }) => _id !== aiPersonId)
}
for (const info of data) {
if (info.room !== room._id) continue
const current = participants.find((p) => p._id === info.person)

View File

@ -0,0 +1,27 @@
<!--
// 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 { Room } from '@hcengineering/love'
import { languagesDisplayData } from '../types'
export let room: Room
$: lang = room.language
</script>
<span title={languagesDisplayData[lang].label ?? languagesDisplayData.en.label}>
{languagesDisplayData[lang].emoji ?? languagesDisplayData.en.emoji}
</span>

View File

@ -0,0 +1,83 @@
<!--
// 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 { getClient } from '@hcengineering/presentation'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { DropdownIntlItem, DropdownLabelsIntl, DropdownLabelsPopupIntl, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Room, RoomLanguage } from '@hcengineering/love'
import { languagesDisplayData } from '../types'
import LanguageIcon from './LanguageIcon.svelte'
import { updateSessionLanguage } from '../utils'
export let room: Room
export let kind: 'dropdown' | 'icon' = 'dropdown'
const client = getClient()
let container: HTMLElement
let selectedItem: RoomLanguage = room.language
let items: DropdownIntlItem[] = []
$: items = Object.entries(languagesDisplayData).map(([lang, data]) => ({
id: lang,
label: getEmbeddedLabel(data.label),
icon: LanguageIcon,
iconProps: { lang }
}))
async function handleSelection (newLang?: RoomLanguage): Promise<void> {
if (newLang == null) return
await client.diffUpdate(room, { language: newLang })
await updateSessionLanguage(room)
}
function showLanguagesPopup (): void {
showPopup(DropdownLabelsPopupIntl, { items, selected: selectedItem }, container, async (result) => {
if (result != null && result !== '') {
selectedItem = result
await handleSelection(result)
}
})
}
</script>
{#if kind === 'dropdown'}
<DropdownLabelsIntl
{items}
kind="regular"
size="large"
icon={LanguageIcon}
iconProps={{ lang: selectedItem }}
bind:selected={selectedItem}
label={view.string.AddSavedView}
on:selected={(e) => handleSelection(e.detail)}
/>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="iconSelector" on:click={showLanguagesPopup} bind:this={container}>
<LanguageIcon lang={selectedItem} size="medium" />
</div>
{/if}
<style lang="scss">
.iconSelector {
display: flex;
cursor: pointer;
}
</style>

View File

@ -16,13 +16,14 @@
import { Person, type PersonAccount } from '@hcengineering/contact'
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { IdMap, getCurrentAccount } from '@hcengineering/core'
import { ParticipantInfo, Room, RoomAccess, RoomType } from '@hcengineering/love'
import { Icon, Label, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { isOffice, ParticipantInfo, Room, RoomAccess, RoomType } from '@hcengineering/love'
import { Icon, Label, eventToHTMLElement, showPopup, DropdownIntlItem } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import love from '../plugin'
import { invites, myInfo, myRequests } from '../stores'
import { getRoomLabel, tryConnect } from '../utils'
import PersonActionPopup from './PersonActionPopup.svelte'
import RoomLanguage from './RoomLanguage.svelte'
export let room: Room
export let info: ParticipantInfo[]
@ -65,7 +66,7 @@
if (room._id === $myInfo?.room || $myInfo === undefined) return
showPopup(PersonActionPopup, { room, person: person._id }, eventToHTMLElement(e))
} else {
tryConnect($personByIdStore, $myInfo, room, info, $myRequests, $invites, { x, y })
void tryConnect($personByIdStore, $myInfo, room, info, $myRequests, $invites, { x, y })
}
}
@ -165,6 +166,9 @@
<span class="overflow-label text-md flex-grow">
<Label label={getRoomLabel(room, $personByIdStore)} />
</span>
{#if !isOffice(room)}
<RoomLanguage {room} />
{/if}
{#if room.access === RoomAccess.DND || room.type === RoomType.Video}
<div class="flex-row-center flex-no-shrink h-full flex-gap-2">
{#if room.access === RoomAccess.DND}

View File

@ -0,0 +1,95 @@
<!--
// 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 { Modal, NavItem } from '@hcengineering/ui'
import presentation from '@hcengineering/presentation'
import { Room } from '@hcengineering/love'
import { createEventDispatcher } from 'svelte'
import { IntlString } from '@hcengineering/platform'
import RoomTranscriptionSettings from './RoomTranscriptionSettings.svelte'
import love from '../plugin'
export let room: Room
const dispatch = createEventDispatcher()
interface Group {
id: string
label: IntlString
}
const groups: Group[] = [{ id: 'transcription', label: love.string.Transcription }]
let selectedGroup = groups[0]
function selectGroup (group: Group): void {
selectedGroup = group
}
</script>
<Modal
label={love.string.Settings}
type="type-popup"
okLabel={presentation.string.Close}
okAction={() => {
dispatch('close')
}}
canSave={true}
showCancelButton={false}
on:close
>
<div class="content">
<div class="groups">
{#each groups as group}
<NavItem
label={group.label}
selected={group.id === selectedGroup?.id}
on:click={() => {
selectGroup(group)
}}
/>
{/each}
</div>
<div class="component">
{#if selectedGroup.id === 'transcription'}
<RoomTranscriptionSettings {room} />
{/if}
</div>
</div>
</Modal>
<style lang="scss">
.content {
display: flex;
gap: 1rem;
height: 20rem;
}
.groups {
padding-right: 0.5rem;
width: 12rem;
display: flex;
flex-direction: column;
border-right: 1px solid var(--global-ui-BorderColor);
}
.component {
display: flex;
flex-direction: column;
flex: 1;
padding: 0.5rem;
}
</style>

View File

@ -0,0 +1,44 @@
<!--
// 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 ui, { Label, ModernToggle } from '@hcengineering/ui'
import love, { Room } from '@hcengineering/love'
import { getClient } from '@hcengineering/presentation'
import RoomLanguageSelector from './RoomLanguageSelector.svelte'
export let room: Room
const client = getClient()
async function toggleTranscribing (): Promise<void> {
await client.diffUpdate(room, { startWithTranscription: !room.startWithTranscription })
}
</script>
<div class="antiGrid">
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={ui.string.Language} />
</div>
<RoomLanguageSelector {room} />
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={love.string.StartWithTranscription} />
</div>
<ModernToggle size="small" checked={room.startWithTranscription} on:change={toggleTranscribing} />
</div>
</div>

View File

@ -0,0 +1,45 @@
<!--
// 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 love, { MeetingMinutes } from '@hcengineering/love'
import { ChannelEmbeddedContent } from '@hcengineering/chunter-resources'
import { ActivityMessage } from '@hcengineering/activity'
import { updateTabData, WidgetState } from '@hcengineering/workbench-resources'
export let widgetState: WidgetState
export let meetingMinutes: MeetingMinutes
export let height: string
export let width: string
function replyToThread (message: ActivityMessage): void {
updateTabData(love.ids.MeetingWidget, 'chat', { thread: message._id })
}
function closeThread (): void {
console.log('closeThread')
updateTabData(love.ids.MeetingWidget, 'chat', { thread: undefined })
}
</script>
<ChannelEmbeddedContent
{width}
{height}
object={meetingMinutes}
threadId={widgetState.tabs.find((tab) => tab.id === 'chat')?.data?.thread}
collection="messages"
on:channel={closeThread}
onReply={replyToThread}
on:close
/>

View File

@ -0,0 +1,105 @@
<!--
// 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 { closeWidget, minimizeSidebar, WidgetState } from '@hcengineering/workbench-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import core, { Ref } from '@hcengineering/core'
import { MeetingMinutes, Room } from '@hcengineering/love'
import { Loading } from '@hcengineering/ui'
import love from '../../plugin'
import VideoTab from './VideoTab.svelte'
import { isCurrentInstanceConnected, lk } from '../../utils'
import { rooms } from '../../stores'
import ChatTab from './ChatTab.svelte'
import TranscriptionTab from './TranscriptionTab.svelte'
export let widgetState: WidgetState | undefined
export let height: string
export let width: string
const meetingQuery = createQuery()
const client = getClient()
let meetingMinutes: MeetingMinutes | undefined = undefined
let isMeetingMinutesLoaded = false
let roomId: Ref<Room> | undefined = undefined
let room: Room | undefined = undefined
let sid: string | undefined = undefined
$: roomId = widgetState?.data?.room
$: room = roomId !== undefined ? $rooms.find((r) => r._id === roomId) : undefined
void lk.getSid().then((res) => {
sid = res
})
$: if (!$isCurrentInstanceConnected || widgetState?.data?.room === undefined) {
closeWidget(love.ids.MeetingWidget)
}
$: if (roomId !== meetingMinutes?.room) {
meetingMinutes = undefined
isMeetingMinutesLoaded = false
}
$: if ($isCurrentInstanceConnected && room && sid) {
meetingQuery.query(love.class.MeetingMinutes, { room: room._id, sid }, async (res) => {
meetingMinutes = res[0]
if (meetingMinutes !== undefined) {
isMeetingMinutesLoaded = true
} else {
void createMeetingMinutes()
}
})
} else {
meetingQuery.unsubscribe()
meetingMinutes = undefined
isMeetingMinutesLoaded = sid !== undefined
}
async function createMeetingMinutes (): Promise<void> {
if (sid === undefined || room === undefined) return
const dateStr = new Date().toISOString().replace('T', '_').slice(0, 19)
await client.createDoc(love.class.MeetingMinutes, core.space.Workspace, {
title: room.name + '_' + dateStr,
room: room._id,
sid
})
}
function handleClose (): void {
minimizeSidebar()
}
</script>
{#if widgetState && room}
{#if widgetState.tab === 'video'}
<VideoTab {room} />
{:else if widgetState.tab === 'chat'}
{#if !isMeetingMinutesLoaded}
<Loading />
{:else if meetingMinutes}
<ChatTab {meetingMinutes} {widgetState} {height} {width} on:close={handleClose} />
{/if}
{:else if widgetState.tab === 'transcription'}
{#if !isMeetingMinutesLoaded}
<Loading />
{:else if meetingMinutes}
<TranscriptionTab {meetingMinutes} {widgetState} {height} {width} on:close={handleClose} />
{/if}
{/if}
{/if}

View File

@ -0,0 +1,43 @@
<!--
// 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 love, { MeetingMinutes } from '@hcengineering/love'
import { ChannelEmbeddedContent } from '@hcengineering/chunter-resources'
import { ActivityMessage } from '@hcengineering/activity'
import { updateTabData, WidgetState } from '@hcengineering/workbench-resources'
export let widgetState: WidgetState
export let meetingMinutes: MeetingMinutes
export let height: string
export let width: string
function replyToThread (message: ActivityMessage): void {
updateTabData(love.ids.MeetingWidget, 'transcription', { thread: message._id })
}
function closeThread (): void {
updateTabData(love.ids.MeetingWidget, 'transcription', { thread: undefined })
}
</script>
<ChannelEmbeddedContent
{width}
{height}
object={meetingMinutes}
threadId={widgetState.tabs.find((tab) => tab.id === 'transcription')?.data?.thread}
collection="transcription"
on:channel={closeThread}
onReply={replyToThread}
on:close
/>

View File

@ -13,28 +13,16 @@
// limitations under the License.
-->
<script lang="ts">
import { Room as TypeRoom } from '@hcengineering/love'
import { Ref } from '@hcengineering/core'
import { closeWidget, WidgetState } from '@hcengineering/workbench-resources'
import { Room } from '@hcengineering/love'
import love from '../plugin'
import VideoPopup from './VideoPopup.svelte'
import VideoPopup from '../VideoPopup.svelte'
export let widgetState: WidgetState | undefined
let room: Ref<TypeRoom> | undefined = undefined
$: room = widgetState?.data?.room
$: if (widgetState?.data?.room === undefined) {
closeWidget(love.ids.VideoWidget)
}
export let room: Room
</script>
{#if room}
<div class="root">
<VideoPopup {room} isDock canUnpin={false} />
</div>
{/if}
<div class="root">
<VideoPopup room={room._id} isDock canUnpin={false} />
</div>
<style lang="scss">
.root {

View File

@ -1,4 +1,7 @@
import { type Resources } from '@hcengineering/platform'
import { getMetadata, type Resources } from '@hcengineering/platform'
import aiBot from '@hcengineering/ai-bot'
import { AccountRole, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import ControlExt from './components/ControlExt.svelte'
import EditMeetingData from './components/EditMeetingData.svelte'
import Main from './components/Main.svelte'
@ -7,8 +10,18 @@ import SelectScreenSourcePopup from './components/SelectScreenSourcePopup.svelte
import Settings from './components/Settings.svelte'
import WorkbenchExtension from './components/WorkbenchExtension.svelte'
import LoveWidget from './components/LoveWidget.svelte'
import VideoWidget from './components/VideoWidget.svelte'
import { createMeeting, toggleMic, toggleVideo } from './utils'
import MeetingWidget from './components/widget/MeetingWidget.svelte'
import MeetingMinutesPresenter from './components/MeetingMinutesPresenter.svelte'
import {
copyGuestLink,
createMeeting,
showRoomSettings,
startTranscription,
stopTranscription,
toggleMic,
toggleVideo
} from './utils'
export { setCustomCreateScreenTracks } from './utils'
@ -22,13 +35,29 @@ export default async (): Promise<Resources> => ({
MeetingData,
EditMeetingData,
LoveWidget,
VideoWidget
MeetingWidget,
MeetingMinutesPresenter
},
function: {
CreateMeeting: createMeeting
CreateMeeting: createMeeting,
CanShowRoomSettings: () => {
if (!hasAccountRole(getCurrentAccount(), AccountRole.User)) {
return
}
// For now settings is available only when AI bot is enabled
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
return url !== ''
},
CanCopyGuestLink: () => {
return hasAccountRole(getCurrentAccount(), AccountRole.User)
}
},
actionImpl: {
ToggleMic: toggleMic,
ToggleVideo: toggleVideo
ToggleVideo: toggleVideo,
StartTranscribing: startTranscription,
StopTranscribing: stopTranscription,
ShowRoomSettings: showRoomSettings,
CopyGuestLink: copyGuestLink
}
})

View File

@ -17,15 +17,19 @@ 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'
import { type ViewActionAvailabilityFunction } from '@hcengineering/view'
export default mergeIds(loveId, love, {
component: {
ControlExt: '' as AnyComponent,
MeetingData: '' as AnyComponent,
EditMeetingData: '' as AnyComponent
EditMeetingData: '' as AnyComponent,
MeetingMinutesPresenter: '' as AnyComponent
},
function: {
CreateMeeting: '' as Resource<DocCreateFunction>
CreateMeeting: '' as Resource<DocCreateFunction>,
CanShowRoomSettings: '' as Resource<ViewActionAvailabilityFunction>,
CanCopyGuestLink: '' as Resource<ViewActionAvailabilityFunction>
},
string: {
CreateMeeting: '' as IntlString,
@ -79,6 +83,7 @@ export default mergeIds(loveId, love, {
Invite: '' as IntlString,
KnockAction: '' as IntlString,
Select: '' as IntlString,
ChooseShare: '' as IntlString
ChooseShare: '' as IntlString,
MoreOptions: '' as IntlString
}
})

View File

@ -1,4 +1,4 @@
import { type Person, type PersonAccount } from '@hcengineering/contact'
import { type PersonAccount } from '@hcengineering/contact'
import { getCurrentAccount, type Ref } from '@hcengineering/core'
import {
RequestStatus,
@ -11,7 +11,10 @@ import {
type Room
} from '@hcengineering/love'
import { createQuery, getClient } from '@hcengineering/presentation'
import { derived, writable } from 'svelte/store'
import { derived, get, writable } from 'svelte/store'
import { personIdByAccountId } from '@hcengineering/contact-resources'
import aiBot from '@hcengineering/ai-bot'
import love from './plugin'
export const rooms = writable<Room[]>([])
@ -57,10 +60,15 @@ export const myPreferences = writable<DevicesPreference | undefined>()
export let $myPreferences: DevicesPreference | undefined
function filterParticipantInfo (value: ParticipantInfo[]): ParticipantInfo[] {
const map = new Map<Ref<Person>, ParticipantInfo>()
const map = new Map<string, ParticipantInfo>()
const aiPersonId = get(personIdByAccountId).get(aiBot.account.AIBot as Ref<PersonAccount>)
for (const val of value) {
if (aiPersonId !== undefined && val.person === aiPersonId) {
map.set(val._id, val)
} else {
map.set(val.person, val)
}
}
return Array.from(map.values())
}

View File

@ -1,4 +1,5 @@
import { type DefSeparators } from '@hcengineering/ui'
import { type RoomLanguage } from '@hcengineering/love'
export interface ResizeInitParams {
x: number
@ -41,3 +42,54 @@ export const shadowNormal: RGBAColor = { r: 81, g: 144, b: 236, a: 1 }
export const shadowError: RGBAColor = { r: 249, g: 110, b: 80, a: 1 }
export const loveSeparators: DefSeparators = [{ minSize: 17.5, size: 25, maxSize: 30, float: 'navigator' }, null]
export const languagesDisplayData: {
[key in RoomLanguage]: { emoji: string, label: string }
} = {
bg: { emoji: '🇧🇬', label: 'Български' },
ca: { emoji: '🇨🇦', label: 'Català' },
zh: { emoji: '🇨🇳', label: '简体中文' },
'zh-TW': { emoji: '🇹🇼', label: '繁體中文' },
'zh-HK': { emoji: '🇭🇰', label: '繁體中文 (香港)' },
cs: { emoji: '🇨🇿', label: 'Čeština' },
da: { emoji: '🇩🇰', label: 'Dansk' },
nl: { emoji: '🇳🇱', label: 'Nederlands' },
en: { emoji: '🇺🇸', label: 'English' },
'en-US': { emoji: '🇺🇸', label: 'English (US)' },
'en-AU': { emoji: '🇦🇺', label: 'English (Australia)' },
'en-GB': { emoji: '🇬🇧', label: 'English (UK)' },
'en-NZ': { emoji: '🇳🇿', label: 'English (New Zealand)' },
'en-IN': { emoji: '🇮🇳', label: 'English (India)' },
et: { emoji: '🇪🇪', label: 'Eesti' },
fi: { emoji: '🇫🇮', label: 'Suomi' },
'nl-BE': { emoji: '🇧🇪', label: 'Vlaams' },
fr: { emoji: '🇫🇷', label: 'Français' },
'fr-CA': { emoji: '🇨🇦', label: 'Français (Canada)' },
de: { emoji: '🇩🇪', label: 'Deutsch' },
'de-CH': { emoji: '🇨🇭', label: 'Schweizerdeutsch' },
el: { emoji: '🇬🇷', label: 'Ελληνικά' },
hi: { emoji: '🇮🇳', label: 'हिन्दी' },
hu: { emoji: '🇭🇺', label: 'Magyar' },
id: { emoji: '🇮🇩', label: 'Bahasa Indonesia' },
it: { emoji: '🇮🇹', label: 'Italiano' },
ja: { emoji: '🇯🇵', label: '日本語' },
ko: { emoji: '🇰🇷', label: '한국어' },
lv: { emoji: '🇱🇻', label: 'Latviešu' },
lt: { emoji: '🇱🇹', label: 'Lietuvių' },
ms: { emoji: '🇲🇾', label: 'Bahasa Melayu' },
no: { emoji: '🇳🇴', label: 'Norsk' },
pl: { emoji: '🇵🇱', label: 'Polski' },
pt: { emoji: '🇵🇹', label: 'Português' },
'pt-BR': { emoji: '🇧🇷', label: 'Português (Brasil)' },
'pt-PT': { emoji: '🇵🇹', label: 'Português (Portugal)' },
ro: { emoji: '🇷🇴', label: 'Română' },
ru: { emoji: '🇷🇺', label: 'Русский' },
sk: { emoji: '🇸🇰', label: 'Slovenčina' },
es: { emoji: '🇪🇸', label: 'Español' },
'es-419': { emoji: '🇲🇽', label: 'Español (Latinoamérica)' },
sv: { emoji: '🇸🇪', label: 'Svenska' },
th: { emoji: '🇹🇭', label: 'ไทย' },
tr: { emoji: '🇹🇷', label: 'Türkçe' },
uk: { emoji: '🇺🇦', label: 'Українська' },
vi: { emoji: '🇻🇳', label: 'Tiếng Việt' }
}

View File

@ -1,11 +1,11 @@
import { Analytics } from '@hcengineering/analytics'
import calendar, { getAllEvents, type Event } from '@hcengineering/calendar'
import calendar, { type Event, getAllEvents } from '@hcengineering/calendar'
import contact, { getName, type Person, type PersonAccount } from '@hcengineering/contact'
import core, {
AccountRole,
concatLink,
getCurrentAccount,
type Data,
getCurrentAccount,
type IdMap,
type Ref,
type Space,
@ -13,43 +13,57 @@ import core, {
} from '@hcengineering/core'
import login from '@hcengineering/login'
import {
RequestStatus,
RoomAccess,
RoomType,
isOffice,
loveId,
getFreeRoomPlace,
type Invite,
isOffice,
type JoinRequest,
LoveEvents,
loveId,
type Meeting,
type Office,
type ParticipantInfo,
RequestStatus,
type Room,
LoveEvents
RoomAccess,
RoomType,
TranscriptionStatus
} from '@hcengineering/love'
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 presentation, {
copyTextToClipboard,
createQuery,
type DocCreatePhase,
getClient
} from '@hcengineering/presentation'
import { type DropdownTextItem, getCurrentLocation, type Location, navigate, showPopup } from '@hcengineering/ui'
import { isKrispNoiseFilterSupported, KrispNoiseFilter } from '@livekit/krisp-noise-filter'
import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors'
import {
ConnectionState,
Room as LKRoom,
LocalAudioTrack,
LocalVideoTrack,
RoomEvent,
Track,
type AudioCaptureOptions,
ConnectionState,
LocalAudioTrack,
type LocalTrack,
type LocalTrackPublication,
LocalVideoTrack,
type RemoteParticipant,
type RemoteTrack,
type RemoteTrackPublication,
Room as LKRoom,
RoomEvent,
Track,
type VideoCaptureOptions
} from 'livekit-client'
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 { type Widget, type WidgetTab } from '@hcengineering/workbench'
import view from '@hcengineering/view'
import { sendMessage } from './broadcast'
import love from './plugin'
import { $myPreferences, currentRoom } from './stores'
import RoomSettingsPopup from './components/RoomSettingsPopup.svelte'
export const selectedCamId = 'selectedDevice_cam'
export const selectedMicId = 'selectedDevice_mic'
@ -130,6 +144,7 @@ isCurrentInstanceConnected.subscribe((value) => {
})
export const screenSharing = writable<boolean>(false)
export const isRecording = writable<boolean>(false)
export const isTranscription = writable<boolean>(false)
export const isRecordingAvailable = writable<boolean>(false)
export const isMicEnabled = writable<boolean>(false)
export const isCameraEnabled = writable<boolean>(false)
@ -340,15 +355,20 @@ lk.on(RoomEvent.RoomMetadataChanged, (metadata) => {
if (data.recording !== undefined) {
isRecording.set(data.recording)
}
if (data.transcription !== undefined) {
isTranscription.set(data.transcription === TranscriptionStatus.InProgress)
}
} catch (err: any) {
Analytics.handleError(err)
}
})
lk.on(RoomEvent.Connected, () => {
isConnected.set(true)
sendMessage({ type: 'connect', value: true })
isCurrentInstanceConnected.set(true)
isRecording.set(lk.isRecording)
initRoomMetadata(lk.metadata)
Analytics.handleEvent(LoveEvents.ConnectedToRoom)
})
lk.on(RoomEvent.Disconnected, () => {
@ -358,6 +378,27 @@ lk.on(RoomEvent.Disconnected, () => {
Analytics.handleEvent(LoveEvents.DisconnectedFromRoom)
})
function initRoomMetadata (metadata: string | undefined): void {
if (metadata === undefined) return
let data: { transcription?: TranscriptionStatus } = {}
try {
data = metadata === '' ? {} : JSON.parse(metadata)
} catch (err: any) {
data = {}
Analytics.handleError(err)
}
isTranscription.set(data.transcription === TranscriptionStatus.InProgress)
const room = get(currentRoom)
if (
(data.transcription == null || data.transcription === TranscriptionStatus.Idle) &&
room?.startWithTranscription === true
) {
void startTranscription(room)
}
}
export async function connect (name: string, room: Room, _id: string): Promise<void> {
const wsURL = getMetadata(love.metadata.WebSocketURL)
if (wsURL === undefined) {
@ -554,25 +595,6 @@ export async function connectRoom (
export const joinRequest: Ref<JoinRequest> | undefined = undefined
const requestsQuery = createQuery(true)
export function getFreePlace (room: Room, info: ParticipantInfo[]): { x: number, y: number } {
const me = getCurrentAccount()
let y = 0
while (true) {
for (let x = 0; x < room.width; x++) {
if (info.find((p) => p.x === x && p.y === y) === undefined) {
if (x === 0 && y === 0 && isOffice(room)) {
if (room.person === (me as PersonAccount).person) {
return { x: 0, y: 0 }
}
} else {
return { x, y }
}
}
}
y++
}
}
export function calculateFloorSize (_rooms: Room[], preview?: boolean): number {
let fH: number = 5
_rooms.forEach((room) => {
@ -626,8 +648,8 @@ export async function tryConnect (
currentInvites: Invite[],
place?: { x: number, y: number }
): Promise<void> {
const me = getCurrentAccount()
const currentPerson = personByIdStore.get((me as PersonAccount).person)
const me = getCurrentAccount() as PersonAccount
const currentPerson = personByIdStore.get(me.person)
if (currentPerson === undefined) return
const client = getClient()
@ -643,7 +665,7 @@ export async function tryConnect (
place = undefined
}
if (place === undefined) {
place = getFreePlace(room, info)
place = getFreeRoomPlace(room, info, me.person)
}
const x: number = place.x
const y: number = place.y
@ -674,7 +696,7 @@ export async function tryConnect (
room: room._id,
status: RequestStatus.Pending
})
requestsQuery.query(love.class.JoinRequest, { person: (me as PersonAccount).person, _id }, (res) => {
requestsQuery.query(love.class.JoinRequest, { person: me.person, _id }, (res) => {
const req = res[0]
if (req === undefined) return
if (req.status === RequestStatus.Pending) return
@ -727,14 +749,8 @@ export async function toggleVideo (): Promise<void> {
export async function record (room: Room): Promise<void> {
try {
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
if (endpoint === undefined) {
throw new Error('Love service endpoint not found')
}
const token = getMetadata(presentation.metadata.Token)
if (token === undefined) {
throw new Error('Token not found')
}
const endpoint = getLoveEndpoint()
const token = getPlatformToken()
const roomName = getTokenRoomName(room.name, room._id)
if (lk.isRecording) {
await fetch(concatLink(endpoint, '/stopRecord'), {
@ -805,3 +821,145 @@ export async function createMeeting (
await client.update(event, { location: link })
}
}
export function getLoveEndpoint (): string {
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
if (endpoint === undefined) {
throw new Error('Love service endpoint not found')
}
return endpoint
}
export function getPlatformToken (): string {
const token = getMetadata(presentation.metadata.Token)
if (token === undefined) {
throw new Error('Token not found')
}
return token
}
export async function startTranscription (room: Room): Promise<void> {
const current = get(currentRoom)
if (current === undefined || room._id !== current._id) return
const sid = await lk.getSid()
await connectMeeting(room._id, sid, room.language, { transcription: true })
}
export async function stopTranscription (room: Room): Promise<void> {
const current = get(currentRoom)
if (current === undefined || room._id !== current._id) return
await disconnectMeeting(room._id)
}
export async function updateSessionLanguage (room: Room): Promise<void> {
const current = get(currentRoom)
if (current === undefined || room._id !== current._id || !get(isTranscription)) return
try {
const endpoint = getLoveEndpoint()
const token = getPlatformToken()
const roomName = getTokenRoomName(room.name, room._id)
await fetch(concatLink(endpoint, '/language'), {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ roomName, room: room.name, language: room.language })
})
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
}
}
export async function showRoomSettings (room?: Room): Promise<void> {
if (room === undefined) return
showPopup(RoomSettingsPopup, { room }, 'top')
}
export async function copyGuestLink (room?: Room): Promise<void> {
if (room === undefined) return
await copyTextToClipboard(getRoomGuestLink(room))
}
async function getRoomGuestLink (room: Room): Promise<string> {
const client = getClient()
const roomInfo = await client.findOne(love.class.RoomInfo, { room: room._id })
if (roomInfo !== undefined) {
const navigateUrl = getCurrentLocation()
navigateUrl.query = {
sessionId: roomInfo._id
}
const func = await getResource(login.function.GetInviteLink)
return await func(24, '', -1, AccountRole.Guest, encodeURIComponent(JSON.stringify(navigateUrl)))
}
return ''
}
export function isTranscriptionAllowed (): boolean {
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
return url !== ''
}
export function createMeetingWidget (widget: Widget, room: Ref<Room>, loc: Location, video: boolean): void {
const tabs: WidgetTab[] = [
...(video
? [
{
id: 'video',
name: 'Video',
icon: love.icon.Cam,
readonly: true
}
]
: []),
{
id: 'chat',
name: 'Chat',
icon: view.icon.Bubble,
readonly: true
},
{
id: 'transcription',
name: 'Transcription',
icon: view.icon.Feather,
readonly: true
}
]
openWidget(
widget,
{
room
},
{ active: loc.path[2] !== loveId, openedByUser: false },
tabs
)
}
export function createMeetingVideoWidgetTab (widget: Widget, loc: Location): void {
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget._id)
if (widgetState === undefined) return
const tab: WidgetTab = {
id: 'video',
name: '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'
})
}

View File

@ -31,6 +31,60 @@ export interface Floor extends Doc {
name: string
}
export enum TranscriptionStatus {
Idle = 'idle',
InProgress = 'inProgress',
Completed = 'completed'
}
export type RoomLanguage =
| 'bg'
| 'ca'
| 'zh'
| 'zh-TW'
| 'zh-HK'
| 'cs'
| 'da'
| 'nl'
| 'en'
| 'en-US'
| 'en-AU'
| 'en-GB'
| 'en-NZ'
| 'en-IN'
| 'et'
| 'fi'
| 'nl-BE'
| 'fr'
| 'fr-CA'
| 'de'
| 'de-CH'
| 'el'
| 'hi'
| 'hu'
| 'id'
| 'it'
| 'ja'
| 'ko'
| 'lv'
| 'lt'
| 'ms'
| 'no'
| 'pl'
| 'pt'
| 'pt-BR'
| 'pt-PT'
| 'ro'
| 'ru'
| 'sk'
| 'es'
| 'es-419'
| 'sv'
| 'th'
| 'tr'
| 'uk'
| 'vi'
export interface Room extends Doc {
name: string
type: RoomType
@ -40,6 +94,8 @@ export interface Room extends Doc {
height: number
x: number
y: number
language: RoomLanguage
startWithTranscription: boolean
}
export interface Office extends Room {
@ -93,6 +149,14 @@ export interface DevicesPreference extends Preference {
camEnabled: boolean
}
export interface MeetingMinutes extends Doc {
sid: string
title: string
room: Ref<Room>
transcription?: number
messages?: number
}
export * from './utils'
const love = plugin(loveId, {
@ -104,7 +168,8 @@ const love = plugin(loveId, {
JoinRequest: '' as Ref<Class<JoinRequest>>,
DevicesPreference: '' as Ref<Class<DevicesPreference>>,
RoomInfo: '' as Ref<Class<RoomInfo>>,
Invite: '' as Ref<Class<Invite>>
Invite: '' as Ref<Class<Invite>>,
MeetingMinutes: '' as Ref<Class<MeetingMinutes>>
},
mixin: {
Meeting: '' as Ref<Mixin<Meeting>>
@ -123,7 +188,13 @@ const love = plugin(loveId, {
RoomType: '' as IntlString,
Knock: '' as IntlString,
Open: '' as IntlString,
DND: '' as IntlString
DND: '' as IntlString,
StartTranscription: '' as IntlString,
StopTranscription: '' as IntlString,
Meeting: '' as IntlString,
Transcription: '' as IntlString,
StartWithTranscription: '' as IntlString,
MeetingMinutes: '' as IntlString
},
ids: {
MainFloor: '' as Ref<Floor>,
@ -131,7 +202,7 @@ const love = plugin(loveId, {
InviteNotification: '' as Ref<NotificationType>,
KnockNotification: '' as Ref<NotificationType>,
LoveWidget: '' as Ref<Widget>,
VideoWidget: '' as Ref<Widget>
MeetingWidget: '' as Ref<Widget>
},
icon: {
Love: '' as Asset,

View File

@ -1,6 +1,7 @@
import { Employee } from '@hcengineering/contact'
import { Employee, Person } from '@hcengineering/contact'
import { Data, Ref } from '@hcengineering/core'
import love, { Office, Room, RoomAccess, RoomType, GRID_WIDTH } from '.'
import love, { Office, Room, ParticipantInfo, RoomAccess, RoomType, GRID_WIDTH } from '.'
interface Slot {
_id?: Ref<Room>
@ -27,7 +28,9 @@ export function createDefaultRooms (employees: Ref<Employee>[]): Data<Room | Off
height: 1,
x: (index % 2) * 3,
y: index - (index % 2),
person: employees[index] ?? null
person: employees[index] ?? null,
language: 'en',
startWithTranscription: false
}
res.push(office)
}
@ -39,7 +42,9 @@ export function createDefaultRooms (employees: Ref<Employee>[]): Data<Room | Off
width: 9,
height: 3,
x: 6,
y: 0
y: 0,
language: 'en',
startWithTranscription: true
})
res.push({
name: 'Meeting Room 1',
@ -49,7 +54,9 @@ export function createDefaultRooms (employees: Ref<Employee>[]): Data<Room | Off
width: 4,
height: 3,
x: 6,
y: 4
y: 4,
language: 'en',
startWithTranscription: true
})
res.push({
name: 'Meeting Room 2',
@ -59,7 +66,9 @@ export function createDefaultRooms (employees: Ref<Employee>[]): Data<Room | Off
width: 4,
height: 3,
x: 11,
y: 4
y: 4,
language: 'en',
startWithTranscription: true
})
res.push({
name: 'Voice Room 1',
@ -69,7 +78,9 @@ export function createDefaultRooms (employees: Ref<Employee>[]): Data<Room | Off
width: 4,
height: 3,
x: 6,
y: 8
y: 8,
language: 'en',
startWithTranscription: true
})
res.push({
name: 'Voice Room 2',
@ -79,7 +90,9 @@ export function createDefaultRooms (employees: Ref<Employee>[]): Data<Room | Off
width: 4,
height: 3,
x: 11,
y: 8
y: 8,
language: 'en',
startWithTranscription: true
})
return res
}
@ -180,3 +193,21 @@ export interface ScreenSource {
thumbnailURL: string
appIconURL: string
}
export function getFreeRoomPlace (room: Room, info: ParticipantInfo[], person: Ref<Person>): { x: number, y: number } {
let y = 0
while (true) {
for (let x = 0; x < room.width; x++) {
if (info.find((p) => p.x === x && p.y === y) === undefined) {
if (x === 0 && y === 0 && isOffice(room)) {
if (room.person === person) {
return { x: 0, y: 0 }
}
} else {
return { x, y }
}
}
}
y++
}
}

View File

@ -218,4 +218,7 @@
<symbol id="file" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C7.89543 4 7 4.89543 7 6V26C7 27.1046 7.89543 28 9 28H23C24.1046 28 25 27.1046 25 26V12H21C18.7909 12 17 10.2091 17 8V4H9ZM19 4.41421V8C19 9.10457 19.8954 10 21 10H24.5858L19 4.41421ZM5 6C5 3.79086 6.79086 2 9 2H18.5858C19.1162 2 19.6249 2.21071 20 2.58579L26.4142 9C26.7893 9.37507 27 9.88378 27 10.4142V26C27 28.2091 25.2091 30 23 30H9C6.79086 30 5 28.2091 5 26V6ZM10 17C10 16.4477 10.4477 16 11 16H21C21.5523 16 22 16.4477 22 17C22 17.5523 21.5523 18 21 18H11C10.4477 18 10 17.5523 10 17ZM10 23C10 22.4477 10.4477 22 11 22H21C21.5523 22 22 22.4477 22 23C22 23.5523 21.5523 24 21 24H11C10.4477 24 10 23.5523 10 23Z" />
</symbol>
<symbol id="feather" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9678 4.56621C16.6192 2.91483 18.8589 1.98709 21.1943 1.98709C23.5297 1.98709 25.7695 2.91483 27.4209 4.56621C29.0722 6.21758 30 8.45733 30 10.7927C30 13.1281 29.0722 15.3679 27.4209 17.0193L23.9558 20.4946C23.897 20.5741 23.8267 20.6446 23.7474 20.7036L18.6585 25.8077C18.4708 25.9959 18.216 26.1016 17.9503 26.1016H7.31267L3.70491 29.7094C3.31439 30.0999 2.68122 30.0999 2.2907 29.7094C1.90017 29.3189 1.90017 28.6857 2.2907 28.2952L5.89845 24.6874V14.0498C5.89845 13.7846 6.00381 13.5302 6.19135 13.3427L14.9678 4.56621ZM9.31267 24.1016H17.5352L20.7266 20.9008H12.5135L9.31267 24.1016ZM11.3889 19.197L7.89845 22.6874V14.464L16.382 5.98042C17.6583 4.70412 19.3894 3.98709 21.1943 3.98709C22.9993 3.98709 24.7303 4.70412 26.0067 5.98042C27.283 7.25673 28 8.98777 28 10.7927C28 12.5977 27.283 14.3287 26.0067 15.605L22.7207 18.9008H14.5135L21.9079 11.5063C22.2985 11.1158 22.2985 10.4827 21.9079 10.0921C21.5174 9.70161 20.8843 9.70161 20.4937 10.0921L11.3955 19.1903C11.3933 19.1925 11.3911 19.1948 11.3889 19.197Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -126,6 +126,7 @@
"Leave": "Leave",
"Join": "Join",
"Copied": "Copied",
"Title": "Title",
"HideArchived": "Hide archived"
}
}

View File

@ -121,6 +121,7 @@
"Leave": "Salir",
"Join": "Unirse",
"Copied": "Copiado",
"Title": "Título",
"HideArchived": "Ocultar archivadas"
}
}

View File

@ -121,6 +121,7 @@
"Leave": "Quitter",
"Join": "Rejoindre",
"Copied": "Copié",
"Title": "Titre",
"HideArchived": "Masquer les archives"
}
}

View File

@ -121,6 +121,7 @@
"Leave": "Esci",
"Join": "Unisciti",
"Copied": "Copiato",
"Title": "Titolo",
"HideArchived": "Nascondi archiviato"
}
}

View File

@ -121,6 +121,7 @@
"Leave": "Sair",
"Join": "Ingressar",
"Copied": "Copiado",
"Title": "Título",
"HideArchived": "Ocultar arquivado"
}
}

View File

@ -123,6 +123,7 @@
"Leave": "Покинуть",
"Join": "Присоединиться",
"Copied": "Скопировано",
"Title": "Заголовок",
"HideArchived": "Скрыть архивные"
}
}

View File

@ -126,6 +126,7 @@
"Leave": "离开",
"Join": "加入",
"Copied": "已复制",
"Title": "标题",
"HideArchived": "隱藏已存檔"
}
}

View File

@ -62,5 +62,6 @@ loadMetadata(view.icon, {
Video: `${icons}#video`,
Audio: `${icons}#audio`,
File: `${icons}#file`,
PinTack: `${icons}#pin-tack`
PinTack: `${icons}#pin-tack`,
Feather: `${icons}#feather`
})

View File

@ -17,7 +17,7 @@
import { Asset } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Action, Menu } from '@hcengineering/ui'
import { ActionGroup, ViewContextType } from '@hcengineering/view'
import { ActionGroup, Action as ViewAction, ViewContextType } from '@hcengineering/view'
import { getActions, invokeAction } from '../actions'
export let object: Doc | Doc[]
@ -26,6 +26,8 @@
export let excludedActions: string[] = []
export let includedActions: string[] = []
export let mode: ViewContextType | undefined = undefined
export let overrides = new Map<Ref<ViewAction>, (object: Doc | Doc[], ev?: Event) => void>()
let resActions = actions
const client = getClient()
@ -61,11 +63,16 @@
inline: a.inline,
group: a.context.group ?? 'other',
action: async (_: any, evt: Event) => {
if (overrides.has(a._id)) {
overrides.get(a._id)?.(object, evt)
return
}
invokeAction(object, evt, a)
},
component: a.actionPopup,
props: { ...a.actionProps, value: object }
}))
resActions = [...newActions, ...actions].sort(
(a, b) => (order as any)[a.group ?? 'other'] - (order as any)[b.group ?? 'other']
)

View File

@ -211,7 +211,8 @@ const view = plugin(viewId, {
Join: '' as IntlString,
Leave: '' as IntlString,
Copied: '' as IntlString,
And: '' as IntlString
And: '' as IntlString,
Title: '' as IntlString
},
icon: {
Table: '' as Asset,
@ -258,7 +259,8 @@ const view = plugin(viewId, {
Video: '' as Asset,
Audio: '' as Asset,
File: '' as Asset,
PinTack: '' as Asset
PinTack: '' as Asset,
Feather: '' as Asset
},
category: {
General: '' as Ref<ActionCategory>,

View File

@ -91,11 +91,11 @@
void closeTab(tab)
}
function handleMenu (event: MouseEvent): void {
function handleMenu (event: CustomEvent<MouseEvent>): void {
event.preventDefault()
event.stopPropagation()
showMenu(event, { object: tab, baseMenuClass: workbench.class.WorkbenchTab })
showMenu(event.detail, { object: tab, baseMenuClass: workbench.class.WorkbenchTab })
}
</script>

View File

@ -30,14 +30,14 @@
})
}
function handleMenu (event: MouseEvent): void {
function handleMenu (event: CustomEvent<MouseEvent>): void {
if (actions.length === 0) {
return
}
event.preventDefault()
event.stopPropagation()
showPopup(Menu, { actions }, event.target as HTMLElement)
showPopup(Menu, { actions }, event.detail.target as HTMLElement)
}
</script>
@ -47,6 +47,7 @@
highlighted={selected}
orientation="vertical"
kind={tab.isPinned ? 'secondary' : 'primary'}
readonly={tab.readonly}
{icon}
iconProps={tab.iconProps}
canClose={!tab.isPinned}

View File

@ -103,7 +103,8 @@ function setSidebarStateToLocalStorage (state: SidebarState): void {
export function openWidget (
widget: Widget,
data?: Record<string, any>,
params?: { active: boolean, openedByUser: boolean }
params?: { active: boolean, openedByUser: boolean },
tabs?: WidgetTab[]
): void {
const state = get(sidebarStore)
const { widgetsState } = state
@ -114,8 +115,8 @@ export function openWidget (
widgetsState.set(widget._id, {
_id: widget._id,
data: data ?? widgetState?.data,
tab: widgetState?.tab,
tabs: widgetState?.tabs ?? [],
tab: widgetState?.tab ?? tabs?.[0]?.id,
tabs: widgetState?.tabs ?? tabs ?? [],
openedByUser
})
@ -352,6 +353,21 @@ export function updateTabData (widget: Ref<Widget>, tabId: string, data: Record<
})
}
export function updateWidgetState (widget: Ref<Widget>, newState: Partial<WidgetState>): void {
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget)
if (widgetState === undefined) return
widgetsState.set(widget, { ...widgetState, ...newState })
sidebarStore.set({
...state,
widgetsState
})
}
export function getSidebarObject (): Partial<Pick<Doc, '_id' | '_class'>> {
const state = get(sidebarStore)
if (state.variant !== SidebarVariant.EXPANDED || state.widget == null) {

View File

@ -92,12 +92,12 @@ export interface WidgetTab {
icon?: Asset | AnySvelteComponent
iconComponent?: AnyComponent
iconProps?: Record<string, any>
widget?: Ref<Widget>
isPinned?: boolean
allowedPath?: string
objectId?: Ref<Doc>
objectClass?: Ref<Class<Doc>>
data?: Record<string, any>
readonly?: boolean
}
export enum SidebarEvent {

View File

@ -1,7 +1,7 @@
import esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.ts', 'src/start.ts', 'src/config.ts', 'src/agent.ts', 'src/stt.ts'],
entryPoints: ['src/index.ts', 'src/start.ts', 'src/config.ts', 'src/agent.ts', 'src/stt.ts', 'src/type.ts'],
platform: 'node',
bundle: false,
minify: false,

View File

@ -15,7 +15,7 @@
"build": "node esbuild.config.js",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint --fix src/**/*.ts",
"format": "pnpm lint:fix && prettier --write src/**/*.ts"
"format": "prettier --write src/**/*.ts && pnpm lint:fix"
},
"devDependencies": {
"@types/node": "~20.11.16",
@ -33,8 +33,8 @@
},
"dependencies": {
"@deepgram/sdk": "^3.8.1",
"@livekit/agents": "^0.3.3",
"@livekit/rtc-node": "^0.9.2",
"@livekit/agents": "^0.3.5",
"@livekit/rtc-node": "^0.11.1",
"dotenv": "^16.4.5"
}
}

View File

@ -12,11 +12,11 @@ importers:
specifier: ^3.8.1
version: 3.8.1
'@livekit/agents':
specifier: ^0.3.3
version: 0.3.3
specifier: ^0.3.5
version: 0.3.5
'@livekit/rtc-node':
specifier: ^0.9.2
version: 0.9.2
specifier: ^0.11.1
version: 0.11.1
dotenv:
specifier: ^16.4.5
version: 16.4.5
@ -63,6 +63,9 @@ packages:
'@bufbuild/protobuf@1.10.0':
resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==}
'@bufbuild/protobuf@2.2.2':
resolution: {integrity: sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==}
'@deepgram/captions@1.2.0':
resolution: {integrity: sha512-8B1C/oTxTxyHlSFubAhNRgCbQ2SQ5wwvtlByn8sDYZvdDtdn/VE2yEPZ4BvUnrKWmsbTQY6/ooLV+9Ka2qmDSQ==}
engines: {node: '>=18.0.0'}
@ -240,44 +243,47 @@ packages:
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
deprecated: Use @eslint/object-schema instead
'@livekit/agents@0.3.3':
resolution: {integrity: sha512-7bw4o5/OcvGrI8hEISI25T29AAB7mV/zqJCsYh2gGPL5vl0xR/3dO0AM0rUR0qstL+7Aty0ciFYzuDxkKYsibg==}
'@livekit/agents@0.3.5':
resolution: {integrity: sha512-qjEIRkr/HdOvEOnvLKZMfnQ472bShQ1Ai2KKng8a1caSDHiLhQBeSb1tzA1Evsmm1k8hilXqh+81/SDj4cfiDA==}
'@livekit/mutex@1.1.0':
resolution: {integrity: sha512-XRLG+z/0uoyDioupjUiskjI06Y51U/IXVPJn7qJ+R3J75XX01irYVBM9MpxeJahpVoe9QhU4moIEolX+HO9U9g==}
'@livekit/protocol@1.27.0':
resolution: {integrity: sha512-jVb4zljNaYKoLiL5MBjGiO1+QKVsxMqXT/c0dwcKUW7NCLjAZXucoQVV1Y79FCbKwVnOCOtI6wwteEntbfk/Qw==}
'@livekit/rtc-node-darwin-arm64@0.9.2':
resolution: {integrity: sha512-40p8hx1URVbEA9qiu/fJv/wsRZnA3lq2Mfky8RQDtGcGzpkUaW9+Seate2UKRbTbQ4LaHo7tezV0uQQgal+nGA==}
'@livekit/rtc-node-darwin-arm64@0.11.1':
resolution: {integrity: sha512-M+Ui87H06ae19GGI7r937dS6hI84MBBTQAkkNlL7qd+pvdCAk25u0FYa8r4SOElKJ0VR3AbzeDoXTihLgpvjMg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@livekit/rtc-node-darwin-x64@0.9.2':
resolution: {integrity: sha512-Gw39yVRH27o52JMTa+CafG9RBIXuGkZ6UQt1EeuhdWxSUwSmFt+2e6hqeP8RizWXXRHfWdc7bGv9fUF2hBwCkA==}
'@livekit/rtc-node-darwin-x64@0.11.1':
resolution: {integrity: sha512-7G92fyuK2p+jdTH2cUJTNeAmtknTGsXuy0xbI727V7VzQvHFDXULCExRlgwn4t9TxvNlIjUpiltiQ6RCSai6zw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@livekit/rtc-node-linux-arm64-gnu@0.9.2':
resolution: {integrity: sha512-Whhh8S+BQ/rXO1Aorqq2lv7CpI4FeApf9N+VHZEHqT49q5/sPvlXu2TeCAOsMR5r5QdMjG5z7ZQ8ANfMLl6ceA==}
'@livekit/rtc-node-linux-arm64-gnu@0.11.1':
resolution: {integrity: sha512-vqZN9+87Pvxit7auYVW69M+GvUPnf+EwipIJ92GgCJA3Ir1Tcceu5ud5/Ic+0FzSoV0cotVVlQNm74F0tQvyCg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@livekit/rtc-node-linux-x64-gnu@0.9.2':
resolution: {integrity: sha512-1mPQG5Y9PuzBa2rAYgUw5Vd3CT2i9WdEtrwTw7G2q3yfebG09ZNOKNQ/0uNQaBQz8a82YpswuJHkrkOM5N2kKA==}
'@livekit/rtc-node-linux-x64-gnu@0.11.1':
resolution: {integrity: sha512-smHZUMfgILQh6/eoauYNe/VlKwQCp4/4jWxiIADHY+mtDtVSvQ9zB6y4GP8FrpohRwFWesKCUpvPBypU0Icrng==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@livekit/rtc-node-win32-x64-msvc@0.9.2':
resolution: {integrity: sha512-N2oaii+RwI8IIj8RYegK8qknZj6p7o48m6vrPzI/aDRHDY9iWRChSs+a7qKGlg6B4sFHikYWkHFzOiEWf0v68w==}
'@livekit/rtc-node-win32-x64-msvc@0.11.1':
resolution: {integrity: sha512-bTWVtb+UiRPFjiuhrqq40gt5vs5mMPTa1e+kd2jGQPTOlKZPLArQ0WgFcep2TAy1zmcpOgfeM1XRPVFhZl7G1A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@livekit/rtc-node@0.9.2':
resolution: {integrity: sha512-oJZUeXAc56ExPJCnNwnq8mR+qCE1AB78eKOmvra9v7UkGXBkdUVhNBwyNZliSnj7fj8IcVzaD3xF1WpLzFBVsw==}
'@livekit/rtc-node@0.11.1':
resolution: {integrity: sha512-EFw+giPll12fcXATZpN2zKkE3umYJAdHvfjW+Yu0aBjwfxbUBXu8rz6le2CzDNvGmRwR888DSZXFZfYikwZgiw==}
engines: {node: '>= 18'}
'@livekit/typed-emitter@3.0.0':
@ -1424,6 +1430,8 @@ snapshots:
'@bufbuild/protobuf@1.10.0': {}
'@bufbuild/protobuf@2.2.2': {}
'@deepgram/captions@1.2.0':
dependencies:
dayjs: 1.11.13
@ -1544,10 +1552,11 @@ snapshots:
'@humanwhocodes/object-schema@2.0.3': {}
'@livekit/agents@0.3.3':
'@livekit/agents@0.3.5':
dependencies:
'@livekit/mutex': 1.1.0
'@livekit/protocol': 1.27.0
'@livekit/rtc-node': 0.9.2
'@livekit/rtc-node': 0.11.1
commander: 12.1.0
livekit-server-sdk: 2.7.2
pino: 8.21.0
@ -1558,35 +1567,38 @@ snapshots:
- bufferutil
- utf-8-validate
'@livekit/mutex@1.1.0': {}
'@livekit/protocol@1.27.0':
dependencies:
'@bufbuild/protobuf': 1.10.0
'@livekit/rtc-node-darwin-arm64@0.9.2':
'@livekit/rtc-node-darwin-arm64@0.11.1':
optional: true
'@livekit/rtc-node-darwin-x64@0.9.2':
'@livekit/rtc-node-darwin-x64@0.11.1':
optional: true
'@livekit/rtc-node-linux-arm64-gnu@0.9.2':
'@livekit/rtc-node-linux-arm64-gnu@0.11.1':
optional: true
'@livekit/rtc-node-linux-x64-gnu@0.9.2':
'@livekit/rtc-node-linux-x64-gnu@0.11.1':
optional: true
'@livekit/rtc-node-win32-x64-msvc@0.9.2':
'@livekit/rtc-node-win32-x64-msvc@0.11.1':
optional: true
'@livekit/rtc-node@0.9.2':
'@livekit/rtc-node@0.11.1':
dependencies:
'@bufbuild/protobuf': 1.10.0
'@bufbuild/protobuf': 2.2.2
'@livekit/mutex': 1.1.0
'@livekit/typed-emitter': 3.0.0
optionalDependencies:
'@livekit/rtc-node-darwin-arm64': 0.9.2
'@livekit/rtc-node-darwin-x64': 0.9.2
'@livekit/rtc-node-linux-arm64-gnu': 0.9.2
'@livekit/rtc-node-linux-x64-gnu': 0.9.2
'@livekit/rtc-node-win32-x64-msvc': 0.9.2
'@livekit/rtc-node-darwin-arm64': 0.11.1
'@livekit/rtc-node-darwin-x64': 0.11.1
'@livekit/rtc-node-linux-arm64-gnu': 0.11.1
'@livekit/rtc-node-linux-x64-gnu': 0.11.1
'@livekit/rtc-node-win32-x64-msvc': 0.11.1
'@livekit/typed-emitter@3.0.0': {}

View File

@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url'
import { RemoteParticipant, RemoteTrack, RemoteTrackPublication, RoomEvent, TrackKind } from '@livekit/rtc-node'
import { STT } from './stt.js'
import { Metadata } from './type.js'
import { Metadata, TranscriptionStatus } from './type.js'
function parseMetadata (metadata: string): Metadata {
try {
@ -30,17 +30,20 @@ function parseMetadata (metadata: string): Metadata {
return {}
}
function applyMetadata (data: string, stt: STT): void {
if (data === '') return
function applyMetadata (data: string | undefined, stt: STT): void {
if (data == null || data === '') return
const metadata = parseMetadata(data)
if (metadata.language != null) {
stt.updateLanguage(metadata.language)
}
if (metadata.transcription === true) {
if (metadata.transcription === TranscriptionStatus.InProgress) {
stt.start()
} else if (metadata.transcription === false) {
} else if (
metadata.transcription === TranscriptionStatus.Completed ||
metadata.transcription === TranscriptionStatus.Idle
) {
stt.stop()
}
}
@ -50,7 +53,15 @@ export default defineAgent({
await ctx.connect()
await ctx.waitForParticipant()
const stt = new STT(ctx.room.name)
const roomName = ctx.room.name
if (roomName === undefined) {
console.error('Room name is undefined')
ctx.shutdown()
return
}
const stt = new STT(roomName)
applyMetadata(ctx.room.metadata, stt)
@ -76,23 +87,17 @@ export default defineAgent({
}
)
ctx.room.on(
RoomEvent.TrackMuted,
(publication) => {
ctx.room.on(RoomEvent.TrackMuted, (publication) => {
if (publication.kind === TrackKind.KIND_AUDIO) {
stt.mute(publication.sid)
}
}
)
})
ctx.room.on(
RoomEvent.TrackUnmuted,
(publication) => {
ctx.room.on(RoomEvent.TrackUnmuted, (publication) => {
if (publication.kind === TrackKind.KIND_AUDIO) {
stt.unmute(publication.sid)
}
}
)
})
ctx.addShutdownCallback(async () => {
stt.close()
@ -101,5 +106,10 @@ export default defineAgent({
})
export function runAgent (): void {
cli.runApp(new WorkerOptions({ agent: fileURLToPath(import.meta.url), permissions: new WorkerPermissions(true, true, true, true, [], true) }))
cli.runApp(
new WorkerOptions({
agent: fileURLToPath(import.meta.url),
permissions: new WorkerPermissions(true, true, true, true, [], true)
})
)
}

View File

@ -18,9 +18,10 @@ import {
createClient,
DeepgramClient,
ListenLiveClient,
LiveTranscriptionEvents,
LiveSchema,
LiveTranscriptionEvent,
LiveSchema
LiveTranscriptionEvents,
SOCKET_STATES
} from '@deepgram/sdk'
import config from './config.js'
@ -121,11 +122,7 @@ export class STT {
}
}
unsubscribe (
_: RemoteTrack | undefined,
publication: RemoteTrackPublication,
participant: RemoteParticipant
): void {
unsubscribe (_: RemoteTrack | undefined, publication: RemoteTrackPublication, participant: RemoteParticipant): void {
this.trackBySid.delete(publication.sid)
this.participantBySid.delete(participant.sid)
this.mutedTracks.delete(publication.sid)
@ -140,6 +137,7 @@ export class STT {
const dgConnection = this.dgConnectionBySid.get(sid)
if (dgConnection !== undefined) {
dgConnection.removeAllListeners()
dgConnection.disconnect()
}
@ -182,7 +180,10 @@ export class STT {
const prevValue = prevData?.value ?? ''
if (data.is_final === true) {
// TODO: how to join the final transcript ?
this.transcriptsBySid.set(sid, { value: prevValue + ' ' + transcript, startedOn: prevData?.startedOn ?? Date.now() })
this.transcriptsBySid.set(sid, {
value: prevValue + ' ' + transcript,
startedOn: prevData?.startedOn ?? Date.now()
})
}
if (data.speech_final === true) {
const result = this.transcriptsBySid.get(sid)?.value
@ -194,8 +195,18 @@ export class STT {
}
})
dgConnection.addListener(LiveTranscriptionEvents.UtteranceEnd, () => {
const result = this.transcriptsBySid.get(sid)?.value ?? ''
if (result.length > 0) {
void this.sendToPlatform(result, sid)
this.transcriptsBySid.delete(sid)
}
})
dgConnection.on(LiveTranscriptionEvents.Close, (d) => {
console.log('Connection closed.', d, track.sid)
this.stopDeepgram(track.sid)
})
dgConnection.on(LiveTranscriptionEvents.Error, (err) => {
@ -215,6 +226,7 @@ export class STT {
stream.close()
return
}
if (dgConnection.getReadyState() !== SOCKET_STATES.open) continue
const buf = Buffer.from(frame.data.buffer)
dgConnection.send(buf)
}
@ -228,7 +240,7 @@ export class STT {
}
try {
await fetch(`${config.PlatformUrl}/transcript`, {
await fetch(`${config.PlatformUrl}/love/transcript`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -13,7 +13,13 @@
// limitations under the License.
//
export enum TranscriptionStatus {
Idle = 'idle',
InProgress = 'inProgress',
Completed = 'completed'
}
export interface Metadata {
transcription?: boolean
transcription?: TranscriptionStatus
language?: string
}

View File

@ -73,6 +73,7 @@
"@hcengineering/setting": "^0.6.17",
"@hcengineering/text": "^0.6.5",
"@hcengineering/workbench": "^0.6.16",
"@hcengineering/love": "^0.6.0",
"cors": "^2.8.5",
"dotenv": "~16.0.0",
"express": "^4.19.2",

Some files were not shown because too many files have changed in this diff Show More