mirror of
https://github.com/aljazceru/sendstr-web.git
synced 2025-12-17 06:24:24 +01:00
Merge upstream main latest changes.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Access the web app: [https://sendstr.com](https://sendstr.com)
|
||||
|
||||
Sendstr is an e2e encrypted shared clipboard web app powered by Nostr.
|
||||
Sendstr is an e2e encrypted bi-directional clipboard web app powered by Nostr.
|
||||
|
||||
The main motivation to build Sendstr was to provide a quick and easy way to transfer text and files (coming soon) between devices. Sendstr defaults to a self-hosted Nostr relay but can easily be configured to point elsewhere.
|
||||
|
||||
|
||||
38
global.d.ts
vendored
38
global.d.ts
vendored
@@ -3,44 +3,6 @@ declare module "remark-html" {
|
||||
export default html
|
||||
}
|
||||
|
||||
declare module "nostr-tools" {
|
||||
export const generatePrivateKey: () => string
|
||||
export const getPublicKey: (priv: string) => string
|
||||
export const relayPool: () => {
|
||||
setPrivateKey: (priv: string) => void
|
||||
addRelay: (url: string, { read: boolean, write: boolean }) => void
|
||||
publish: ({pubkey, created_at, kind, tags, content}: {
|
||||
pubkey: string,
|
||||
created_at: number,
|
||||
kind: number,
|
||||
tags: [[string,string]],
|
||||
content: string,
|
||||
}) => Promise<void>,
|
||||
sub: ({
|
||||
cb,
|
||||
filter,
|
||||
}: {
|
||||
cb: (event: {
|
||||
content: string
|
||||
created_at: number
|
||||
id: string
|
||||
kind: number
|
||||
pubkey: string
|
||||
message: string
|
||||
sig: string
|
||||
tags: [[string, string]]
|
||||
}) => void
|
||||
filter: Record<string, string[]>[]
|
||||
}) => {
|
||||
unsub: () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module "nostr-tools/nip04" {
|
||||
export const decrypt: (priv: string, pub: string, message: string) => string
|
||||
export const encrypt: (priv: string, pub: string, message: string) => string
|
||||
}
|
||||
declare module "toastify-js" {
|
||||
const Toastify: ({
|
||||
text,
|
||||
|
||||
948
package-lock.json
generated
948
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
||||
"next": "12.1.6",
|
||||
"next-pwa": "^5.5.4",
|
||||
"next-themes": "^0.2.1",
|
||||
"nostr-tools": "^0.23.3",
|
||||
"nostr-tools": "^1.0.1",
|
||||
"qrcode.react": "^3.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-device-detect": "^2.2.2",
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3C3744",
|
||||
"description": "Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load."
|
||||
"description": "Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load."
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getRelays } from "./localStorage"
|
||||
import { NostrEventType, NostrKeysType, NostrPoolType } from "../types"
|
||||
import { Relay, Sub } from "nostr-tools"
|
||||
|
||||
export const getLatestEvent = (events: Record<string, NostrEventType>) =>
|
||||
Object.entries(events).reduce((acc, x) => {
|
||||
@@ -20,30 +21,31 @@ export const subscribe = async (
|
||||
peerKey: string,
|
||||
cb: (event: NostrEventType) => void,
|
||||
) => {
|
||||
const { decrypt } = await import("nostr-tools/nip04")
|
||||
const { relayPool } = await import("nostr-tools")
|
||||
const pool = relayPool()
|
||||
pool.setPrivateKey(keys.priv)
|
||||
const { nip04, relayInit } = await import("nostr-tools")
|
||||
const relays = getRelays()
|
||||
relays.forEach((relay) => relay.enabled && pool.addRelay(relay.url, { read: true, write: true }))
|
||||
const sub = pool.sub({
|
||||
cb: (event: NostrEventType) => {
|
||||
try {
|
||||
.filter((x) => x.enabled)
|
||||
.map((x) => relayInit(x.url))
|
||||
await Promise.allSettled(relays.map((x) => x.connect()))
|
||||
relays.forEach((relay) =>
|
||||
relay.on("error", () => console.error(`Failed to connect to relay ${relay.url}`)),
|
||||
)
|
||||
const subs = relays.map((relay) =>
|
||||
relay.sub([{ "#p": [keys.pub, peerKey] }, { authors: [keys.pub, peerKey] }]),
|
||||
)
|
||||
subs.forEach((x) =>
|
||||
x.on("event", async (event: NostrEventType) => {
|
||||
const p = event.tags.find(([tag]) => tag === "p") || ["p", ""]
|
||||
const pubkey = event.pubkey === keys.pub ? p[1] : event.pubkey
|
||||
const message = decrypt(keys.priv, pubkey, event.content)
|
||||
const message = await nip04.decrypt(keys.priv, pubkey, event.content)
|
||||
cb({ ...event, message })
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
filter: [{ "#p": [keys.pub, peerKey] }, { authors: [keys.pub, peerKey] }],
|
||||
})
|
||||
return { sub, pool }
|
||||
}),
|
||||
)
|
||||
return { subs, relays }
|
||||
}
|
||||
|
||||
type SendEncryptedMessage = {
|
||||
pool: NostrPoolType | null
|
||||
relays: Relay[]
|
||||
subs: Sub[]
|
||||
priv: string
|
||||
pub: string
|
||||
peerKey: string
|
||||
@@ -51,18 +53,28 @@ type SendEncryptedMessage = {
|
||||
}
|
||||
|
||||
export const sendEncryptedMessage = async ({
|
||||
pool,
|
||||
relays,
|
||||
priv,
|
||||
pub,
|
||||
peerKey,
|
||||
message,
|
||||
}: SendEncryptedMessage) => {
|
||||
const { encrypt } = await import("nostr-tools/nip04")
|
||||
return pool?.publish({
|
||||
const { nip04, getEventHash, signEvent } = await import("nostr-tools")
|
||||
relays.map((relay) => {
|
||||
nip04
|
||||
.encrypt(priv, peerKey, message)
|
||||
.then((content) => {
|
||||
const event = {
|
||||
pubkey: pub,
|
||||
created_at: Math.round(Date.now() / 1000),
|
||||
kind: 4,
|
||||
tags: [["p", peerKey]],
|
||||
content: encrypt(priv, peerKey, message),
|
||||
content,
|
||||
}
|
||||
const id = getEventHash(event)
|
||||
const sig = signEvent(event, priv)
|
||||
relay.publish({ ...event, id, sig })
|
||||
})
|
||||
.catch((e) => console.error(`Failed to send message to relay ${relay.url}`, e))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import Head from "next/head"
|
||||
|
||||
import { Header } from "../components/header"
|
||||
import { Button } from "../components/button"
|
||||
import { Card } from "../components/card"
|
||||
import { SendView } from "../views/send"
|
||||
import { ReceiveView } from "../views/receive"
|
||||
import { NostrKeysType } from "../types"
|
||||
@@ -61,16 +60,16 @@ export default function Home({ keys }: HomeProps) {
|
||||
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Sendstr is an open source end-to-end encrypted shared clipboard app
|
||||
content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app
|
||||
built on top of Nostr. No login needed, new throwaway encryption keys are generated on
|
||||
page load, and the default relay deletes messages after 1 hour."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://sendstr.com/" />
|
||||
<meta property="og:title" content="Senstr" />
|
||||
<meta property="og:title" content="Sendstr" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
|
||||
content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
|
||||
/>
|
||||
<meta property="og:image" content="/favicon-16x16.png" />
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
@@ -78,7 +77,7 @@ export default function Home({ keys }: HomeProps) {
|
||||
<meta property="twitter:title" content="Sendstr" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
|
||||
content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
|
||||
/>
|
||||
<meta property="twitter:image" content="/favicon-16x16.png" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
@@ -32,16 +32,16 @@ export default function Settings() {
|
||||
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Sendstr is an open source end-to-end encrypted shared clipboard app
|
||||
content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app
|
||||
built on top of Nostr. No login needed, new throwaway encryption keys are generated on
|
||||
page load, and the default relay deletes messages after 1 hour."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://sendstr.com/settings" />
|
||||
<meta property="og:title" content="Senstr - Settings" />
|
||||
<meta property="og:title" content="Sendstr - Settings" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
|
||||
content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
|
||||
/>
|
||||
<meta property="og:image" content="/favicon-16x16.png" />
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
@@ -49,7 +49,7 @@ export default function Settings() {
|
||||
<meta property="twitter:title" content="Sendstr - Settings" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
|
||||
content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
|
||||
/>
|
||||
<meta property="twitter:image" content="/favicon-16x16.png" />
|
||||
</Head>
|
||||
|
||||
10
src/types.ts
10
src/types.ts
@@ -1,3 +1,5 @@
|
||||
import { Relay, Sub } from "nostr-tools"
|
||||
|
||||
export type NostrSubType = ({
|
||||
cb,
|
||||
filter,
|
||||
@@ -34,17 +36,15 @@ export type NostrPublishType = ({
|
||||
export type NostrPoolType = {
|
||||
setPrivateKey: (priv: string) => void
|
||||
addRelay: (url: string, { read, write }: { read: boolean; write: boolean }) => void
|
||||
sub: NostrSubType
|
||||
subs: NostrSubType
|
||||
publish: NostrPublishType
|
||||
}
|
||||
|
||||
export type NostrType = {
|
||||
priv: string
|
||||
pub: string
|
||||
pool: NostrPoolType | null
|
||||
sub: {
|
||||
unsub: () => void
|
||||
} | null
|
||||
subs: Sub[],
|
||||
relays: Relay[]
|
||||
}
|
||||
|
||||
export type NostrEventType = {
|
||||
|
||||
@@ -45,10 +45,10 @@ export const ReceiveView = ({ keys }: ReceiveViewProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
const sub = await subscribe(keys, peerKey, processEvent)
|
||||
nostr.current = { ...sub, ...keys }
|
||||
const { subs, relays } = await subscribe(keys, peerKey, processEvent)
|
||||
nostr.current = { subs, relays, ...keys }
|
||||
return () => {
|
||||
nostr?.current?.sub?.unsub()
|
||||
nostr?.current?.subs.forEach(sub => sub.unsub())
|
||||
}
|
||||
})()
|
||||
}, [peerKey])
|
||||
@@ -56,7 +56,7 @@ export const ReceiveView = ({ keys }: ReceiveViewProps) => {
|
||||
const sendMessage = useRef(
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
debounce(async (peerKey: string, message: string) => {
|
||||
if (nostr?.current?.pool) {
|
||||
if (nostr?.current?.relays) {
|
||||
await sendEncryptedMessage({ ...nostr.current, peerKey, message })
|
||||
}
|
||||
}, 500),
|
||||
|
||||
@@ -43,7 +43,7 @@ const PeerInput = ({ peerKey, onChange, setShowScan }: PeerInputProps) => {
|
||||
<section className="mx-auto max-w-[40rem] p-4">
|
||||
<label>Peer pubkey:</label>
|
||||
<div className="relative">
|
||||
<Input value={peerKey} onChange={(e) => onChange(e?.currentTarget?.value || "")} />
|
||||
<Input value={peerKey} onChange={(e) => onChange(e?.currentTarget?.value.trim() || "")} />
|
||||
<div className="absolute right-0 top-0 h-full flex items-center">
|
||||
<IconButton className="w-10 h-10 mr-2" onClick={() => setShowScan(true)}>
|
||||
<div className="m-2">
|
||||
@@ -79,10 +79,10 @@ export const SendView = ({ keys }: SendViewProps) => {
|
||||
if (isMobile) {
|
||||
setShowScan(true)
|
||||
}
|
||||
const sub = await subscribe(keys, peerKey, processEvent)
|
||||
nostr.current = { ...sub, ...keys }
|
||||
const { subs, relays } = await subscribe(keys, peerKey, processEvent)
|
||||
nostr.current = { subs, relays, ...keys }
|
||||
return () => {
|
||||
nostr?.current?.sub?.unsub()
|
||||
nostr?.current?.subs.forEach(sub => sub.unsub())
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
@@ -99,7 +99,7 @@ export const SendView = ({ keys }: SendViewProps) => {
|
||||
const sendMessage = useRef(
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
debounce(async (peerKey: string, message: string) => {
|
||||
if (nostr.current?.pool) {
|
||||
if (nostr.current?.relays) {
|
||||
await sendEncryptedMessage({ ...nostr.current, peerKey, message })
|
||||
}
|
||||
}, 500),
|
||||
|
||||
Reference in New Issue
Block a user