Instant office links (#6121)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-07-23 22:46:37 +05:00 committed by GitHub
parent 1b598b9ad8
commit 807426e0b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 121 additions and 36 deletions

View File

@ -534,17 +534,17 @@ export async function getBlobURL (blob: Blob): Promise<string> {
/**
* @public
*/
export async function copyTextToClipboard (text: string): Promise<void> {
export async function copyTextToClipboard (text: string | Promise<string>): Promise<void> {
try {
// Safari specific behavior
// see https://bugs.webkit.org/show_bug.cgi?id=222262
const clipboardItem = new ClipboardItem({
'text/plain': Promise.resolve(text)
'text/plain': text instanceof Promise ? text : Promise.resolve(text)
})
await navigator.clipboard.write([clipboardItem])
} catch {
// Fallback to default clipboard API implementation
await navigator.clipboard.writeText(text)
await navigator.clipboard.writeText(text instanceof Promise ? await text : text)
}
}

View File

@ -14,7 +14,13 @@
-->
<script lang="ts">
import { OK, setMetadata, Severity, Status } from '@hcengineering/platform'
import { fetchMetadataLocalStorage, getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
import {
fetchMetadataLocalStorage,
getCurrentLocation,
Location,
navigate,
setMetadataLocalStorage
} from '@hcengineering/ui'
import { checkJoined, join, signUpJoin } from '../utils'
import Form from './Form.svelte'
@ -85,6 +91,19 @@
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
if (location.query?.navigateUrl != null) {
try {
const loc = JSON.parse(decodeURIComponent(location.query.navigateUrl)) as Location
if (loc.path[1] === result.workspace) {
navigate(loc)
return
}
} catch (err: any) {
// Json parse error could be ignored
}
}
navigate({ path: [workbenchId, result.workspace] })
}
}
@ -126,6 +145,17 @@
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
if (location.query?.navigateUrl != null) {
try {
const loc = JSON.parse(decodeURIComponent(location.query.navigateUrl)) as Location
if (loc.path[1] === result.workspace) {
navigate(loc)
return
}
} catch (err: any) {
// Json parse error could be ignored
}
}
navigate({ path: [workbenchId, result.workspace] })
}
}

View File

@ -247,7 +247,7 @@ export async function getAccount (doNavigate: boolean = true): Promise<LoginInfo
export async function selectWorkspace (
workspace: string,
token: string | null | undefined
token?: string | null | undefined
): Promise<[Status, WorkspaceLoginInfo | undefined]> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
@ -449,7 +449,8 @@ export async function getInviteLink (
expHours: number,
mask: string,
limit: number | undefined,
role: AccountRole
role: AccountRole,
navigateUrl?: string
): Promise<string> {
const inviteId = await getInviteLinkId(expHours, mask, limit ?? -1, role)
const loc = getCurrentLocation()
@ -459,6 +460,9 @@ export async function getInviteLink (
loc.query = {
inviteId
}
if (navigateUrl !== undefined) {
loc.query.navigateUrl = navigateUrl
}
loc.fragment = undefined
const url = locationToUrl(loc)

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import { Ref, Doc, AccountRole } from '@hcengineering/core'
import { AccountRole, Doc, Ref } from '@hcengineering/core'
import type { Asset, IntlString, Metadata, Plugin, Resource, Status } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
@ -80,7 +80,13 @@ export default plugin(loginId, {
function: {
SendInvite: '' as Resource<(email: string, personId?: Ref<Doc>, role?: AccountRole) => Promise<void>>,
GetInviteLink: '' as Resource<
(expHours: number, mask: string, limit: number | undefined, role: AccountRole) => Promise<string>
(
expHours: number,
mask: string,
limit: number | undefined,
role: AccountRole,
navigateUrl?: string
) => Promise<string>
>,
LeaveWorkspace: '' as Resource<(email: string) => Promise<void>>,
ChangePassword: '' as Resource<(oldPassword: string, password: string) => Promise<void>>,

View File

@ -16,30 +16,31 @@
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 { getResource } from '@hcengineering/platform'
import { copyTextToClipboard, getClient } from '@hcengineering/presentation'
import {
IconUpOutline,
ModernButton,
PopupInstance,
SplitButton,
eventToHTMLElement,
getCurrentLocation,
showPopup,
PopupInstance,
type CompAndProps,
type AnySvelteComponent
type AnySvelteComponent,
type CompAndProps
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import love, { Room, RoomType, isOffice, roomAccessIcon } from '@hcengineering/love'
import plugin from '../plugin'
import { currentRoom, myInfo, myOffice } from '../stores'
import {
isCameraEnabled,
isConnected,
isFullScreen,
isMicEnabled,
isRecording,
isRecordingAvailable,
isSharingEnabled,
isFullScreen,
leaveRoom,
record,
screenSharing,
@ -116,10 +117,21 @@
}
}
async function getLink (): Promise<string> {
const roomInfo = await client.findOne(love.class.RoomInfo, { room: room._id })
if (roomInfo !== undefined) {
const navigateUrl = getCurrentLocation()
navigateUrl.query = {
meetId: roomInfo._id
}
const func = await getResource(login.function.GetInviteLink)
return await func(24 * 30, '', -1, AccountRole.Guest, JSON.stringify(navigateUrl))
}
return ''
}
async function copyGuestLink (): Promise<void> {
const getLink = await getResource(login.function.GetInviteLink)
const link = await getLink(24 * 30, '', -1, AccountRole.Guest)
await copyTextToClipboard(link)
await copyTextToClipboard(getLink())
linkCopied = true
clearTimeout(linkTimeout)
linkTimeout = setTimeout(() => {

View File

@ -13,14 +13,18 @@
// limitations under the License.
-->
<script lang="ts">
import { Contact, Person } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import { Floor as FloorType, Office, Room, isOffice } from '@hcengineering/love'
import { activeFloor, floors, rooms } from '../stores'
import love, { Floor as FloorType, Office, Room, RoomInfo, isOffice } from '@hcengineering/love'
import { getClient } from '@hcengineering/presentation'
import { deviceOptionsStore as deviceInfo, getCurrentLocation, navigate } from '@hcengineering/ui'
import { onMount } from 'svelte'
import { activeFloor, floors, infos, invites, myInfo, myRequests, rooms } from '../stores'
import { tryConnect } from '../utils'
import Floor from './Floor.svelte'
import FloorConfigure from './FloorConfigure.svelte'
import Floors from './Floors.svelte'
import { Contact, Person } from '@hcengineering/contact'
function getRooms (rooms: Room[], floor: Ref<FloorType>): Room[] {
return rooms.filter((p) => p.floor === floor)
@ -36,6 +40,28 @@
.map((p) => (p as Office).person) as Ref<Person>[]
$: $deviceInfo.replacedPanel = replacedPanel
onMount(async () => {
const loc = getCurrentLocation()
const { meetId, ...query } = loc.query ?? {}
if (meetId != null) {
loc.query = Object.keys(query).length === 0 ? undefined : query
navigate(loc, true)
const client = getClient()
const info = await client.findOne(love.class.RoomInfo, { _id: meetId as Ref<RoomInfo> })
if (info === undefined) return
const room = $rooms.find((p) => p._id === info.room)
if (room === undefined) return
tryConnect(
$personByIdStore,
$myInfo,
room,
$infos.filter((p) => p.room === room._id),
$myRequests,
$invites
)
}
})
</script>
<Floors bind:floor={selectedFloor} bind:configure />

View File

@ -17,7 +17,7 @@
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, showTooltip } from '@hcengineering/ui'
import { Icon, Label, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import love from '../plugin'
import { invites, myInfo, myRequests } from '../stores'

View File

@ -1,11 +1,6 @@
import { Analytics } from '@hcengineering/analytics'
import contact, { getName, type Person, type PersonAccount } from '@hcengineering/contact'
import core, { concatLink, getCurrentAccount, type IdMap, type Ref, type Space } from '@hcengineering/core'
import { getEmbeddedLabel, getMetadata, type IntlString } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { getCurrentLocation, navigate, type DropdownTextItem } from '@hcengineering/ui'
import { KrispNoiseFilter, isKrispNoiseFilterSupported } from '@livekit/krisp-noise-filter'
import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors'
import {
RequestStatus,
RoomAccess,
@ -18,15 +13,20 @@ import {
type ParticipantInfo,
type Room
} from '@hcengineering/love'
import { getEmbeddedLabel, getMetadata, type IntlString } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { getCurrentLocation, navigate, type DropdownTextItem } from '@hcengineering/ui'
import { KrispNoiseFilter, isKrispNoiseFilterSupported } from '@livekit/krisp-noise-filter'
import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors'
import {
ConnectionState,
Room as LKRoom,
LocalAudioTrack,
type LocalTrack,
LocalVideoTrack,
RoomEvent,
Track,
type AudioCaptureOptions,
type LocalTrack,
type LocalTrackPublication,
type RemoteParticipant,
type RemoteTrack,
@ -568,7 +568,10 @@ export async function tryConnect (
const currentPerson = personByIdStore.get((me as PersonAccount).person)
if (currentPerson === undefined) return
const client = getClient()
// guests can't join without invite
if (!client.getHierarchy().hasMixin(currentPerson, contact.mixin.Employee)) return
if (room._id === currentInfo?.room) return
if (room.access === RoomAccess.DND) return
const thisRoomRequest = currentRequests.find((p) => p.room === room._id)

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import contact, { Employee, Person, PersonAccount, getName, formatName } from '@hcengineering/contact'
import contact, { Employee, Person, PersonAccount, formatName, getName } from '@hcengineering/contact'
import core, {
Account,
Ref,
@ -25,7 +25,6 @@ import core, {
TxUpdateDoc,
UserStatus
} from '@hcengineering/core'
import { TriggerControl } from '@hcengineering/server-core'
import love, {
Invite,
JoinRequest,
@ -36,10 +35,11 @@ import love, {
isOffice,
loveId
} from '@hcengineering/love'
import { createPushNotification, isAllowed } from '@hcengineering/server-notification-resources'
import notification from '@hcengineering/notification'
import { workbenchId } from '@hcengineering/workbench'
import { translate } from '@hcengineering/platform'
import { TriggerControl } from '@hcengineering/server-core'
import { createPushNotification, isAllowed } from '@hcengineering/server-notification-resources'
import { workbenchId } from '@hcengineering/workbench'
export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) as TxMixin<Person, Employee>
@ -202,11 +202,15 @@ function setDefaultRoomAccess (info: ParticipantInfo, roomInfos: RoomInfo[], con
const oldRoomInfo = roomInfos.find((ri) => ri.persons.includes(info.person))
if (oldRoomInfo !== undefined) {
oldRoomInfo.persons = oldRoomInfo.persons.filter((p) => p !== info.person)
res.push(
control.txFactory.createTxUpdateDoc(love.class.RoomInfo, core.space.Workspace, oldRoomInfo._id, {
persons: oldRoomInfo.persons
})
)
if (oldRoomInfo.persons.length === 0) {
res.push(control.txFactory.createTxRemoveDoc(oldRoomInfo._class, oldRoomInfo.space, oldRoomInfo._id))
} else {
res.push(
control.txFactory.createTxUpdateDoc(love.class.RoomInfo, core.space.Workspace, oldRoomInfo._id, {
persons: oldRoomInfo.persons
})
)
}
if (oldRoomInfo.persons.length === 0) {
const resetAccessTx = control.txFactory.createTxUpdateDoc(
oldRoomInfo.isOffice ? love.class.Office : love.class.Room,