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)
|
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.
|
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
|
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" {
|
declare module "toastify-js" {
|
||||||
const Toastify: ({
|
const Toastify: ({
|
||||||
text,
|
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": "12.1.6",
|
||||||
"next-pwa": "^5.5.4",
|
"next-pwa": "^5.5.4",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"nostr-tools": "^0.23.3",
|
"nostr-tools": "^1.0.1",
|
||||||
"qrcode.react": "^3.0.2",
|
"qrcode.react": "^3.0.2",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-device-detect": "^2.2.2",
|
"react-device-detect": "^2.2.2",
|
||||||
|
|||||||
@@ -18,5 +18,5 @@
|
|||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"theme_color": "#3C3744",
|
"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 { getRelays } from "./localStorage"
|
||||||
import { NostrEventType, NostrKeysType, NostrPoolType } from "../types"
|
import { NostrEventType, NostrKeysType, NostrPoolType } from "../types"
|
||||||
|
import { Relay, Sub } from "nostr-tools"
|
||||||
|
|
||||||
export const getLatestEvent = (events: Record<string, NostrEventType>) =>
|
export const getLatestEvent = (events: Record<string, NostrEventType>) =>
|
||||||
Object.entries(events).reduce((acc, x) => {
|
Object.entries(events).reduce((acc, x) => {
|
||||||
@@ -20,30 +21,31 @@ export const subscribe = async (
|
|||||||
peerKey: string,
|
peerKey: string,
|
||||||
cb: (event: NostrEventType) => void,
|
cb: (event: NostrEventType) => void,
|
||||||
) => {
|
) => {
|
||||||
const { decrypt } = await import("nostr-tools/nip04")
|
const { nip04, relayInit } = await import("nostr-tools")
|
||||||
const { relayPool } = await import("nostr-tools")
|
|
||||||
const pool = relayPool()
|
|
||||||
pool.setPrivateKey(keys.priv)
|
|
||||||
const relays = getRelays()
|
const relays = getRelays()
|
||||||
relays.forEach((relay) => relay.enabled && pool.addRelay(relay.url, { read: true, write: true }))
|
.filter((x) => x.enabled)
|
||||||
const sub = pool.sub({
|
.map((x) => relayInit(x.url))
|
||||||
cb: (event: NostrEventType) => {
|
await Promise.allSettled(relays.map((x) => x.connect()))
|
||||||
try {
|
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 p = event.tags.find(([tag]) => tag === "p") || ["p", ""]
|
||||||
const pubkey = event.pubkey === keys.pub ? p[1] : event.pubkey
|
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 })
|
cb({ ...event, message })
|
||||||
} catch (e) {
|
}),
|
||||||
console.warn(e)
|
)
|
||||||
}
|
return { subs, relays }
|
||||||
},
|
|
||||||
filter: [{ "#p": [keys.pub, peerKey] }, { authors: [keys.pub, peerKey] }],
|
|
||||||
})
|
|
||||||
return { sub, pool }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SendEncryptedMessage = {
|
type SendEncryptedMessage = {
|
||||||
pool: NostrPoolType | null
|
relays: Relay[]
|
||||||
|
subs: Sub[]
|
||||||
priv: string
|
priv: string
|
||||||
pub: string
|
pub: string
|
||||||
peerKey: string
|
peerKey: string
|
||||||
@@ -51,18 +53,28 @@ type SendEncryptedMessage = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const sendEncryptedMessage = async ({
|
export const sendEncryptedMessage = async ({
|
||||||
pool,
|
relays,
|
||||||
priv,
|
priv,
|
||||||
pub,
|
pub,
|
||||||
peerKey,
|
peerKey,
|
||||||
message,
|
message,
|
||||||
}: SendEncryptedMessage) => {
|
}: SendEncryptedMessage) => {
|
||||||
const { encrypt } = await import("nostr-tools/nip04")
|
const { nip04, getEventHash, signEvent } = await import("nostr-tools")
|
||||||
return pool?.publish({
|
relays.map((relay) => {
|
||||||
|
nip04
|
||||||
|
.encrypt(priv, peerKey, message)
|
||||||
|
.then((content) => {
|
||||||
|
const event = {
|
||||||
pubkey: pub,
|
pubkey: pub,
|
||||||
created_at: Math.round(Date.now() / 1000),
|
created_at: Math.round(Date.now() / 1000),
|
||||||
kind: 4,
|
kind: 4,
|
||||||
tags: [["p", peerKey]],
|
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 { Header } from "../components/header"
|
||||||
import { Button } from "../components/button"
|
import { Button } from "../components/button"
|
||||||
import { Card } from "../components/card"
|
|
||||||
import { SendView } from "../views/send"
|
import { SendView } from "../views/send"
|
||||||
import { ReceiveView } from "../views/receive"
|
import { ReceiveView } from "../views/receive"
|
||||||
import { NostrKeysType } from "../types"
|
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="viewport" content="initial-scale=1.0, width=device-width" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
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
|
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."
|
page load, and the default relay deletes messages after 1 hour."
|
||||||
/>
|
/>
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://sendstr.com/" />
|
<meta property="og:url" content="https://sendstr.com/" />
|
||||||
<meta property="og:title" content="Senstr" />
|
<meta property="og:title" content="Sendstr" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
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="og:image" content="/favicon-16x16.png" />
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<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:title" content="Sendstr" />
|
||||||
<meta
|
<meta
|
||||||
property="twitter:description"
|
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 property="twitter:image" content="/favicon-16x16.png" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<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="viewport" content="initial-scale=1.0, width=device-width" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
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
|
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."
|
page load, and the default relay deletes messages after 1 hour."
|
||||||
/>
|
/>
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://sendstr.com/settings" />
|
<meta property="og:url" content="https://sendstr.com/settings" />
|
||||||
<meta property="og:title" content="Senstr - Settings" />
|
<meta property="og:title" content="Sendstr - Settings" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
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="og:image" content="/favicon-16x16.png" />
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<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:title" content="Sendstr - Settings" />
|
||||||
<meta
|
<meta
|
||||||
property="twitter:description"
|
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 property="twitter:image" content="/favicon-16x16.png" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|||||||
10
src/types.ts
10
src/types.ts
@@ -1,3 +1,5 @@
|
|||||||
|
import { Relay, Sub } from "nostr-tools"
|
||||||
|
|
||||||
export type NostrSubType = ({
|
export type NostrSubType = ({
|
||||||
cb,
|
cb,
|
||||||
filter,
|
filter,
|
||||||
@@ -34,17 +36,15 @@ export type NostrPublishType = ({
|
|||||||
export type NostrPoolType = {
|
export type NostrPoolType = {
|
||||||
setPrivateKey: (priv: string) => void
|
setPrivateKey: (priv: string) => void
|
||||||
addRelay: (url: string, { read, write }: { read: boolean; write: boolean }) => void
|
addRelay: (url: string, { read, write }: { read: boolean; write: boolean }) => void
|
||||||
sub: NostrSubType
|
subs: NostrSubType
|
||||||
publish: NostrPublishType
|
publish: NostrPublishType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NostrType = {
|
export type NostrType = {
|
||||||
priv: string
|
priv: string
|
||||||
pub: string
|
pub: string
|
||||||
pool: NostrPoolType | null
|
subs: Sub[],
|
||||||
sub: {
|
relays: Relay[]
|
||||||
unsub: () => void
|
|
||||||
} | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NostrEventType = {
|
export type NostrEventType = {
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ export const ReceiveView = ({ keys }: ReceiveViewProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const sub = await subscribe(keys, peerKey, processEvent)
|
const { subs, relays } = await subscribe(keys, peerKey, processEvent)
|
||||||
nostr.current = { ...sub, ...keys }
|
nostr.current = { subs, relays, ...keys }
|
||||||
return () => {
|
return () => {
|
||||||
nostr?.current?.sub?.unsub()
|
nostr?.current?.subs.forEach(sub => sub.unsub())
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}, [peerKey])
|
}, [peerKey])
|
||||||
@@ -56,7 +56,7 @@ export const ReceiveView = ({ keys }: ReceiveViewProps) => {
|
|||||||
const sendMessage = useRef(
|
const sendMessage = useRef(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
debounce(async (peerKey: string, message: string) => {
|
debounce(async (peerKey: string, message: string) => {
|
||||||
if (nostr?.current?.pool) {
|
if (nostr?.current?.relays) {
|
||||||
await sendEncryptedMessage({ ...nostr.current, peerKey, message })
|
await sendEncryptedMessage({ ...nostr.current, peerKey, message })
|
||||||
}
|
}
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const PeerInput = ({ peerKey, onChange, setShowScan }: PeerInputProps) => {
|
|||||||
<section className="mx-auto max-w-[40rem] p-4">
|
<section className="mx-auto max-w-[40rem] p-4">
|
||||||
<label>Peer pubkey:</label>
|
<label>Peer pubkey:</label>
|
||||||
<div className="relative">
|
<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">
|
<div className="absolute right-0 top-0 h-full flex items-center">
|
||||||
<IconButton className="w-10 h-10 mr-2" onClick={() => setShowScan(true)}>
|
<IconButton className="w-10 h-10 mr-2" onClick={() => setShowScan(true)}>
|
||||||
<div className="m-2">
|
<div className="m-2">
|
||||||
@@ -79,10 +79,10 @@ export const SendView = ({ keys }: SendViewProps) => {
|
|||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
setShowScan(true)
|
setShowScan(true)
|
||||||
}
|
}
|
||||||
const sub = await subscribe(keys, peerKey, processEvent)
|
const { subs, relays } = await subscribe(keys, peerKey, processEvent)
|
||||||
nostr.current = { ...sub, ...keys }
|
nostr.current = { subs, relays, ...keys }
|
||||||
return () => {
|
return () => {
|
||||||
nostr?.current?.sub?.unsub()
|
nostr?.current?.subs.forEach(sub => sub.unsub())
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}, [])
|
}, [])
|
||||||
@@ -99,7 +99,7 @@ export const SendView = ({ keys }: SendViewProps) => {
|
|||||||
const sendMessage = useRef(
|
const sendMessage = useRef(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
debounce(async (peerKey: string, message: string) => {
|
debounce(async (peerKey: string, message: string) => {
|
||||||
if (nostr.current?.pool) {
|
if (nostr.current?.relays) {
|
||||||
await sendEncryptedMessage({ ...nostr.current, peerKey, message })
|
await sendEncryptedMessage({ ...nostr.current, peerKey, message })
|
||||||
}
|
}
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|||||||
Reference in New Issue
Block a user