Skip to content

Commit

Permalink
feat: install invite flow (#497)
Browse files Browse the repository at this point in the history
  • Loading branch information
agazso authored Nov 24, 2023
1 parent d1cb1e6 commit c935f04
Show file tree
Hide file tree
Showing 22 changed files with 760 additions and 64 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
113 changes: 111 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,83 @@ 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,
logo: objectSpec.object.files.logo.path,
installed: false,
}

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

if (!installedObject.installed) {
return
}

// 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 +762,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 +1030,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]
7 changes: 6 additions & 1 deletion src/lib/components/chat-message.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,14 @@
max-width: 75%;
margin-right: auto;
margin-left: 0;
font-family: (--font-serif);
&:not(:last-child) {
margin-bottom: var(--spacing-12);
}
&.object {
font-family: sans-serif;
.message-content {
width: 100%;
}
Expand All @@ -93,7 +95,6 @@
padding: var(--spacing-12);
border-radius: var(--border-radius);
display: inline-block;
font-family: var(--font-serif);
background-color: var(--color-base, var(--color-dark-step-40));
}
Expand All @@ -118,6 +119,10 @@
}
}
&.object {
font-style: normal;
}
.timestamp {
text-align: end;
}
Expand Down
117 changes: 117 additions & 0 deletions src/lib/components/chat-object-invite.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<script lang="ts">
import adapters from '$lib/adapters'
import routes from '$lib/routes'
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 ObjectInstallInfo from './object-install-info.svelte'
import { goto } from '$app/navigation'
import { hashString } from '$lib/adapters/waku/crypto'
import Checkmark from './icons/checkmark.svelte'
import Pending from './icons/pending.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 senderName: string | undefined = undefined
export let timestamp: string | undefined = undefined
let senderNameLabel = myMessage
? 'You'
: users.find((user) => user.publicKey === message.senderPublicKey)?.name ?? '<unknown>'
let recipientName = !group
? myMessage
? users.find((user) => user.publicKey !== message.senderPublicKey)?.name ?? '<unknown>'
: 'you'
: `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} object={true} {senderName}>
{#if !object}
<Loading />
{:else}
<div class="wo text-normal">
<Container>
{#if message.command === 'invite'}
{senderNameLabel} invited {recipientName} to use "{object.name}" in this chat.
{:else if message.command === 'accept'}
{senderNameLabel} accepted the invite. You can now use "{object.name}" in this chat.
{/if}
</Container>
<Container gap={12}>
<ObjectInstallInfo
onClick={() => object && goto(routes.SETTINGS_OBJECT(hashString(object.objectId)))}
name={object.name}
logoImg={object.logo}
logoAlt={`${object.name} logo`}
/>
</Container>
<Container padY={0}>
{#if message.command === 'invite'}
{#if isInstalledInChat}
<p class="install-status">
<CheckmarkFilled />Invite accepted
</p>
{:else if myMessage}
<p class="install-status">
<Pending />Invite pending
</p>
{:else}
<Button variant="strong" on:click={() => acceptInstall(message.objectId)}
><Checkmark /> Accept</Button
>
{/if}
{:else if message.command === 'accept'}
<p class="install-status">
{#if isInstalledInChat}
<CheckmarkFilled />Invite accepted
{:else}
<Pending />Invite pending
{/if}
</p>
{/if}
</Container>
</div>
{/if}
<svelte:fragment slot="avatar">
{#if $$slots.avatar}
<slot name="avatar" />
{/if}
</svelte:fragment>
</ChatMessage>

<style lang="scss">
.install-status {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: var(--spacing-6);
background-color: var(--color-step-10, var(--color-dark-step-50));
border-radius: var(--spacing-24);
padding: 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>
Loading

1 comment on commit c935f04

@vercel
Copy link

@vercel vercel bot commented on c935f04 Nov 24, 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.