diff --git a/package.json b/package.json index d1c89b19..3142ea8e 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,16 @@ }, "dependencies": { "@headlessui/react": "^1.7.16", + "@noble/ciphers": "^0.2.0", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", "@nostr-dev-kit/ndk": "^0.8.1", "@nostr-fetch/adapter-ndk": "^0.11.0", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-tooltip": "^1.0.6", + "@scure/base": "^1.1.1", "@tanstack/react-query": "^4.32.1", "@tanstack/react-query-devtools": "^4.32.1", "@tanstack/react-virtual": "3.0.0-beta.54", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af13d821..cdf55f7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,15 @@ dependencies: '@headlessui/react': specifier: ^1.7.16 version: 1.7.16(react-dom@18.2.0)(react@18.2.0) + '@noble/ciphers': + specifier: ^0.2.0 + version: 0.2.0 + '@noble/curves': + specifier: ^1.1.0 + version: 1.1.0 + '@noble/hashes': + specifier: ^1.3.1 + version: 1.3.1 '@nostr-dev-kit/ndk': specifier: ^0.8.1 version: 0.8.1(typescript@4.9.5) @@ -22,6 +31,9 @@ dependencies: '@radix-ui/react-tooltip': specifier: ^1.0.6 version: 1.0.6(@types/react-dom@18.2.7)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + '@scure/base': + specifier: ^1.1.1 + version: 1.1.1 '@tanstack/react-query': specifier: ^4.32.1 version: 4.32.1(react-dom@18.2.0)(react@18.2.0) @@ -1097,6 +1109,10 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + /@noble/ciphers@0.2.0: + resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==} + dev: false + /@noble/curves@1.1.0: resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} dependencies: diff --git a/public/clapping_hands.png b/public/clapping_hands.png new file mode 100644 index 00000000..593056a4 Binary files /dev/null and b/public/clapping_hands.png differ diff --git a/public/clown_face.png b/public/clown_face.png new file mode 100644 index 00000000..3c650616 Binary files /dev/null and b/public/clown_face.png differ diff --git a/public/crying_face.png b/public/crying_face.png new file mode 100644 index 00000000..47bfc717 Binary files /dev/null and b/public/crying_face.png differ diff --git a/public/face_with_open_mouth.png b/public/face_with_open_mouth.png new file mode 100644 index 00000000..4b6f5c57 Binary files /dev/null and b/public/face_with_open_mouth.png differ diff --git a/public/face_with_tongue.png b/public/face_with_tongue.png new file mode 100644 index 00000000..b6bf2b8e Binary files /dev/null and b/public/face_with_tongue.png differ diff --git a/src/libs/nip44.ts b/src/libs/nip44.ts new file mode 100644 index 00000000..d5716804 --- /dev/null +++ b/src/libs/nip44.ts @@ -0,0 +1,44 @@ +// source: https://github.com/nbd-wtf/nostr-tools/blob/b1fc8ab401b8074f53e6a05a1a6a13422fb01b2d/nip44.ts +import { xchacha20 } from '@noble/ciphers/chacha'; +import { secp256k1 } from '@noble/curves/secp256k1'; +import { sha256 } from '@noble/hashes/sha256'; +import { randomBytes } from '@noble/hashes/utils'; +import { base64 } from '@scure/base'; + +export function getConversationKey(privkeyA: string, pubkeyB: string) { + const key = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB); + return sha256(key.slice(1, 33)); +} + +export function nip44Encrypt( + privkey: string, + pubkey: string, + text: string, + ver = 1 +): string { + if (ver !== 1) throw new Error('NIP44: unknown encryption version'); + + const key = getConversationKey(privkey, pubkey); + const nonce = randomBytes(24); + const plaintext = new TextEncoder().encode(text); + const ciphertext = xchacha20(key, nonce, plaintext, plaintext); + const ctb64 = base64.encode(ciphertext); + const nonceb64 = base64.encode(nonce); + + return JSON.stringify({ ciphertext: ctb64, nonce: nonceb64, v: 1 }); +} + +export function nip44Decrypt(privkey: string, pubkey: string, data: string): string { + const dt = JSON.parse(data); + if (dt.v !== 1) throw new Error('NIP44: unknown encryption version'); + + let { ciphertext, nonce } = dt; + ciphertext = base64.decode(ciphertext); + nonce = base64.decode(nonce); + + const key = getConversationKey(privkey, pubkey); + const plaintext = xchacha20(key, nonce, ciphertext, ciphertext); + const text = new TextDecoder('utf-8').decode(plaintext); + + return text; +} diff --git a/src/shared/icons/horizontalDots.tsx b/src/shared/icons/horizontalDots.tsx new file mode 100644 index 00000000..d7c8adf2 --- /dev/null +++ b/src/shared/icons/horizontalDots.tsx @@ -0,0 +1,28 @@ +import { SVGProps } from 'react'; + +export function HorizontalDotsIcon( + props: JSX.IntrinsicAttributes & SVGProps +) { + return ( + + + + + ); +} diff --git a/src/shared/icons/index.tsx b/src/shared/icons/index.tsx index 7bbdecb4..1ea186cf 100644 --- a/src/shared/icons/index.tsx +++ b/src/shared/icons/index.tsx @@ -47,4 +47,5 @@ export * from './reaction'; export * from './thread'; export * from './strangers'; export * from './download'; +export * from './horizontalDots'; // @endindex diff --git a/src/shared/notes/actions.tsx b/src/shared/notes/actions.tsx index 6b9afe9d..4bb02c4c 100644 --- a/src/shared/notes/actions.tsx +++ b/src/shared/notes/actions.tsx @@ -1,6 +1,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'; import { ThreadIcon } from '@shared/icons'; +import { MoreActions } from '@shared/notes/actions/more'; import { NoteReaction } from '@shared/notes/actions/reaction'; import { NoteReply } from '@shared/notes/actions/reply'; import { NoteRepost } from '@shared/notes/actions/repost'; @@ -62,6 +63,7 @@ export function NoteActions({ )} + ); diff --git a/src/shared/notes/actions/more.tsx b/src/shared/notes/actions/more.tsx new file mode 100644 index 00000000..16de4ecf --- /dev/null +++ b/src/shared/notes/actions/more.tsx @@ -0,0 +1,76 @@ +import * as Popover from '@radix-ui/react-popover'; +import * as Tooltip from '@radix-ui/react-tooltip'; +import { writeText } from '@tauri-apps/plugin-clipboard-manager'; +import { nip19 } from 'nostr-tools'; +import { EventPointer } from 'nostr-tools/lib/nip19'; +import { Link } from 'react-router-dom'; + +import { HorizontalDotsIcon } from '@shared/icons'; + +export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) { + const nevent = nip19.neventEncode(id as unknown as EventPointer); + + const copyID = async () => { + await writeText(nevent); + }; + + const copyLink = async () => { + await writeText('https://nostr.com/' + nevent); + }; + + return ( + + + + + + + + + + More + + + + + + +
+ + + + + View profile + +
+
+
+
+ ); +} diff --git a/src/shared/notes/actions/reaction.tsx b/src/shared/notes/actions/reaction.tsx index d0c56dfe..89897751 100644 --- a/src/shared/notes/actions/reaction.tsx +++ b/src/shared/notes/actions/reaction.tsx @@ -1,5 +1,5 @@ import * as Popover from '@radix-ui/react-popover'; -import { useCallback, useEffect, useState } from 'react'; +import { useState } from 'react'; import { ReactionIcon } from '@shared/icons'; @@ -8,23 +8,23 @@ import { usePublish } from '@utils/hooks/usePublish'; const REACTIONS = [ { content: '👏', - img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Hand%20gestures/Clapping%20Hands.png', + img: '/public/clapping_hands.png', }, { content: '🤪', - img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Smilies/Face%20with%20Tongue.png', + img: '/public/face_with_tongue.png', }, { content: '😮', - img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Smilies/Face%20with%20Open%20Mouth.png', + img: '/public/face_with_open_mouth.png', }, { content: '😢', - img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Smilies/Crying%20Face.png', + img: '/public/crying_face.png', }, { content: '🤡', - img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Smilies/Clown%20Face.png', + img: '/public/clown_face.png', }, ]; @@ -83,7 +83,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) { className="inline-flex h-8 w-8 items-center justify-center rounded hover:bg-white/10" > Clapping Hands @@ -94,7 +94,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) { className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10" > Face with Tongue @@ -105,7 +105,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) { className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10" > Face with Open Mouth @@ -115,22 +115,14 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) { onClick={() => react('😢')} className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10" > - Crying Face + Crying Face