google meet: add hardware whiteboard integration (#3765)
pushing out my changes but this is a draft. we need to do look into this more: - [ ] we might need to reach out to Google support and request to be put on their "start a whiteboard" whitelist/partner list? it's actually pretty unclear how to get on that list. I don't see any permissions/API scopes that is meant to enable that 🤔 - [ ] but maybe this is something that you @steveruizok as Google Workspace admin see on your end? let's look together when you get back. I initially tried doing a bundle (using esbuild (and i tried parcel/rollup too)) but it didn't feel like the right path, and also it didn't work when loading it in the Apps Scripts. So then I went the route of just doing an iframe and I think that feels much better. This means though that we do want our iframe protector to let through this usecase. But also, we could maybe just redirect always to a new room? I'm not sure yet. The build script helps either build the prod or staging version depending on what you want. Once we do find that the staging version works, then we'll go through the process of: - [x] applied for https://developers.google.com/workspace/preview (already did this to get access to new Google Meet APIs just in case) - [x] added to google analytics (already done) - [ ] turn off testing mode for oauth and submit for review - [ ] continue publishing process to Create a store listing for our prod app: https://developers.google.com/workspace/marketplace/how-to-publish ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [ ] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [x] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [x] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Release Notes - Google Meet: add hardware whiteboard integration
This commit is contained in:
parent
16ba1eb2c2
commit
9ffd7f15ee
11 changed files with 1015 additions and 67 deletions
1
apps/apps-script/.gitignore
vendored
Normal file
1
apps/apps-script/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.clasp.json
|
33
apps/apps-script/appsscript.json
Normal file
33
apps/apps-script/appsscript.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"addOns": {
|
||||||
|
"common": {
|
||||||
|
"layoutProperties": {
|
||||||
|
"primaryColor": "#000",
|
||||||
|
"secondaryColor": "#fff"
|
||||||
|
},
|
||||||
|
"logoUrl": "TLDRAW_HOST/android-chrome-maskable-512x512.png",
|
||||||
|
"name": "tldraw"
|
||||||
|
},
|
||||||
|
"meet": {
|
||||||
|
"web": {
|
||||||
|
"sidePanelUri": "TLDRAW_HOST/ts-side",
|
||||||
|
"mainStageUri": "TLDRAW_HOST/ts",
|
||||||
|
"supportsScreenSharing": true,
|
||||||
|
"logoUrl": "TLDRAW_HOST/android-chrome-maskable-512x512.png",
|
||||||
|
"addOnOrigins": ["TLDRAW_HOST"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeZone": "Europe/London",
|
||||||
|
"dependencies": {},
|
||||||
|
"exceptionLogging": "STACKDRIVER",
|
||||||
|
"runtimeVersion": "V8",
|
||||||
|
"oauthScopes": [
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile",
|
||||||
|
"https://www.googleapis.com/auth/documents.currentonly",
|
||||||
|
"https://www.googleapis.com/auth/script.external_request",
|
||||||
|
"https://www.googleapis.com/auth/workspace.linkpreview"
|
||||||
|
],
|
||||||
|
"urlFetchWhitelist": ["TLDRAW_HOST/"]
|
||||||
|
}
|
30
apps/apps-script/build-workspace-app.ts
Normal file
30
apps/apps-script/build-workspace-app.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { mkdirSync, writeFileSync } from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { exec } from '../../scripts/lib/exec'
|
||||||
|
import { readFileIfExists } from '../../scripts/lib/file'
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
await exec('rm', ['-rf', 'dist'])
|
||||||
|
mkdirSync('dist')
|
||||||
|
|
||||||
|
const appsScriptPath = './appsscript.json'
|
||||||
|
|
||||||
|
await exec('cp', ['-r', appsScriptPath, 'dist'])
|
||||||
|
|
||||||
|
const isProduction = process.env.IS_PRODUCTION === '1'
|
||||||
|
const scriptId = isProduction
|
||||||
|
? '1FWcAvz7Rl4iPXQX3KmXm2mNG_RK2kryS7Bja8Y7RHvuAHnic51p_pqe7'
|
||||||
|
: '1cJfZM0M_rGU-nYgG-4KR1DnERb7itkCsl1QmlqPxFvHnrz5n6Gfy8iht'
|
||||||
|
writeFileSync('./.clasp.json', `{"scriptId":"${scriptId}","rootDir":"./dist"}`)
|
||||||
|
|
||||||
|
const host = isProduction ? 'https://www.tldraw.com' : 'https://staging.tldraw.com'
|
||||||
|
await replaceInFile(appsScriptPath, 'TLDRAW_HOST', host)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replaceInFile(filename: string, searchValue: string, replaceValue: string) {
|
||||||
|
let contents = (await readFileIfExists(path.join('dist', filename))) ?? ''
|
||||||
|
contents = contents.replaceAll(searchValue, replaceValue)
|
||||||
|
writeFileSync(path.join('dist', filename), contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
build()
|
46
apps/apps-script/package.json
Normal file
46
apps/apps-script/package.json
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "apps-scripts",
|
||||||
|
"description": "tldraw for Google Apps",
|
||||||
|
"version": "2.0.0-beta.2",
|
||||||
|
"private": true,
|
||||||
|
"author": {
|
||||||
|
"name": "tldraw Inc.",
|
||||||
|
"email": "hello@tldraw.com"
|
||||||
|
},
|
||||||
|
"homepage": "https://tldraw.dev",
|
||||||
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/tldraw/tldraw"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tldraw/tldraw/issues"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tldraw",
|
||||||
|
"drawing",
|
||||||
|
"app",
|
||||||
|
"development",
|
||||||
|
"whiteboard",
|
||||||
|
"canvas",
|
||||||
|
"infinite"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"lint": "yarn run -T tsx ../../scripts/lint.ts",
|
||||||
|
"glogin": "clasp login",
|
||||||
|
"glogout": "clasp logout",
|
||||||
|
"gcreate": "clasp create --type standalone --title \"tldraw.com\" --rootDir ./dist",
|
||||||
|
"gpull": "yarn build && clasp pull",
|
||||||
|
"gpush": "yarn build && clasp push",
|
||||||
|
"gpull:staging": "yarn build:staging && clasp pull",
|
||||||
|
"gpush:staging": "yarn build:staging && clasp push",
|
||||||
|
"build": "IS_PRODUCTION=1 yarn run -T tsx build-workspace-app.ts",
|
||||||
|
"build:staging": "yarn run -T tsx build-workspace-app.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@google/clasp": "^2.4.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/google-apps-script": "^1.0.83"
|
||||||
|
}
|
||||||
|
}
|
28
apps/apps-script/tsconfig.json
Normal file
28
apps/apps-script/tsconfig.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"references": []
|
||||||
|
}
|
|
@ -34,6 +34,14 @@ exports[`the_routes 1`] = `
|
||||||
"reactRouterPattern": "/s/:roomId",
|
"reactRouterPattern": "/s/:roomId",
|
||||||
"vercelRouterPattern": "^/s/[^/]*/?$",
|
"vercelRouterPattern": "^/s/[^/]*/?$",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"reactRouterPattern": "/ts",
|
||||||
|
"vercelRouterPattern": "^/ts/?$",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reactRouterPattern": "/ts-side",
|
||||||
|
"vercelRouterPattern": "^/ts-side/?$",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"reactRouterPattern": "/v/:roomId",
|
"reactRouterPattern": "/v/:roomId",
|
||||||
"vercelRouterPattern": "^/v/[^/]*/?$",
|
"vercelRouterPattern": "^/v/[^/]*/?$",
|
||||||
|
|
|
@ -10,6 +10,7 @@ export const ROOM_CONTEXT = {
|
||||||
HISTORY_SNAPSHOT: 'history-snapshot',
|
HISTORY_SNAPSHOT: 'history-snapshot',
|
||||||
HISTORY: 'history',
|
HISTORY: 'history',
|
||||||
LOCAL: 'local',
|
LOCAL: 'local',
|
||||||
|
PUBLIC_TOUCHSCREEN_APP: 'public-touchscreen-app',
|
||||||
} as const
|
} as const
|
||||||
type $ROOM_CONTEXT = (typeof ROOM_CONTEXT)[keyof typeof ROOM_CONTEXT]
|
type $ROOM_CONTEXT = (typeof ROOM_CONTEXT)[keyof typeof ROOM_CONTEXT]
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ const WHITELIST_CONTEXT: $ROOM_CONTEXT[] = [
|
||||||
ROOM_CONTEXT.PUBLIC_MULTIPLAYER,
|
ROOM_CONTEXT.PUBLIC_MULTIPLAYER,
|
||||||
ROOM_CONTEXT.PUBLIC_READONLY,
|
ROOM_CONTEXT.PUBLIC_READONLY,
|
||||||
ROOM_CONTEXT.PUBLIC_SNAPSHOT,
|
ROOM_CONTEXT.PUBLIC_SNAPSHOT,
|
||||||
|
ROOM_CONTEXT.PUBLIC_TOUCHSCREEN_APP,
|
||||||
]
|
]
|
||||||
|
|
||||||
function getEmbeddedState(context: $ROOM_CONTEXT) {
|
function getEmbeddedState(context: $ROOM_CONTEXT) {
|
||||||
|
|
24
apps/dotcom/src/pages/public-touchscreen-app.tsx
Normal file
24
apps/dotcom/src/pages/public-touchscreen-app.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import '../../styles/globals.css'
|
||||||
|
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
|
||||||
|
import { LocalEditor } from '../components/LocalEditor'
|
||||||
|
|
||||||
|
export function Component() {
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadSession() {
|
||||||
|
const session = await (window as any).meet.addon.createAddonSession({
|
||||||
|
cloudProjectNumber: `${process.env.GOOGLE_CLOUD_PROJECT_NUMBER}`,
|
||||||
|
})
|
||||||
|
const mainStageClient = await session.createMainStageClient()
|
||||||
|
await mainStageClient.unloadSidePanel()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSession()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IFrameProtector slug="ts" context={ROOM_CONTEXT.PUBLIC_TOUCHSCREEN_APP}>
|
||||||
|
<LocalEditor />
|
||||||
|
</IFrameProtector>
|
||||||
|
)
|
||||||
|
}
|
28
apps/dotcom/src/pages/public-touchscreen-side-panel.tsx
Normal file
28
apps/dotcom/src/pages/public-touchscreen-side-panel.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import '../../styles/globals.css'
|
||||||
|
|
||||||
|
export function Component() {
|
||||||
|
useEffect(() => {
|
||||||
|
async function createSession() {
|
||||||
|
const session = await (window as any).meet.addon.createAddonSession({
|
||||||
|
cloudProjectNumber: `${process.env.GOOGLE_CLOUD_PROJECT_NUMBER}`,
|
||||||
|
})
|
||||||
|
const sidePanelClient = await session.createSidePanelClient()
|
||||||
|
await sidePanelClient.setCollaborationStartingState({
|
||||||
|
sidePanelUrl: `${window.location.origin}/tc-side`,
|
||||||
|
mainStageUrl: `${window.location.origin}/tc`,
|
||||||
|
additionalData: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createSession()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* eslint-disable @next/next/no-sync-scripts */}
|
||||||
|
<script src="https://www.gstatic.com/meetjs/addons/0.1.0/meet.addons.js"></script>
|
||||||
|
Starting a new session…
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -37,6 +37,8 @@ export const router = createRoutesFromElements(
|
||||||
<Route path="/" lazy={() => import('./pages/root')} />
|
<Route path="/" lazy={() => import('./pages/root')} />
|
||||||
<Route path={`/${ROOM_PREFIX}`} lazy={() => import('./pages/new')} />
|
<Route path={`/${ROOM_PREFIX}`} lazy={() => import('./pages/new')} />
|
||||||
<Route path="/new" lazy={() => import('./pages/new')} />
|
<Route path="/new" lazy={() => import('./pages/new')} />
|
||||||
|
<Route path={`/ts`} lazy={() => import('./pages/public-touchscreen-app')} />
|
||||||
|
<Route path={`/ts-side`} lazy={() => import('./pages/public-touchscreen-side-panel')} />
|
||||||
<Route path={`/${ROOM_PREFIX}/:roomId`} lazy={() => import('./pages/public-multiplayer')} />
|
<Route path={`/${ROOM_PREFIX}/:roomId`} lazy={() => import('./pages/public-multiplayer')} />
|
||||||
<Route path={`/${ROOM_PREFIX}/:boardId/history`} lazy={() => import('./pages/history')} />
|
<Route path={`/${ROOM_PREFIX}/:boardId/history`} lazy={() => import('./pages/history')} />
|
||||||
<Route
|
<Route
|
||||||
|
|
Loading…
Reference in a new issue