mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-28 14:36:59 +03:00
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
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:
parent
1ce18226ab
commit
e34f59ca37
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@ -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"],
|
||||
|
@ -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:
|
||||
|
@ -77,7 +77,7 @@ function defineMessageActions (builder: Builder): void {
|
||||
group: 'edit'
|
||||
}
|
||||
},
|
||||
chunter.action.ReplyToThreadAction
|
||||
activity.action.Reply
|
||||
)
|
||||
|
||||
createAction(
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -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>,
|
||||
|
@ -94,6 +94,7 @@
|
||||
"TypeHere": "Type here...",
|
||||
"FullSize": "Full size",
|
||||
"UseMaxWidth": "Max width",
|
||||
"Sidebar": "Sidebar"
|
||||
"Sidebar": "Sidebar",
|
||||
"Language": "Language"
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@
|
||||
"TypeHere": "Escribe aquí...",
|
||||
"FullSize": "Tamaño completo",
|
||||
"UseMaxWidth": "Ancho máximo",
|
||||
"Sidebar": "Barra lateral"
|
||||
"Sidebar": "Barra lateral",
|
||||
"Language": "Idioma"
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@
|
||||
"TypeHere": "Tapez ici...",
|
||||
"FullSize": "Taille réelle",
|
||||
"UseMaxWidth": "Largeur maximale",
|
||||
"Sidebar": "Barre latérale"
|
||||
"Sidebar": "Barre latérale",
|
||||
"Language": "Langue"
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@
|
||||
"TypeHere": "Scrivi qui...",
|
||||
"FullSize": "Dimensione intera",
|
||||
"UseMaxWidth": "Larghezza massima",
|
||||
"Sidebar": "Barra laterale"
|
||||
"Sidebar": "Barra laterale",
|
||||
"Language": "Lingua"
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@
|
||||
"TypeHere": "Escreva aqui...",
|
||||
"FullSize": "Tamanho completo",
|
||||
"UseMaxWidth": "Largura máxima",
|
||||
"Sidebar": "Barra lateral"
|
||||
"Sidebar": "Barra lateral",
|
||||
"Language": "Idioma"
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@
|
||||
"TypeHere": "Вводите здесь...",
|
||||
"FullSize": "Полный размер",
|
||||
"UseMaxWidth": "Максимальная ширина",
|
||||
"Sidebar": "Боковая панель"
|
||||
"Sidebar": "Боковая панель",
|
||||
"Language": "Язык"
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@
|
||||
"TypeHere": "在此输入...",
|
||||
"FullSize": "全尺寸",
|
||||
"UseMaxWidth": "使用最大宽度",
|
||||
"Sidebar": "侧边栏"
|
||||
"Sidebar": "侧边栏",
|
||||
"Language": "语言"
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>,
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
}
|
||||
})
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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: {
|
||||
|
108
plugins/ai-bot-resources/src/requests.ts
Normal file
108
plugins/ai-bot-resources/src/requests.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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} />
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 />
|
||||
|
@ -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'
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,13 @@
|
||||
"ExitingFullscreenMode": "Выход из полноэкранного режима",
|
||||
"Select": "Выбрать",
|
||||
"ChooseShare": "Выберите, чем вы хотите поделиться",
|
||||
"CreateMeeting": "Создать встречу"
|
||||
"CreateMeeting": "Создать встречу",
|
||||
"MoreOptions": "Дополнительные опции",
|
||||
"StartTranscription": "Начать транскрипцию",
|
||||
"StopTranscription": "Остановить транскрипцию",
|
||||
"Meeting": "Встреча",
|
||||
"Transcription": "Транскрипция",
|
||||
"StartWithTranscription": "Начинать с транскрипцией",
|
||||
"MeetingMinutes": "Протоколы встреч"
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,13 @@
|
||||
"ExitingFullscreenMode": "退出全屏模式",
|
||||
"Select": "选择",
|
||||
"ChooseShare": "选择共享内容",
|
||||
"CreateMeeting": "创建会议"
|
||||
"CreateMeeting": "创建会议",
|
||||
"MoreOptions": "更多选项",
|
||||
"StartTranscription": "开始转录",
|
||||
"StopTranscription": "停止转录",
|
||||
"Meeting": "会议",
|
||||
"Transcription": "转录",
|
||||
"StartWithTranscription": "开始转录",
|
||||
"MeetingMinutes": "会议记录"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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))
|
||||
|
@ -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} />
|
||||
|
41
plugins/love-resources/src/components/FloorView.svelte
Normal file
41
plugins/love-resources/src/components/FloorView.svelte
Normal 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>
|
@ -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>
|
||||
|
36
plugins/love-resources/src/components/LanguageIcon.svelte
Normal file
36
plugins/love-resources/src/components/LanguageIcon.svelte
Normal 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>
|
@ -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}
|
@ -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']} />
|
@ -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)
|
||||
|
27
plugins/love-resources/src/components/RoomLanguage.svelte
Normal file
27
plugins/love-resources/src/components/RoomLanguage.svelte
Normal 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>
|
@ -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>
|
@ -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}
|
||||
|
@ -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>
|
@ -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>
|
45
plugins/love-resources/src/components/widget/ChatTab.svelte
Normal file
45
plugins/love-resources/src/components/widget/ChatTab.svelte
Normal 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
|
||||
/>
|
@ -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}
|
@ -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
|
||||
/>
|
@ -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 {
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -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' }
|
||||
}
|
||||
|
@ -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'
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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++
|
||||
}
|
||||
}
|
||||
|
@ -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 |
@ -126,6 +126,7 @@
|
||||
"Leave": "Leave",
|
||||
"Join": "Join",
|
||||
"Copied": "Copied",
|
||||
"Title": "Title",
|
||||
"HideArchived": "Hide archived"
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +121,7 @@
|
||||
"Leave": "Salir",
|
||||
"Join": "Unirse",
|
||||
"Copied": "Copiado",
|
||||
"Title": "Título",
|
||||
"HideArchived": "Ocultar archivadas"
|
||||
}
|
||||
}
|
@ -121,6 +121,7 @@
|
||||
"Leave": "Quitter",
|
||||
"Join": "Rejoindre",
|
||||
"Copied": "Copié",
|
||||
"Title": "Titre",
|
||||
"HideArchived": "Masquer les archives"
|
||||
}
|
||||
}
|
@ -121,6 +121,7 @@
|
||||
"Leave": "Esci",
|
||||
"Join": "Unisciti",
|
||||
"Copied": "Copiato",
|
||||
"Title": "Titolo",
|
||||
"HideArchived": "Nascondi archiviato"
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +121,7 @@
|
||||
"Leave": "Sair",
|
||||
"Join": "Ingressar",
|
||||
"Copied": "Copiado",
|
||||
"Title": "Título",
|
||||
"HideArchived": "Ocultar arquivado"
|
||||
}
|
||||
}
|
@ -123,6 +123,7 @@
|
||||
"Leave": "Покинуть",
|
||||
"Join": "Присоединиться",
|
||||
"Copied": "Скопировано",
|
||||
"Title": "Заголовок",
|
||||
"HideArchived": "Скрыть архивные"
|
||||
}
|
||||
}
|
||||
|
@ -126,6 +126,7 @@
|
||||
"Leave": "离开",
|
||||
"Join": "加入",
|
||||
"Copied": "已复制",
|
||||
"Title": "标题",
|
||||
"HideArchived": "隱藏已存檔"
|
||||
}
|
||||
}
|
||||
|
@ -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`
|
||||
})
|
||||
|
@ -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']
|
||||
)
|
||||
|
@ -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>,
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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': {}
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user