Skip to content

Commit

Permalink
feat: external object props (#359)
Browse files Browse the repository at this point in the history
  • Loading branch information
agazso authored Sep 11, 2023
1 parent 8fcc0bf commit 2850d8b
Show file tree
Hide file tree
Showing 14 changed files with 191 additions and 95 deletions.
1 change: 0 additions & 1 deletion knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const config: KnipConfig = {
},
ignoreExportsUsedInFile: true,
ignoreBinaries: ['docker'],
ignoreDependencies: ['@waku-objects/sandbox-example'],
}

export default config
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"@waku-objects/luminance": "^2.0.1",
"@waku-objects/sandbox-example": "^0.3.0",
"@waku-objects/sandbox-example": "^0.4.0",
"@waku/interfaces": "^0.0.15",
"@waku/sdk": "^0.0.16",
"copy-to-clipboard": "^3.3.3",
Expand Down
16 changes: 8 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/lib/objects/chat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
export let message: DataMessage
export let users: User[]
const { wakuObject, customArgs } = lookup(message.objectId) || {}
const { wakuObject } = lookup(message.objectId) || {}
let store: JSONSerializable | undefined
$: store = $objectStore.objects.get(objectKey(message.objectId, message.instanceId))
Expand Down Expand Up @@ -78,4 +78,4 @@
}
</script>

<svelte:component this={wakuObject} {message} {args} {customArgs} />
<svelte:component this={wakuObject} {message} {args} />
11 changes: 11 additions & 0 deletions src/lib/objects/external/chat.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import type { DataMessage } from '$lib/stores/chat'
import type { WakuObjectArgs } from '..'
import IframeComponent from './iframe.svelte'
// Exports
export let message: DataMessage
export let args: WakuObjectArgs
</script>

<IframeComponent {message} {args} />
7 changes: 7 additions & 0 deletions src/lib/objects/external/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
JSONValue,
WakuObjectAdapter,
WakuObjectArgs,
WakuObjectContextProps,
WakuObjectState,
} from '..'
import type { Token } from '../schemas'
Expand Down Expand Up @@ -41,6 +42,12 @@ export interface IframeDataMessage {
state: WakuObjectState
}

export interface IframeContextChange {
type: 'iframe-context-change'
state: WakuObjectState
context: WakuObjectContextProps
}

interface IframeDispatcher {
onMessage: (
data: JSONSerializable,
Expand Down
89 changes: 75 additions & 14 deletions src/lib/objects/external/iframe.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
// Types
import { getNPMObject, type LoadedObject } from './lib'
import { makeIframeDispatcher } from './dispatch'
import { registerWindow, unregisterWindow } from '.'
import { makeIframeDispatcher, type IframeContextChange } from './dispatch'
import { postWindowMessage, registerWindow, unregisterWindow } from '.'
import { onDestroy } from 'svelte'
import adapters from '$lib/adapters'
import { walletStore } from '$lib/stores/wallet'
Expand All @@ -19,19 +19,17 @@
return template
.replace('__CSP__', object.csp)
.replace('__URL__', object.script)
.replace('__EMBED__', object.embed.message)
.replace('__CLASS__', object.className)
}
// Exports
export let message: DataMessage
export let message: DataMessage | undefined
export let args: WakuObjectArgs
export let customArgs: { name: string }
const { name } = customArgs
// Local variables
let object: LoadedObject | null
let iframe: HTMLIFrameElement
let lastContextHash: number | undefined = undefined
$: wallet = $walletStore.wallet
Expand All @@ -49,10 +47,16 @@
switch (data.type) {
case 'window-size': {
const { scrollWidth, scrollHeight } = data
iframe.style.width = `${scrollWidth}px`
if (isStandalone()) {
iframe.style.width = `${scrollWidth}px`
}
iframe.style.height = `${scrollHeight}px`
return
}
case 'init': {
updateContext(true)
return
}
default: {
const window = iframe.contentWindow
iframeDispatcher.onMessage(data, args, window, adapterWallet)
Expand All @@ -67,29 +71,86 @@
started = true
}
// TODO: Add option to add external objects
$: name && getNPMObject(name, message).then((result) => (object = result))
$: args &&
getNPMObject(args.objectId, message ? 'chat' : 'standalone').then((result) => (object = result))
$: if (iframe && iframe.contentWindow) {
registerWindow(args.instanceId, iframe.contentWindow)
updateContext()
}
onDestroy(() => unregisterWindow(args.instanceId))
function isStandalone() {
return !message
}
function updateContext(force = false) {
const {
chatId,
chatName,
objectId,
instanceId,
profile,
users,
tokens,
view,
viewParams,
store,
} = args
const iframeContextChange: IframeContextChange = {
type: 'iframe-context-change',
state: {
chatId,
chatName,
objectId,
instanceId,
profile,
users,
tokens,
},
context: {
view,
viewParams,
store,
},
}
const json = JSON.stringify(iframeContextChange, (key, value) =>
typeof value === 'bigint' ? value.toString(10) : value,
)
const contextHash = Array.from(json).reduce(
(hash, char) => 0 | (31 * hash + char.charCodeAt(0)),
0,
)
if (force || lastContextHash !== contextHash) {
postWindowMessage(args.instanceId, iframeContextChange)
lastContextHash = contextHash
}
}
</script>

{#if object}
<ChatMessage myMessage={args?.profile.address === message?.fromAddress} bubble noText>
{#if message}
<ChatMessage myMessage={args?.profile.address === message?.fromAddress} bubble noText>
<iframe
title={object.name}
bind:this={iframe}
sandbox="allow-scripts"
srcdoc={getIframeSource(object)}
/>
</ChatMessage>
{:else}
<iframe
title={object.name}
bind:this={iframe}
sandbox="allow-scripts"
srcdoc={getIframeSource(object)}
/>
</ChatMessage>
{/if}
{/if}

<style>
iframe {
all: unset;
overflow: hidden;
/* overflow: hidden; */
}
</style>
22 changes: 13 additions & 9 deletions src/lib/objects/external/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { WakuObjectSvelteDescriptor } from '..'
import type { IframeDataMessage } from './dispatch'
import IframeComponent from './iframe.svelte'
import ChatComponent from './chat.svelte'
import StandaloneComponent from './standalone.svelte'

const instanceWindowMap = new Map<string, Window>()

Expand All @@ -9,25 +10,21 @@ export const getExternalDescriptor = (
name: string,
description: string,
logo: string,
hasStandalone?: boolean,
): WakuObjectSvelteDescriptor => ({
objectId,
name,
description,
logo,
wakuObject: IframeComponent,
customArgs: { name: objectId },
wakuObject: ChatComponent,
standalone: hasStandalone ? StandaloneComponent : undefined,
onMessage: async (message, args) => {
const window = instanceWindowMap.get(message.instanceId)
if (!window) {
return
}

const iframeDataMessage: IframeDataMessage = {
type: 'iframe-data-message',
message,
state: args,
}
window.postMessage(iframeDataMessage, { targetOrigin: '*' })
postWindowMessage(message.instanceId, iframeDataMessage)
},
})

Expand All @@ -42,3 +39,10 @@ export function registerWindow(instanceId: string, window: Window) {
export function unregisterWindow(instanceId: string) {
instanceWindowMap.delete(instanceId)
}

export function postWindowMessage(instanceId: string, message: unknown) {
const window = instanceWindowMap.get(instanceId)
if (window) {
window.postMessage(message, { targetOrigin: '*' })
}
}
40 changes: 4 additions & 36 deletions src/lib/objects/external/lib.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { DataMessage } from '$lib/stores/chat'

// Not sure how inefficient this is
const scripts = import.meta.glob('/node_modules/**/object/index.js', {
as: 'url',
Expand All @@ -18,19 +16,15 @@ export type Csp = {

export type WakuObject = {
name: string
standalone?: boolean
csp: Csp
}

export type Embed = {
message: string
sha256: string
}

export type LoadedObject = {
script: string
csp: string
name: string
embed: Embed
className: string
}

const DEFAULT_CSP: Csp = {
Expand All @@ -47,50 +41,24 @@ const formatCsp = (csp: Csp, add?: Csp): string => {
.join('; ')
}

const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
let binary = ''
const bytes = new Uint8Array(buffer)
const len = bytes.byteLength

for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i])
}

return window.btoa(binary)
}

const getEmbed = async (dataMessage: DataMessage): Promise<Embed> => {
const message = `
window.wakuObject = {}
window.wakuObject.message = JSON.parse('${JSON.stringify(dataMessage)}')
`

const utf8 = new TextEncoder().encode(message)
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8)

return { message, sha256: arrayBufferToBase64(hashBuffer) }
}

export const getNPMObject = async (
module: string,
message: DataMessage,
className: 'chat' | 'standalone',
): Promise<LoadedObject | null> => {
try {
const object = (await objects[`/node_modules/${module}/object/metadata.json`]()) as WakuObject
const script = await scripts[`/node_modules/${module}/object/index.js`]()
const embed = await getEmbed(message)

const added = { ...DEFAULT_CSP } as Csp
if (!added['script-src']) {
added['script-src'] = ''
}
added['script-src'] += ` 'sha256-${embed.sha256}'`

return {
script,
csp: formatCsp(object.csp, added),
name: object.name,
embed: embed,
className,
}
} catch (err) {
console.error(err)
Expand Down
Loading

1 comment on commit 2850d8b

@vercel
Copy link

@vercel vercel bot commented on 2850d8b Sep 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.