Merge pull request #1 from reyamir/nostr-connect

Implemented nostr connect redesign
This commit is contained in:
Ren Amamiya
2023-11-21 10:25:23 +07:00
committed by GitHub
28 changed files with 7591 additions and 1772 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules node_modules
*.build.js *.build.js
*.zip *.zip
/extension/build/style.css

View File

@@ -1,5 +1,3 @@
module.exports = api => { module.exports = api => {
return { return {
presets: [ presets: [

View File

@@ -14,11 +14,12 @@ esbuild
'background.build': './extension/background.js', 'background.build': './extension/background.js',
'content-script.build': './extension/content-script.js' 'content-script.build': './extension/content-script.js'
}, },
outdir: './extension', outdir: './extension/build',
sourcemap: prod ? false : 'inline', sourcemap: prod ? false : 'inline',
define: { define: {
window: 'self', window: 'self',
global: 'self' global: 'self'
} },
watch: !prod
}) })
.then(() => console.log('build success.')) .then(() => console.log('build success.'))

BIN
extension/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -130,8 +130,8 @@ async function handleContentScriptMessage({type, params, host}) {
browser.windows.create({ browser.windows.create({
url: `${browser.runtime.getURL('prompt.html')}?${qs.toString()}`, url: `${browser.runtime.getURL('prompt.html')}?${qs.toString()}`,
type: 'popup', type: 'popup',
width: 340, width: 600,
height: 360 height: 600
}) })
}) })

87
extension/icons.jsx Normal file
View File

@@ -0,0 +1,87 @@
import React from 'react'
export function LogoIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="56"
height="56"
fill="none"
viewBox="0 0 56 56"
>
<rect width="56" height="56" fill="#EEECFD" rx="16"></rect>
<rect
width="55"
height="55"
x="0.5"
y="0.5"
stroke="#5A41F4"
strokeOpacity="0.25"
rx="15.5"
></rect>
<rect
width="39"
height="39"
x="8.5"
y="8.5"
fill="url(#paint0_linear_24_2379)"
rx="19.5"
></rect>
<rect
width="39"
height="39"
x="8.5"
y="8.5"
stroke="#6149F6"
rx="19.5"
></rect>
<g fill="#fff" stroke="#6149F6" clipPath="url(#clip0_24_2379)">
<path d="M23.78 20.634l.408-.235-.21-.422a4.432 4.432 0 01-.458-1.797l-.031-.78-.696.355A11.533 11.533 0 0016.5 27.998h0V28c.002.87.103 1.738.302 2.585a3.525 3.525 0 102.843-1.058A8.377 8.377 0 0119.5 28a8.523 8.523 0 014.28-7.366zM36.5 28.023v.468l.467.03c.621.042 1.227.212 1.778.5l.687.36.044-.774.005-.075c.01-.166.02-.349.02-.532v-.001a11.524 11.524 0 00-8.142-10.99 3.526 3.526 0 10-.501 2.989A8.524 8.524 0 0136.5 28s0 0 0 0v.022zM33.185 32.622a3.49 3.49 0 00.311 1.844 8.442 8.442 0 01-9.766.877l-.407-.239-.262.392c-.343.514-.79.95-1.311 1.282l-.652.414.645.425a11.39 11.39 0 0014.092-1.23c.264.069.536.107.81.113h.01a3.5 3.5 0 002.803-5.6h.556l-1.603-.932a3.49 3.49 0 00-5.226 2.654z"></path>
</g>
<defs>
<linearGradient
id="paint0_linear_24_2379"
x1="28"
x2="28"
y1="8"
y2="48"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#8E7CFF"></stop>
<stop offset="1" stopColor="#5A41F4"></stop>
</linearGradient>
<clipPath id="clip0_24_2379">
<path
fill="#fff"
d="M0 0H24V24H0z"
transform="translate(16 15)"
></path>
</clipPath>
</defs>
</svg>
)
}
export function SettingsIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
)
}

BIN
extension/icons/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

BIN
extension/icons/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
extension/icons/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

BIN
extension/icons/icon32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
extension/icons/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,27 +1,27 @@
{ {
"name": "nos2x", "name": "Nostr Connect",
"description": "Nostr Signer Extension", "description": "Nostr Signer Extension",
"version": "2.2.0", "version": "0.1.0",
"homepage_url": "https://github.com/fiatjaf/nos2x", "homepage_url": "https://github.com/reyamir/nostr-connect",
"manifest_version": 3, "manifest_version": 3,
"icons": { "icons": {
"16": "icons/16x16.png", "16": "icons/icon16.png",
"32": "icons/32x32.png", "32": "icons/icon32.png",
"48": "icons/48x48.png", "48": "icons/icon48.png",
"128": "icons/128x128.png" "128": "icons/icon128.png"
}, },
"options_page": "options.html", "options_page": "options.html",
"background": { "background": {
"service_worker": "background.build.js" "service_worker": "/build/background.build.js"
}, },
"action": { "action": {
"default_title": "nos2x", "default_title": "Nostr Connect",
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"content_scripts": [ "content_scripts": [
{ {
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
"js": ["content-script.build.js"], "js": ["/build/content-script.build.js"],
"all_frames": true "all_frames": true
} }
], ],

View File

@@ -1,24 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html>
<meta charset="utf-8" /> <head>
<title>nos2x</title> <meta charset="utf-8" />
<style> <title>Nostr Connect</title>
* { <meta name="viewport" content="width=device-width, initial-scale=1.0" />
font-family: monospace; <link href="/build/style.css" rel="stylesheet" />
} </head>
</style> <body class="bg-background text-foreground text-sm font-sans antialiased">
<div id="main" />
<div id="main" /> <script src="/build/options.build.js"></script>
</body>
<script src="options.build.js"></script> </html>
<style>
table {
border-collapse: collapse;
}
th,
td {
border: 1px solid;
padding: 1px 2px;
}
</style>

View File

@@ -3,8 +3,10 @@ import React, {useState, useCallback, useEffect} from 'react'
import {render} from 'react-dom' import {render} from 'react-dom'
import {generatePrivateKey, nip19} from 'nostr-tools' import {generatePrivateKey, nip19} from 'nostr-tools'
import QRCode from 'react-qr-code' import QRCode from 'react-qr-code'
import * as Tabs from '@radix-ui/react-tabs'
import {LogoIcon} from './icons'
import {removePermissions} from './common' import {removePermissions} from './common'
import * as Checkbox from '@radix-ui/react-checkbox'
function Options() { function Options() {
let [privKey, setPrivKey] = useState('') let [privKey, setPrivKey] = useState('')
@@ -79,216 +81,261 @@ function Options() {
} }
return ( return (
<> <div className="w-screen h-screen flex flex-col items-center justify-center">
<h1 style={{fontSize: '25px', marginBlockEnd: '0px'}}>nos2x</h1> <div className="p-8 shadow-primary border border-primary rounded-2xl max-w-xl mx-auto flex flex-col gap-4">
<p style={{marginBlockStart: '0px'}}>nostr signer extension</p> <div className="flex items-center gap-4">
<h2 style={{marginBlockStart: '20px', marginBlockEnd: '5px'}}>options</h2> <LogoIcon />
<div
style={{
marginBottom: '10px',
display: 'flex',
flexDirection: 'column',
gap: '10px',
width: 'fit-content'
}}
>
<div> <div>
<div>private key:&nbsp;</div> <h1 className="text-lg font-semibold">Nostr Connect</h1>
<div <p className="text-sm text-muted font-medium">Nostr signer</p>
style={{ </div>
marginLeft: '10px', </div>
display: 'flex', <div className="flex flex-col">
flexDirection: 'column', <div className="mb-4 flex flex-col gap-2">
gap: '10px' <div className="font-semibold text-base">Private key:</div>
}} <div>
> <div className="flex gap-2">
<div style={{display: 'flex', gap: '10px'}}>
<input <input
type={hidingPrivateKey ? 'password' : 'text'} type={hidingPrivateKey ? 'password' : 'text'}
style={{width: '600px'}}
value={privKey} value={privKey}
onChange={handleKeyChange} onChange={handleKeyChange}
className="flex-1 h-9 bg-transparent border border-primary px-3 py-1 rounded-lg"
/> />
{privKey === '' && <button onClick={generate}>generate</button>} <div className="shrink-0">
{!privKey && (
<button
type="button"
onClick={generate}
className="px-3 h-9 font-semibold border w-24 border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
>
Generate
</button>
)}
{privKey && hidingPrivateKey && ( {privKey && hidingPrivateKey && (
<button onClick={() => hidePrivateKey(false)}>show key</button> <button
type="button"
onClick={() => hidePrivateKey(false)}
className="px-3 h-9 font-bold border w-24 border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
>
Show key
</button>
)} )}
{privKey && !hidingPrivateKey && ( {privKey && !hidingPrivateKey && (
<button onClick={() => hidePrivateKey(true)}>hide key</button> <button
type="button"
onClick={() => hidePrivateKey(true)}
className="px-3 h-9 font-bold border w-24 border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
>
Hide key
</button>
)} )}
</div> </div>
{privKey && !isKeyValid() && ( </div>
<div style={{color: 'red'}}>private key is invalid!</div> <div className="mt-1 text-sm">
{privKey && !isKeyValid() ? (
<p className="text-red-500">Private key is invalid!</p>
) : (
<p className="text-gray-500">
Your key is stored locally. The developer has no way of
seeing your keys.
</p>
)} )}
</div>
{!hidingPrivateKey && isKeyValid() && ( {!hidingPrivateKey && isKeyValid() && (
<div <div className="mt-5 flex flex-col items-center">
style={{
height: 'auto',
maxWidth: 256,
width: '100%',
marginTop: '5px'
}}
>
<QRCode <QRCode
size={256} size={256}
style={{height: 'auto', maxWidth: '100%', width: '100%'}}
value={privKey.toUpperCase()} value={privKey.toUpperCase()}
viewBox={`0 0 256 256`} viewBox={`0 0 256 256`}
className="w-full max-w-full"
/> />
</div> </div>
)} )}
</div> </div>
</div> </div>
<div> <Tabs.Root className="mb-4" defaultValue="relays">
<div>preferred relays:</div> <Tabs.List className="mb-4 w-full border-b border-primary h-11 flex items-center gap-6">
<div <Tabs.Trigger
style={{ className="font-medium flex items-center text-muted gap-2 h-11 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
marginLeft: '10px', value="relays"
display: 'flex',
flexDirection: 'column',
gap: '1px'
}}
> >
Relays
<span className="px-3 h-6 inline-flex items-center justify-center bg-muted data-[state=active]:text-primary rounded-full">
{relays.length}
</span>
</Tabs.Trigger>
<Tabs.Trigger
className="font-medium flex items-center text-muted gap-2 h-11 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
value="permissions"
>
Permissions
<span className="px-3 h-6 inline-flex items-center justify-center bg-muted data-[state=active]:text-primary rounded-full">
{policies.length}
</span>
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="relays">
<div className="flex flex-col gap-2">
<div className="font-semibold text-base">Preferred Relays:</div>
<div className="flex flex-col gap-2">
{relays.map(({url, policy}, i) => ( {relays.map(({url, policy}, i) => (
<div <div key={i} className="flex items-center gap-4">
key={i}
style={{display: 'flex', alignItems: 'center', gap: '15px'}}
>
<input <input
style={{width: '400px'}}
value={url} value={url}
onChange={changeRelayURL.bind(null, i)} onChange={changeRelayURL.bind(null, i)}
className="flex-1 h-9 bg-transparent border px-3 py-1 border-primary rounded-lg placeholder:text-muted"
/> />
<div style={{display: 'flex', gap: '5px'}}> <div className="flex items-center gap-2">
<label style={{display: 'flex', alignItems: 'center'}}> <div className="inline-flex items-center gap-2">
read <Checkbox.Root
<input id={`read-${i}`}
type="checkbox"
checked={policy.read} checked={policy.read}
onChange={toggleRelayPolicy.bind(null, i, 'read')} onCheckedChange={toggleRelayPolicy.bind(
/> null,
</label> i,
<label style={{display: 'flex', alignItems: 'center'}}> 'read'
write )}
<input className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
type="checkbox" >
checked={policy.write} <Checkbox.Indicator className="text-white">
onChange={toggleRelayPolicy.bind(null, i, 'write')} <svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/> />
</svg>
</Checkbox.Indicator>
</Checkbox.Root>
<label
htmlFor={`read-${i}`}
className="text-muted font-medium"
>
Read
</label> </label>
</div> </div>
<button onClick={removeRelay.bind(null, i)}>remove</button> <div className="inline-flex items-center gap-2">
<Checkbox.Root
id={`write-${i}`}
checked={policy.write}
onCheckedChange={toggleRelayPolicy.bind(
null,
i,
'write'
)}
className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
>
<Checkbox.Indicator className="text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
</Checkbox.Indicator>
</Checkbox.Root>
<label
htmlFor={`write-${i}`}
className="text-muted font-medium"
>
Write
</label>
</div>
</div>
<button
onClick={removeRelay.bind(null, i)}
className="shrink-0 px-3 w-24 h-9 font-semibold border border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
>
Remove
</button>
</div> </div>
))} ))}
<div style={{display: 'flex', gap: '10px', marginTop: '5px'}}> <div className="flex gap-2">
<input <input
style={{width: '400px'}}
value={newRelayURL} value={newRelayURL}
onChange={e => setNewRelayURL(e.target.value)} onChange={e => setNewRelayURL(e.target.value)}
onKeyDown={e => { onKeyDown={e => {
if (e.key === 'Enter') addNewRelay() if (e.key === 'Enter') addNewRelay()
}} }}
placeholder="wss://"
className="flex-1 h-9 bg-transparent border px-3 py-1 border-primary rounded-lg placeholder:text-muted"
/> />
<button disabled={!newRelayURL} onClick={addNewRelay}>
add relay
</button>
</div>
</div>
</div>
<div>
<label style={{display: 'flex', alignItems: 'center'}}>
<div>
handle{' '}
<span style={{padding: '2px', background: 'silver'}}>nostr:</span>{' '}
links:
</div>
<input
type="checkbox"
checked={handleNostrLinks}
onChange={changeHandleNostrLinks}
/>
</label>
<div style={{marginLeft: '10px'}}>
{handleNostrLinks && (
<div>
<div style={{display: 'flex'}}>
<input
placeholder="url template"
value={protocolHandler}
onChange={handleChangeProtocolHandler}
style={{width: '680px', maxWidth: '90%'}}
/>
{!showProtocolHandlerHelp && (
<button onClick={changeShowProtocolHandlerHelp}>?</button>
)}
</div>
{showProtocolHandlerHelp && (
<pre>{`
{raw} = anything after the colon, i.e. the full nip19 bech32 string
{hex} = hex pubkey for npub or nprofile, hex event id for note or nevent
{p_or_e} = "p" for npub or nprofile, "e" for note or nevent
{u_or_n} = "u" for npub or nprofile, "n" for note or nevent
{relay0} = first relay in a nprofile or nevent
{relay1} = second relay in a nprofile or nevent
{relay2} = third relay in a nprofile or nevent
{hrp} = human-readable prefix of the nip19 string
examples:
- https://njump.me/{raw}
- https://snort.social/{raw}
- https://nostr.band/{raw}
`}</pre>
)}
</div>
)}
</div>
</div>
<label style={{display: 'flex', alignItems: 'center'}}>
show notifications when permissions are used:
<input
type="checkbox"
checked={showNotifications}
onChange={handleNotifications}
/>
</label>
<button <button
disabled={!unsavedChanges.length} disabled={!newRelayURL}
onClick={saveChanges} onClick={addNewRelay}
style={{padding: '5px 20px'}} className="shrink-0 px-3 w-24 h-9 font-semibold border border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
> >
save Add Relay
</button> </button>
<div style={{fontSize: '120%'}}>
{messages.map((message, i) => (
<div key={i}>{message}</div>
))}
</div> </div>
</div> </div>
<div> </div>
<h2>permissions</h2> </Tabs.Content>
<table> <Tabs.Content value="permissions">
<div className="flex flex-col gap-2">
<div className="font-semibold text-base">Permissions:</div>
{!policies.length ? (
<div className="text-muted">
You haven't granted any permissions to any apps yet
</div>
) : (
<table className="table-auto">
<thead> <thead>
<tr> <tr className="mb-2">
<th>domain</th> <th className="text-left border-b-8 border-transparent">
<th>permission</th> Domain
<th>answer</th> </th>
<th>conditions</th> <th className="text-left border-b-8 border-transparent">
<th>since</th> Permission
</th>
<th className="text-left border-b-8 border-transparent">
Answer
</th>
<th className="text-left border-b-8 border-transparent">
Conditions
</th>
<th className="text-left border-b-8 border-transparent">
Since
</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{policies.map(({host, type, accept, conditions, created_at}) => ( {policies.map(
<tr key={host + type + accept + JSON.stringify(conditions)}> ({host, type, accept, conditions, created_at}) => (
<td>{host}</td> <tr
<td>{type}</td> key={
<td>{accept === 'true' ? 'allow' : 'deny'}</td> host + type + accept + JSON.stringify(conditions)
<td> }
>
<td className="font-semibold">{host}</td>
<td className="text-muted">{type}</td>
<td className="text-muted">
{accept === 'true' ? 'allow' : 'deny'}
</td>
<td className="text-muted">
{conditions.kinds {conditions.kinds
? `kinds: ${Object.keys(conditions.kinds).join(', ')}` ? `kinds: ${Object.keys(conditions.kinds).join(
', '
)}`
: 'always'} : 'always'}
</td> </td>
<td> <td className="text-muted">
{new Date(created_at * 1000) {new Date(created_at * 1000)
.toISOString() .toISOString()
.split('.')[0] .split('.')[0]
@@ -301,12 +348,14 @@ function Options() {
data-host={host} data-host={host}
data-accept={accept} data-accept={accept}
data-type={type} data-type={type}
className="text-primary font-semibold"
> >
revoke Revoke
</button> </button>
</td> </td>
</tr> </tr>
))} )
)}
{!policies.length && ( {!policies.length && (
<tr> <tr>
{Array(5) {Array(5)
@@ -318,13 +367,134 @@ function Options() {
)} )}
</tbody> </tbody>
</table> </table>
{!policies.length && ( )}
<div style={{marginTop: '5px'}}> </div>
no permissions have been granted yet </Tabs.Content>
</Tabs.Root>
<div className="mb-3">
<div className="flex items-center gap-2">
<Checkbox.Root
id="notification"
className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
checked={showNotifications}
onCheckedChange={handleNotifications}
>
<Checkbox.Indicator className="text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
</Checkbox.Indicator>
</Checkbox.Root>
<label htmlFor="notification">
Show desktop notifications when a permissions has been used
</label>
</div>
</div>
<div>
<details>
<summary className="flex items-center justify-between">
<div className="font-semibold text-base">Advanced</div>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
/>
</svg>
</div>
</summary>
<div className="mt-3">
<div className="flex items-center gap-2">
<Checkbox.Root
id="nostrlink"
className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
checked={handleNostrLinks}
onCheckedChange={changeHandleNostrLinks}
>
<Checkbox.Indicator className="text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
</Checkbox.Indicator>
</Checkbox.Root>
<label htmlFor="nostrlink">Handle nostr links</label>
</div>
{handleNostrLinks && (
<div className="mt-3">
<div className="flex">
<input
placeholder="url template"
value={protocolHandler}
onChange={handleChangeProtocolHandler}
/>
{!showProtocolHandlerHelp && (
<button onClick={changeShowProtocolHandlerHelp}>
?
</button>
)}
</div>
{showProtocolHandlerHelp && (
<pre className="bg-muted px-2 rounded-xl overflow-scroll">{`
{raw} = anything after the colon, i.e. the full nip19 bech32 string
{hex} = hex pubkey for npub or nprofile, hex event id for note or nevent
{p_or_e} = "p" for npub or nprofile, "e" for note or nevent
{u_or_n} = "u" for npub or nprofile, "n" for note or nevent
{relay0} = first relay in a nprofile or nevent
{relay1} = second relay in a nprofile or nevent
{relay2} = third relay in a nprofile or nevent
{hrp} = human-readable prefix of the nip19 string
examples:
- https://njump.me/{raw}
- https://snort.social/{raw}
- https://nostr.band/{raw}
`}</pre>
)}
</div> </div>
)} )}
</div> </div>
</> </details>
</div>
</div>
<button
disabled={!unsavedChanges.length}
onClick={saveChanges}
className="w-full h-10 bg-primary rounded-xl font-bold inline-flex items-center justify-center text-white disabled:cursor-not-allowed disabled:opacity-70 transform active:translate-y-1 transition-transform ease-in-out duration-75"
>
Save
</button>
</div>
</div>
) )
async function handleKeyChange(e) { async function handleKeyChange(e) {
@@ -399,6 +569,7 @@ function Options() {
function addNewRelay() { function addNewRelay() {
if (newRelayURL.trim() === '') return if (newRelayURL.trim() === '') return
if (!newRelayURL.startsWith('wss://')) return
relays.push({ relays.push({
url: newRelayURL, url: newRelayURL,
policy: {read: true, write: true} policy: {read: true, write: true}

View File

@@ -1,8 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html>
<meta charset="utf-8" /> <head>
<title>nos2x</title> <meta charset="utf-8" />
<title>Nostr Connect</title>
<div id="main" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/build/style.css" rel="stylesheet" />
<script src="popup.build.js"></script> </head>
<body class="bg-background text-foreground text-sm font-sans antialiased">
<div id="main" />
<script src="/build/popup.build.js"></script>
</body>
</html>

View File

@@ -1,12 +1,28 @@
import browser from 'webextension-polyfill' import browser from 'webextension-polyfill'
import {render} from 'react-dom' import {render} from 'react-dom'
import {getPublicKey, nip19} from 'nostr-tools' import {getPublicKey, nip19} from 'nostr-tools'
import React, {useState, useRef, useEffect} from 'react' import React, {useState, useMemo, useEffect} from 'react'
import QRCode from 'react-qr-code' import QRCode from 'react-qr-code'
import {SettingsIcon} from './icons'
import {minidenticon} from 'minidenticons'
import * as Tabs from '@radix-ui/react-tabs'
function Popup() { function Popup() {
let [pubKey, setPubKey] = useState('') let [keys, setKeys] = useState(null)
let keys = useRef([]) let avatarURI = useMemo(
() =>
keys
? 'data:image/svg+xml;utf8,' +
encodeURIComponent(minidenticon(keys.npub, 90, 30))
: null,
[keys]
)
const gotoSettings = () => {
browser.tabs.create({
url: browser.runtime.getURL('/options.html')
})
}
useEffect(() => { useEffect(() => {
browser.storage.local.get(['private_key', 'relays']).then(results => { browser.storage.local.get(['private_key', 'relays']).then(results => {
@@ -14,10 +30,7 @@ function Popup() {
let hexKey = getPublicKey(results.private_key) let hexKey = getPublicKey(results.private_key)
let npubKey = nip19.npubEncode(hexKey) let npubKey = nip19.npubEncode(hexKey)
setPubKey(npubKey) setKeys({npub: npubKey, hex: hexKey})
keys.current.push(npubKey)
keys.current.push(hexKey)
if (results.relays) { if (results.relays) {
let relaysList = [] let relaysList = []
@@ -32,63 +45,138 @@ function Popup() {
pubkey: hexKey, pubkey: hexKey,
relays: relaysList relays: relaysList
}) })
keys.current.push(nprofileKey) setKeys(prev => ({...prev, nprofile: nprofileKey}))
} }
} }
} else { } else {
setPubKey(null) setKeys(null)
} }
}) })
}, []) }, [])
return ( return (
<> <div className="w-[320px] p-6">
<h2>nos2x</h2> {!keys ? (
{pubKey === null ? ( <div className="flex items-center justify-between gap-6">
<p style={{width: '150px'}}> <div className="flex-1 flex items-center justify-between">
you don't have a private key set. use the options page to set one. <p className="text-sm font-medium">
Click here to enter or create
<br />
your first identity
</p> </p>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/>
</svg>
</div>
<button
type="button"
onClick={() => gotoSettings()}
className="w-9 h-9 shrink-0 border border-primary shadow-sm rounded-xl inline-flex items-center justify-center"
>
<SettingsIcon className="w-5 h-5 text-muted" />
</button>
</div>
) : ( ) : (
<> <div>
<p> <div className="mb-2 flex items-center justify-between">
<a onClick={toggleKeyType}>↩️</a> your public key: <div className="flex items-center gap-2">
</p> {avatarURI ? (
<pre <img
style={{ src={avatarURI}
whiteSpace: 'pre-wrap', className="w-9 h-9 rounded-full bg-muted"
wordBreak: 'break-all', />
width: '200px' ) : (
}} <div className="w-9 h-9 rounded-full bg-muted" />
)}
<p className="font-semibold">Account</p>
</div>
<button
type="button"
onClick={() => gotoSettings()}
className="w-9 h-9 shrink-0 border border-primary shadow-sm rounded-xl inline-flex items-center justify-center"
> >
<code>{pubKey}</code> <SettingsIcon className="w-5 h-5 text-muted" />
</pre> </button>
</div>
<div <div>
style={{ <Tabs.Root defaultValue="npub">
height: 'auto', <Tabs.List className="w-full border-b border-primary h-10 flex items-center">
margin: '0 auto', <Tabs.Trigger
maxWidth: 256, value="npub"
width: '100%' className="font-medium flex-1 flex items-center justify-center text-muted h-10 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
}}
> >
<QRCode npub
size={256} </Tabs.Trigger>
style={{height: 'auto', maxWidth: '100%', width: '100%'}} <Tabs.Trigger
value={pubKey.startsWith('n') ? pubKey.toUpperCase() : pubKey} value="hex"
viewBox={`0 0 256 256`} className="font-medium flex-1 flex items-center justify-center text-muted h-10 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
>
hex
</Tabs.Trigger>
{keys.nprofile ? (
<Tabs.Trigger
value="nprofile"
className="font-medium flex-1 flex items-center justify-center text-muted h-10 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
>
nprofile
</Tabs.Trigger>
) : null}
</Tabs.List>
<Tabs.Content value="npub">
<div className="my-4">
<textarea
value={keys.npub}
readOnly
className="w-full h-20 resize-none p-3 bg-muted rounded-xl"
/> />
</div> </div>
</> <div className="w-full rounded-xl border border-primary p-4 flex items-center justify-center">
<QRCode size={128} value={keys.npub} />
</div>
</Tabs.Content>
<Tabs.Content value="hex">
<div className="my-4">
<textarea
value={keys.hex}
readOnly
className="w-full h-20 resize-none p-3 bg-muted rounded-xl"
/>
</div>
<div className="w-full rounded-xl border border-primary p-4 flex items-center justify-center">
<QRCode size={128} value={keys.hex} />
</div>
</Tabs.Content>
{keys.nprofile ? (
<Tabs.Content value="nprofile">
<div className="my-4">
<textarea
value={keys.nprofile}
readOnly
className="w-full h-20 resize-none p-3 bg-muted rounded-xl"
/>
</div>
<div className="w-full rounded-xl border border-primary p-4 flex items-center justify-center">
<QRCode size={128} value={keys.nprofile} />
</div>
</Tabs.Content>
) : null}
</Tabs.Root>
</div>
</div>
)} )}
</> </div>
) )
function toggleKeyType(e) {
e.preventDefault()
let nextKeyType =
keys.current[(keys.current.indexOf(pubKey) + 1) % keys.current.length]
setPubKey(nextKeyType)
}
} }
render(<Popup />, document.getElementById('main')) render(<Popup />, document.getElementById('main'))

View File

@@ -1,8 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html>
<meta charset="utf-8" /> <head>
<title>nos2x</title> <meta charset="utf-8" />
<title>Nostr Connect</title>
<div id="main" style="width: 300px; height: 200px; margin: auto" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/build/style.css" rel="stylesheet" />
<script src="prompt.build.js"></script> </head>
<body class="bg-background text-foreground text-sm font-sans antialiased">
<div id="main" />
<script src="build/prompt.build.js"></script>
</body>
</html>

View File

@@ -1,15 +1,20 @@
import browser from 'webextension-polyfill' import browser from 'webextension-polyfill'
import {render} from 'react-dom' import {render} from 'react-dom'
import React from 'react' import React, {useState} from 'react'
import {PERMISSION_NAMES} from './common' import {PERMISSION_NAMES} from './common'
import {LogoIcon} from './icons'
import * as Checkbox from '@radix-ui/react-checkbox'
function Prompt() { function Prompt() {
const [isRemember, setIsRemember] = useState(false)
let qs = new URLSearchParams(location.search) let qs = new URLSearchParams(location.search)
let id = qs.get('id') let id = qs.get('id')
let host = qs.get('host') let host = qs.get('host')
let type = qs.get('type') let type = qs.get('type')
let params, event let params, event
try { try {
params = JSON.parse(qs.get('params')) params = JSON.parse(qs.get('params'))
if (Object.keys(params).length === 0) params = null if (Object.keys(params).length === 0) params = null
@@ -18,24 +23,85 @@ function Prompt() {
params = null params = null
} }
function authorizeHandler(accept) {
const conditions = isRemember ? {} : null
return function (ev) {
ev.preventDefault()
browser.runtime.sendMessage({
prompt: true,
id,
host,
type,
accept,
conditions
})
}
}
return ( return (
<> <div className="w-screen h-screen flex flex-col items-center justify-center">
<div> <div className="p-8 shadow-primary border border-primary rounded-2xl max-w-xl mx-auto flex flex-col gap-5">
<b style={{display: 'block', textAlign: 'center', fontSize: '200%'}}> <div className="flex flex-col items-center gap-5">
{host} <LogoIcon />
</b>{' '} <div className="flex flex-col items-center text-center">
<h1 className="font-semibold text-lg">{host}</h1>
<p> <p>
is requesting your permission to <b>{PERMISSION_NAMES[type]}:</b> is requesting your permission to <b>{PERMISSION_NAMES[type]}</b>
</p> </p>
</div> </div>
</div>
{params && ( {params && (
<> <div className="flex flex-col gap-1">
<p>now acting on</p> <p>Now acting on</p>
<pre style={{overflow: 'auto', maxHeight: '120px'}}> <pre className="bg-muted px-2 rounded-xl overflow-scroll">
<code>{JSON.stringify(event || params, null, 2)}</code> <code>{JSON.stringify(event || params, null, 2)}</code>
</pre> </pre>
</> </div>
)} )}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-center gap-2">
<Checkbox.Root
id="remember"
className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
onCheckedChange={setIsRemember}
>
<Checkbox.Indicator className="text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
</Checkbox.Indicator>
</Checkbox.Root>
<label htmlFor="remember" className="text-muted">
Remember my preference forever
</label>
</div>
<div className="flex gap-3">
<button
onClick={authorizeHandler(false)}
className="flex-1 h-10 rounded-lg shadow-sm border border-primary inline-flex items-center justify-center font-semibold"
>
Reject
</button>
<button
onClick={authorizeHandler(true)}
className="flex-1 h-10 rounded-lg shadow-sm border border-secondary bg-primary text-white inline-flex items-center justify-center font-semibold"
>
Authorize
</button>
</div>
</div>
{/*
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@@ -90,23 +156,10 @@ function Prompt() {
<button style={{marginTop: '5px'}} onClick={authorizeHandler(false)}> <button style={{marginTop: '5px'}} onClick={authorizeHandler(false)}>
reject reject
</button> </button>
</div>*/}
</div>
</div> </div>
</>
) )
function authorizeHandler(accept, conditions) {
return function (ev) {
ev.preventDefault()
browser.runtime.sendMessage({
prompt: true,
id,
host,
type,
accept,
conditions
})
}
}
} }
render(<Prompt />, document.getElementById('main')) render(<Prompt />, document.getElementById('main'))

7
extension/style.css Normal file
View File

@@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.data-\[state\=active\]\:text-primary[data-state=active] > .bg-muted {
@apply bg-secondary
}

View File

@@ -1,25 +1,31 @@
{ {
"license": "WTFPL", "license": "WTFPL",
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-tabs": "^1.0.4",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"esbuild": "^0.14.11", "esbuild": "^0.14.54",
"eslint": "^8.6.0", "eslint": "^8.54.0",
"eslint-plugin-babel": "^5.3.1", "eslint-plugin-babel": "^5.3.1",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.33.2",
"events": "^3.3.0", "events": "^3.3.0",
"nostr-tools": "^1.12.0", "minidenticons": "^4.2.0",
"prettier": "^2.5.1", "nostr-tools": "^1.17.0",
"prettier": "^2.8.8",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-native-svg": "^13.8.0", "react-native-svg": "^13.14.0",
"react-qr-code": "^2.0.11", "react-qr-code": "^2.0.12",
"use-boolean-state": "^1.0.2", "use-boolean-state": "^1.0.2",
"use-debounce": "^7.0.1", "use-debounce": "^7.0.1",
"webextension-polyfill": "^0.8.0" "webextension-polyfill": "^0.8.0"
}, },
"scripts": { "scripts": {
"build": "./build.js prod", "dev": "./build.js; pnpm exec tailwindcss -i ./extension/style.css -o ./extension/build/style.css --watch",
"watch": "ag -l --js | entr ./build.js", "build": "pnpm exec tailwindcss -i ./extension/style.css -o ./extension/build/style.css; ./build.js prod",
"package": "./build.js prod; cd extension; zip -r archive *; cd ..; mv extension/archive.zip ./nos2x.zip" "package": "./build.js prod; cd extension; zip -r archive *; cd ..; mv extension/archive.zip ./nostrconnect.zip"
},
"devDependencies": {
"tailwindcss": "^3.3.5"
} }
} }

6723
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

31
tailwind.config.js Normal file
View File

@@ -0,0 +1,31 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./extension/**/*.{html,js,jsx}'],
theme: {
extend: {
colors: {
background: '#FFF',
foreground: '#36364A',
muted: '#818498'
},
boxShadow: {
primary: '0px 10px 36px 0px rgba(64, 47, 132, 0.04)',
secondary:
'0px 1px 2px 0px rgba(18, 43, 105, 0.1), 0px 2px 6px 0px rgba(18, 43, 105, 0.04), 0px 0px 0px 1px rgba(18, 43, 105, 0.08)'
},
borderColor: {
primary: '#E1E3EA',
secondary: 'rgba(90, 65, 244, 1)'
},
backgroundColor: {
primary: 'rgba(90, 65, 244, 1)',
secondary: 'rgba(90, 65, 244, 0.1)',
muted: 'rgba(240, 240, 246, 1)'
},
textColor: {
primary: 'rgba(90, 65, 244, 1)'
}
}
},
plugins: []
}

1346
yarn.lock

File diff suppressed because it is too large Load Diff