Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: install invite flow #497

Merged
merged 5 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
) {
console.debug({ publicKey, message })
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove

Suggested change
console.debug({ publicKey, message })


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 +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]
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