Skip to content

Commit

Permalink
wip: install invite flow
Browse files Browse the repository at this point in the history
  • Loading branch information
agazso committed Nov 21, 2023
1 parent d1cb1e6 commit a39efe1
Show file tree
Hide file tree
Showing 18 changed files with 649 additions and 55 deletions.
1 change: 1 addition & 0 deletions src/lib/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface Adapter {
data: JSONSerializable,
): Promise<void>
sendGroupChatInvite(wallet: BaseWallet, chatId: string, users: string[]): Promise<void>
sendInstall(chatId: string, objectId: string, command: 'invite' | 'accept'): Promise<void>

updateStore(
address: string,
Expand Down
5 changes: 5 additions & 0 deletions src/lib/adapters/waku/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export function hash(data: Uint8Array | Hex): Hex {
return keccak256(bytes)
}

export function hashString(s: string): Hex {
const data = new TextEncoder().encode(s)
return hash(data)
}

export function encrypt(
plaintext: Uint8Array,
key: Uint8Array,
Expand Down
115 changes: 113 additions & 2 deletions src/lib/adapters/waku/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
type WithoutMeta,
type ChatMessage,
type BabbleMessage,
type WithMeta,
type InstallMessage,
} from '$lib/stores/chat'
import type { User } from '$lib/types'
import type { TimeFilter } from '@waku/interfaces'
Expand All @@ -38,6 +40,7 @@ import { makeWakustore, type Wakustore } from './wakustore'
import type {
StorageChat,
StorageChatEntry,
StorageInstalledObject,
StorageInstalledObjectEntry,
StorageObjectEntry,
StorageProfile,
Expand All @@ -62,6 +65,7 @@ import {
} from './codec'
import type { DecodedMessage } from '@waku/message-encryption'
import { utils } from '@noble/secp256k1'
import { getObjectSpec } from '$lib/objects/external/lib'

const MAX_MESSAGES = 100

Expand Down Expand Up @@ -148,6 +152,10 @@ async function addMessageToChat(
await executeOnDataMessage(ownPublicKey, blockchainAdapter, chatId, message, send)
}

if (message.type === 'install') {
await executeOnInstallMessage(ownPublicKey, chatId, message)
}

const unread = message.type === 'user' && message.senderPublicKey !== ownPublicKey ? 1 : 0
chats.updateChat(chatId, (chat) => ({
...chat,
Expand All @@ -160,7 +168,7 @@ async function executeOnDataMessage(
publicKey: string,
blockchainAdapter: WakuObjectAdapter,
chatId: string,
dataMessage: DataMessage,
dataMessage: WithMeta<DataMessage>,
send: (data: JSONValue) => Promise<void>,
) {
const descriptor = lookup(dataMessage.objectId)
Expand Down Expand Up @@ -218,6 +226,85 @@ async function executeOnDataMessage(
}
}

async function executeOnInstallMessage(
publicKey: string,
chatId: string,
message: WithMeta<InstallMessage>,
) {
if (message.senderPublicKey === publicKey) {
if (message.command === 'accept') {
const installedObjects = get(installedObjectStore).objects
if (!installedObjects.has(message.objectId)) {
return
}

installedObjectStore.updateInstalledObject(message.objectId, (object) => ({
...object,
installed: true,
}))

// add to chat objects
chats.updateChat(chatId, (chat) => {
if (!chat.objects) {
chat.objects = []
}
if (!chat.objects.includes(message.objectId)) {
chat.objects.push(message.objectId)
}
return chat
})
}

return
}

if (message.command === 'invite') {
// add to installed objects
const installedObjects = get(installedObjectStore).objects
if (installedObjects.has(message.objectId)) {
return
}

// TODO fix WakuScriptType
const objectSpec = await getObjectSpec(message.objectId, 'chat')
if (!objectSpec) {
return
}

const installedObject: StorageInstalledObject = {
objectId: message.objectId,
name: objectSpec.object.name,
description: objectSpec.object.description,
// TODO fix relative path to absolute
logo: objectSpec.object.files.logo.path,
installed: false,
}

installedObjectStore.addInstalledObject(installedObject)
} else if (message.command === 'accept') {
const installedObjects = get(installedObjectStore).objects
if (!installedObjects.has(message.objectId)) {
return
}

installedObjectStore.updateInstalledObject(message.objectId, (object) => ({
...object,
installed: true,
}))

// add to chat objects
chats.updateChat(chatId, (chat) => {
if (!chat.objects) {
chat.objects = []
}
if (!chat.objects.includes(message.objectId)) {
chat.objects.push(message.objectId)
}
return chat
})
}
}

function decryptHexToString(h: string, symKey: Uint8Array): string {
const encrypted = hexToBytes(h)
const decrypted = decrypt(encrypted, symKey)
Expand Down Expand Up @@ -677,6 +764,23 @@ export default class WakuAdapter implements Adapter {
}
}

async sendInstall(chatId: string, objectId: string, command: 'invite' | 'accept'): Promise<void> {
const wallet = get(walletStore).wallet
if (!wallet) {
return
}

const senderPrivateKey = wallet.privateKey
const message: WithoutMeta<InstallMessage> = {
type: 'install',
objectId,
command,
}
const encryptionKey = hexToBytes(chatId)

await this.safeWaku.sendMessage(message, encryptionKey, hexToBytes(senderPrivateKey))
}

async updateStore(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_address: string,
Expand Down Expand Up @@ -928,7 +1032,14 @@ export default class WakuAdapter implements Adapter {
adapter: WakuObjectAdapter,
) {
// only handle certain types of messages
if (!(message.type === 'invite' || message.type === 'data' || message.type === 'user')) {
if (
!(
message.type === 'invite' ||
message.type === 'data' ||
message.type === 'user' ||
message.type === 'install'
)
) {
return
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/adapters/waku/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ export interface StorageInstalledObject {
name: string
description: string
logo: string
installed: boolean
}
export type StorageInstalledObjectEntry = [objectId: string, object: StorageInstalledObject]
94 changes: 94 additions & 0 deletions src/lib/components/chat-object-invite.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<script lang="ts">
import adapters from '$lib/adapters'
import type { InstallMessage, WithMeta } from '$lib/stores/chat'
import { installedObjectStore } from '$lib/stores/installed-objects'
import type { User } from '$lib/types'
import Button from './button.svelte'
import ChatMessage from './chat-message.svelte'
import Container from './container.svelte'
import CheckmarkFilled from './icons/checkmark-filled.svelte'
import Loading from './loading.svelte'
import ObjectHeader from './object-header.svelte'
//am I the sender of this message?
export let myMessage = false
export let message: WithMeta<InstallMessage>
export let users: User[]
export let objects: string[] | undefined
export let chatId: string
//is this message in a group chat?
export let group = false
//is the sender of the current message the same as the previous message?
export let sameSender = false
export let timestamp: string | undefined = undefined
let senderName =
users.find((user) => user.publicKey === message.senderPublicKey)?.name ?? '<unknown>'
let recipientName = myMessage
? 'you'
: users.length === 2
? users.find((user) => user.publicKey !== message.senderPublicKey)?.name ?? '<unknown>'
: `chat members`
$: object = $installedObjectStore.objects.get(message.objectId)
$: isInstalledInChat = object && objects && objects.includes(object.objectId)
async function acceptInstall(objectId: string) {
await adapters.sendInstall(chatId, objectId, 'accept')
}
</script>

<ChatMessage bubble={true} {group} {sameSender} {timestamp} {myMessage}>
{#if !object}
<Loading />
{:else}
<div class="wo text-normal">
<Container>
{#if message.command === 'invite'}
{senderName} invited {recipientName} to use "{object.name}" in this chat.
{:else if message.command === 'accept'}
{senderName} accepted the invite. You can now use "{object.name}" in this chat.
{/if}
</Container>
<Container gap={12}>
<ObjectHeader name={object.name} logoImg={object.logo} logoAlt={`${object.name} logo`} />
</Container>
<Container padY={0}>
{#if message.command === 'invite'}
{#if myMessage}
<p class="install-status">
{#if isInstalledInChat}
<CheckmarkFilled />Invite accepted
{:else}
Invite pending...
{/if}
</p>
{:else}
<Button variant="strong" on:click={() => acceptInstall(message.objectId)}>Accept</Button
>
{/if}
{:else if message.command === 'accept'}
<p class="install-status">
{#if isInstalledInChat}
<CheckmarkFilled />Invite accepted
{:else}
Invite pending...
{/if}
</p>
{/if}
</Container>
</div>
{/if}
</ChatMessage>

<style lang="scss">
.install-status {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-6);
}
</style>
36 changes: 36 additions & 0 deletions src/lib/components/icons/data-blob.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
export let size = 20
</script>

<svg id="icon" xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
fill: none;
}
</style>
</defs>
<path
d="M28,30H4a2.0021,2.0021,0,0,1-2-2V4A2.0021,2.0021,0,0,1,4,2H28a2.0021,2.0021,0,0,1,2,2V28A2.0021,2.0021,0,0,1,28,30ZM4,4V28H28V4Z"
transform="translate(0 0)"
/>
<rect x="13" y="7" width="2" height="7" />
<rect x="8" y="7" width="2" height="7" />
<path
d="M22,14H20a2.0021,2.0021,0,0,1-2-2V9a2.0021,2.0021,0,0,1,2-2h2a2.0021,2.0021,0,0,1,2,2v3A2.0021,2.0021,0,0,1,22,14ZM20,9v3h2V9Z"
transform="translate(0 0)"
/>
<rect x="22" y="18" width="2" height="7" />
<rect x="8" y="18" width="2" height="7" />
<path
d="M17,25H15a2.0021,2.0021,0,0,1-2-2V20a2.0021,2.0021,0,0,1,2-2h2a2.0021,2.0021,0,0,1,2,2v3A2.0021,2.0021,0,0,1,17,25Zm-2-5v3h2V20Z"
transform="translate(0 0)"
/>
<rect
id="_Transparent_Rectangle_"
data-name="&lt;Transparent Rectangle&gt;"
class="cls-1"
width="32"
height="32"
/>
</svg>
File renamed without changes.
30 changes: 22 additions & 8 deletions src/lib/objects/lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { splitDescriptor } from './split'
import { installedObjectStore } from '$lib/stores/installed-objects'
import { get } from 'svelte/store'

export type InstalledObjectDescriptor = WakuObjectSvelteDescriptor & {
preInstalled: boolean
installed: boolean
}

const preInstalledObjectList: WakuObjectSvelteDescriptor[] = [
helloWorldDescriptor,
payggyDescriptor,
Expand All @@ -20,16 +25,25 @@ const preInstalledObjectList: WakuObjectSvelteDescriptor[] = [
),
]

export function lookup(objectId: string): WakuObjectSvelteDescriptor | undefined {
export function lookup(objectId: string): InstalledObjectDescriptor | undefined {
const installedObjectList = getInstalledObjectList()
return installedObjectList.find((object) => object.objectId === objectId)
}

export function getInstalledObjectList() {
const installedObjectList = Array.from(get(installedObjectStore).objects)
.map((item) => item[1])
.map((object) =>
getExternalDescriptor(object.objectId, object.name, object.description, object.logo),
)
return preInstalledObjectList.concat(installedObjectList)
export function getInstalledObjectList(): InstalledObjectDescriptor[] {
const installedObjectList = Array.from(get(installedObjectStore).objects).map((item) => {
const object = item[1]
return {
...getExternalDescriptor(object.objectId, object.name, object.description, object.logo),
preInstalled: false,
installed: object.installed,
}
})
return preInstalledObjectList
.map((object) => ({
...object,
preInstalled: true,
installed: true,
}))
.concat(installedObjectList)
}
2 changes: 2 additions & 0 deletions src/lib/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export default {
BABBLES_CHAT: (id: string, threadId?: string) =>
`/babbles/chat/${id}${threadId ? `/${threadId}` : ''}`,
BABBLES_EDIT: (id: string) => `/babbles/chat/${id}/edit`,
SETTINGS_OBJECTS: '/objects',
SETTINGS_OBJECT: (objectId: string) => `/objects/${objectId}`,
}
Loading

0 comments on commit a39efe1

Please sign in to comment.