Tldraw/apps/vscode/extension/src/WebViewMessageHandler.ts

213 wiersze
5.7 KiB
TypeScript

import { UnknownRecord } from '@tldraw/tlstore'
import { isEqual } from 'lodash'
import fetch from 'node-fetch'
import * as vscode from 'vscode'
import { TLDrawDocument } from './TldrawDocument'
import { loadFile } from './file'
// @ts-ignore
import type { VscodeMessage } from '../../messages'
import { nicelog } from './utils'
export const GlobalStateKeys = {
ShowV1FileOpenWarning: 'showV1fileOpenWarning',
UserId: 'userId',
}
export class WebViewMessageHandler {
multiplayerOmitKeys = /^(user_presence:|camera:|user:|user_document:|instance:)/
newDocumentOmitKeys = /^(user_presence:|camera:|user:|user_document:|instance:|document:|page:)/
constructor(
private document: TLDrawDocument,
private webviewPanel: vscode.WebviewPanel,
private context: vscode.ExtensionContext,
private userId: unknown,
private assetSrc: string
) {}
isLoaded = false
firstChangeDone = false
handle = async (e: VscodeMessage) => {
if (!this.document) return
switch (e.type) {
case 'vscode:ready-to-receive-file': {
// Send the initial document content to bootstrap the Tldraw/Tldraw component.
this.webviewPanel.webview.postMessage({
type: 'vscode:opened-file',
data: {
fileContents: JSON.stringify(this.document.documentData),
uri: this.document.uri.toString(),
userId: this.userId,
assetSrc: this.assetSrc,
isDarkMode:
this.document.isBlankDocument &&
(vscode.window.activeColorTheme.kind === 2 ||
vscode.window.activeColorTheme.kind === 3),
},
} as VscodeMessage)
break
}
case 'vscode:open-window': {
vscode.env.openExternal(vscode.Uri.parse(e.data.url))
break
}
case 'vscode:undo': {
vscode.commands.executeCommand('undo')
break
}
case 'vscode:redo': {
vscode.commands.executeCommand('redo')
break
}
case 'vscode:refresh-page': {
vscode.commands.executeCommand('workbench.action.reloadWindow')
break
}
case 'vscode:hard-reset': {
await this.document.loadBlankDocument()
vscode.commands.executeCommand('workbench.action.reloadWindow')
break
}
case 'vscode:bookmark/request': {
const url = e.data.url
fetch('https://www.tldraw.com/api/bookmark', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// We can fake the origin here because we're in node.js
origin: 'https://www.tldraw.com',
},
body: JSON.stringify({
url,
}),
})
.then((resp) => {
return resp.json()
})
.then((json: any) => {
this.webviewPanel.webview.postMessage({
type: 'vscode:bookmark/response',
uuid: e.uuid,
data: {
url,
title: json.title,
description: json.description,
image: json.image,
},
})
})
.catch((error: any) => {
this.webviewPanel.webview.postMessage({
type: 'vscode:bookmark/error',
data: {
error: error.toString(),
},
})
})
break
}
case 'vscode:editor-loaded': {
this.isLoaded = true
break
}
case 'vscode:v1-file-opened': {
const showV1FileOpenWarning = this.context.globalState.get(
GlobalStateKeys.ShowV1FileOpenWarning
)
if (!showV1FileOpenWarning) return
const { backup, open, description, dontAskAgain } = e.data
vscode.window
.showInformationMessage(description, open, dontAskAgain, backup)
.then((result) => {
if (result === backup) {
this.document.v1Backup(e.data.backupSaved, e.data.backupFailed)
} else if (result === dontAskAgain) {
this.context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, false)
}
})
break
}
case 'vscode:editor-updated': {
if (!this.isLoaded) return
if (!this.firstChangeDone) {
this.firstChangeDone = true
return
}
const raw = e.data.fileContents
if (!raw) return
// The event will contain the new TDFile as JSON.
const nextFile = loadFile(raw)
const existingDoc = this.document.documentData
let isSame = false
if (existingDoc?.records?.length > 0) {
const oldDoc = this.omit(existingDoc.records, this.multiplayerOmitKeys)
const newDoc = this.omit(nextFile.records, this.multiplayerOmitKeys)
isSame = isEqual(oldDoc, newDoc)
} else {
const newDoc = this.omit(nextFile.records, this.newDocumentOmitKeys)
isSame = isEqual(newDoc, [])
}
if (!isSame) {
this.document.makeEdit(nextFile)
}
break
}
case 'vscode:hide-v1-file-open-warning': {
this.context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, false)
break
}
case 'vscode:cancel-v1-migrate': {
vscode.commands.executeCommand('workbench.action.closeActiveEditor')
break
}
}
}
private omit = (records: UnknownRecord[], keys: RegExp) => {
return records.filter((record) => {
return !record.id.match(keys)
})
}
findDiff(oldDoc: Record<string, any>, newDoc: Record<string, any>) {
const newRecords = Object.values(newDoc)
const oldRecords = Object.values(oldDoc)
for (const oldRecord of oldRecords) {
const newRecord = newRecords.find((r: any) => r.id === oldRecord.id)
if (!newRecord) {
nicelog('record missing in new doc', oldRecord)
continue
} else {
if (!isEqual(oldRecord, newRecord)) {
nicelog('record different', oldRecord, newRecord)
continue
}
}
}
for (const newRecord of newRecords) {
const oldRecord = oldRecords.find((r: any) => r.id === newRecord.id)
if (!oldRecord) {
nicelog('record missing in oldDoc doc', newRecord)
continue
} else {
if (!isEqual(newRecord, oldRecord)) {
nicelog('record different', newRecord, oldRecord)
continue
}
}
}
}
}