initial
commit
8d60bf871c
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 qcode.ch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -0,0 +1,29 @@
|
||||
# nostr web sandbox
|
||||
|
||||
a playground for a web interface to [nostr](https://nostr.info/).
|
||||
some useful resources:
|
||||
|
||||
* JS library used in this project: https://github.com/fiatjaf/nostr-tools
|
||||
* NIPs: https://github.com/nostr-protocol/nips
|
||||
* relays registry: https://nostr-registry.netlify.app/
|
||||
* event inspector: https://nostr.com/
|
||||
* a working web interface in vue.js: https://astral.ninja/
|
||||
* https://github.com/aljazceru/awesome-nostr
|
||||
|
||||
## dev
|
||||
|
||||
nodejs v18.x and npm v8.x are recommended.
|
||||
|
||||
after `npm install`, start by running a dev server with:
|
||||
|
||||
npm run serve
|
||||
|
||||
and point a browser to http://127.0.0.1:8001/
|
||||
|
||||
the `serve` command injects a live reload snippet. to build a "production" copy,
|
||||
execute
|
||||
|
||||
npm run build
|
||||
|
||||
the build is done using [esbuild](https://esbuild.github.io/), with a config in
|
||||
[esbuildconf.js](esbuildconf.js). the result is placed in `dist` directory.
|
@ -0,0 +1,34 @@
|
||||
import { createRequire } from 'module';
|
||||
|
||||
import alias from 'esbuild-plugin-alias';
|
||||
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
|
||||
|
||||
// see docs at https://esbuild.github.io/api/
|
||||
export const options = {
|
||||
entryPoints: [
|
||||
'src/main.js',
|
||||
'src/main.css',
|
||||
'src/index.html',
|
||||
'src/bubble.svg'
|
||||
],
|
||||
outdir: 'dist',
|
||||
//entryNames: '[name]-[hash]', TODO: replace urls in index.html with hashed paths
|
||||
loader: {'.html': 'copy', '.svg': 'copy'},
|
||||
bundle: true,
|
||||
platform: 'browser',
|
||||
minify: false, // TODO: true for release and enable sourcemap
|
||||
define: {
|
||||
window: 'self',
|
||||
global: 'self'
|
||||
},
|
||||
// https://github.com/esbuild/community-plugins
|
||||
plugins: [
|
||||
alias({
|
||||
// cipher-base require's "stream"
|
||||
stream: createRequire(import.meta.url).resolve('readable-stream')
|
||||
}),
|
||||
NodeGlobalsPolyfillPlugin({buffer: true})
|
||||
]
|
||||
};
|
||||
|
||||
export default {options: options}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "nostrweb",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||
"esbuild": "^0.14.54",
|
||||
"esbuild-plugin-alias": "^0.2.1",
|
||||
"events": "^3.3.0",
|
||||
"nostr-tools": "0.24.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node tools/build.js",
|
||||
"serve": "node tools/serve.js"
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 1000 1000" version="1.1" viewBox="0 0 1e3 1e3" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m240.4 673.3c-67.5 0-122.5 44.8-122.5 100 0 55.1 55 100 122.5 100s122.5-44.8 122.5-100c0-55.1-54.9-100-122.5-100zm0 160.3c-45.7 0-82.8-27-82.8-60.3 0-33.2 37.1-60.3 82.8-60.3s82.8 27 82.8 60.3c0 33.2-37.1 60.3-82.8 60.3z"/><path d="m108.5 865.9c-37.1 0-67.3 25.5-67.3 56.9s30.2 56.9 67.3 56.9 67.3-25.5 67.3-56.9-30.2-56.9-67.3-56.9zm0 74c-15.8 0-27.6-9.1-27.6-17.2s11.3-17.2 27.6-17.2c15.8 0 27.6 9.1 27.6 17.2s-11.8 17.2-27.6 17.2z"/><path d="m989.1 391c-5.4-48.7-35.5-92-83-120.4 0-5.9-0.3-11.5-0.9-16.8-10.2-91.8-93.6-161.2-194.7-162.8-38.1-50-102.5-77.1-171.9-69.3-35.3 4-68 16.6-95.5 36.8-28.5-25-67.5-37.4-108.8-32.8-58.6 6.5-106.6 47.2-119.1 99.1-130.6 36.7-217.5 153.8-203.8 277.4 13.7 122.4 120.2 216.1 254.6 225.3 28.3 24.1 69.3 35.8 112.4 31.1 19-2.1 37.5-7.5 54.1-15.7 31.8 22.3 72.4 34.4 115.8 34.4 8.6 0 17.2-0.5 25.8-1.4 62.8-7 117.7-38.7 147-84.1 12 1.6 24.1 2.4 36.2 2.4 10.1 0 20.3-0.6 30.4-1.7 122-13.6 212.3-104 201.4-201.5zm-205.6 162c-8.7 1-17.4 1.4-26 1.4-14.4 0-28.8-1.3-42.8-4l-14.5-2.7-6.8 13.2c-20.8 40.4-68.1 69.3-123.5 75.5-47 5.2-92.4-7.1-122.8-32.4l-10.9-9.1-12.1 7.4c-14.6 9-31.9 14.8-49.9 16.8-34 3.8-66.1-6-86-25.3l-5.4-5.3-7.5-0.3c-118.2-5.4-212.5-85.4-224.2-190.5-12-107.6 68-209.7 186.1-237.6l13.6-3.2 1.6-13.8c4.7-40.1 41.1-72.9 86.6-78 34.5-3.8 67.7 9 87.7 32.8l13.2 15.8 15.3-13.8c24.1-21.7 54.6-35.2 88.2-38.9 57.5-6.4 112.3 18.1 140.8 60.8l6.1 9.2 17.1-0.5c82.3 0 150.6 54.8 158.7 127.5 0.8 6.8 0.9 14.2 0.4 22.6l-0.8 12.9 11.5 6c42 21.9 68.6 56.7 72.9 95.8 8 75.9-66.6 146.5-166.6 157.7z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,20 @@
|
||||
/* https://developer.mozilla.org/en-US/docs/Web/CSS/Layout_cookbook/Media_objects */
|
||||
.mbox {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.mbox .mbox-img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.mbox .mbox-body {
|
||||
flex: 1;
|
||||
color: var(--fgcolor-accent);
|
||||
}
|
||||
|
||||
.mbox-body > .header {
|
||||
margin-top: 0;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* example usage:
|
||||
*
|
||||
* const props = {className: 'btn', onclick: async (e) => alert('hi')};
|
||||
* const btn = elem('button', props, ['download']);
|
||||
* document.body.append(btn);
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {HTMLElement.prototype} props
|
||||
* @param {Array<HTMLElement|string>} children
|
||||
* @return HTMLElement
|
||||
*/
|
||||
export function elem(name = 'div', props = {}, children = []) {
|
||||
const el = document.createElement(name);
|
||||
Object.assign(el, props);
|
||||
el.append(...children);
|
||||
return el;
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>nostr sandbox</title>
|
||||
<link rel="stylesheet" href="main.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="tabs">
|
||||
|
||||
<div class="tab">
|
||||
<input type="radio" name="maintabs" id="homefeed" checked>
|
||||
<label for="homefeed">feed</label>
|
||||
<div class="content">
|
||||
<div class="cards" id="feedlist"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<input type="radio" name="maintabs" id="trending">
|
||||
<label for="trending">trending</label>
|
||||
<div class="content">
|
||||
<p><a href="https://github.com/nostr-protocol/nips/blob/master/12.md">NIP-12 (generic queries)</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<input type="radio" name="maintabs" id="direct">
|
||||
<label for="direct">direct</label>
|
||||
<div class="content">
|
||||
<p><a href="https://github.com/nostr-protocol/nips/blob/master/04.md">NIP-04 (direct msg)</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<input type="radio" name="maintabs" id="chat">
|
||||
<label for="chat">chat</label>
|
||||
<div class="content">
|
||||
<p><a href="https://github.com/nostr-protocol/nips/blob/master/28.md">NIP-28 (public chat)</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
<script src="main.js"></script>
|
||||
</html>
|
@ -0,0 +1,39 @@
|
||||
@import "tabs.css";
|
||||
@import "cards.css";
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
--bgcolor: #fff7e9;
|
||||
--bgcolor-accent: #ff731d;
|
||||
--fgcolor: #1746a2;
|
||||
--fgcolor-accent: #5f9df7;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
--bgcolor: #191919;
|
||||
--bgcolor-accent: #2d4263;
|
||||
--fgcolor: #c84b31;
|
||||
--fgcolor-accent: #ecdbba;
|
||||
}
|
||||
|
||||
img {
|
||||
opacity: .75;
|
||||
transition: opacity .5s ease-in-out;
|
||||
}
|
||||
img:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
*, ::after, ::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bgcolor);
|
||||
color: var(--fgcolor);
|
||||
font-family: monospace;
|
||||
font-size: 120%;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import {relayPool} from 'nostr-tools';
|
||||
import {elem} from './domutil.js';
|
||||
|
||||
const pool = relayPool();
|
||||
pool.addRelay('wss://nostr.x1ddos.ch', {read: true, write: true});
|
||||
pool.addRelay('wss://nostr.bitcoiner.social/', {read: true, write: true});
|
||||
|
||||
const feedlist = document.querySelector('#feedlist');
|
||||
|
||||
function onEvent(evt, relay) {
|
||||
console.log(`event from ${relay}`, evt);
|
||||
const time = new Date(evt.created_at * 1000);
|
||||
const text = `${evt.content} - ${time.toISOString()}`; // TODO: Intl.DateTimeFormat
|
||||
const img = elem('img', {className: 'mbox-img', src: 'bubble.svg'}, '');
|
||||
const body = elem('div', {className: 'mbox-body'}, [text]);
|
||||
const art = elem('article', {className: 'mbox'}, [img, body]);
|
||||
feedlist.append(art);
|
||||
}
|
||||
|
||||
pool.sub({
|
||||
cb: onEvent,
|
||||
filter: {authors: [
|
||||
'52155da703585f25053830ac39863c80ea6adc57b360472c16c566a412d2bc38', // quark
|
||||
'a6057742e73ff93b89587c27a74edf2cdab86904291416e90dc98af1c5f70cfa', // mosc
|
||||
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d', // fiatjaf
|
||||
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245' // jb55
|
||||
]}
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
.tabs {
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.tab label {
|
||||
cursor: pointer;
|
||||
font-size: 1.1em;
|
||||
border-radius: 5px 5px 0 0;
|
||||
padding: .5em 1em;
|
||||
}
|
||||
|
||||
.tab [type=radio] {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.tab [type=radio] + label {
|
||||
outline: 1px solid var(--bgcolor-accent);
|
||||
}
|
||||
|
||||
/*
|
||||
.tab [type=radio]:focus + label {
|
||||
outline: 2px dotted black;
|
||||
}
|
||||
*/
|
||||
|
||||
.tab [type=radio]:checked ~ label {
|
||||
z-index: 2;
|
||||
background-color: var(--bgcolor-accent);
|
||||
color: var(--fgcolor-accent);
|
||||
}
|
||||
|
||||
.tab [type=radio]:checked ~ label ~ .content {
|
||||
z-index: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab .content {
|
||||
position: absolute;
|
||||
top: 2.5em;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 5px;
|
||||
opacity: 0;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import esbuild from 'esbuild';
|
||||
import config from '../esbuildconf.js';
|
||||
|
||||
(async () => {
|
||||
config.options.metafile = true;
|
||||
let res = await esbuild.build(config.options);
|
||||
let text = await esbuild.analyzeMetafile(res.metafile);
|
||||
console.log(text);
|
||||
})();
|
@ -0,0 +1,55 @@
|
||||
import { createServer, request } from 'http'
|
||||
import esbuild from 'esbuild';
|
||||
|
||||
import config from '../esbuildconf.js';
|
||||
|
||||
// from https://github.com/evanw/esbuild/issues/802
|
||||
const clients = [];
|
||||
config.options.banner = {
|
||||
js: ' (() => new EventSource("/_live").onmessage = () => location.reload())();'
|
||||
};
|
||||
config.options.watch = {
|
||||
onRebuild(err, res) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
clients.forEach((c) => c.write('data: update\n\n'));
|
||||
const n = clients.length;
|
||||
clients.length = 0;
|
||||
console.log(`>>> sent reload msg to ${n} client(s)`);
|
||||
}
|
||||
};
|
||||
esbuild.build(config.options).catch(() => process.exit(1));
|
||||
|
||||
const s = {
|
||||
host: '127.0.0.1',
|
||||
servedir: config.options.outdir,
|
||||
onRequest: (req) => {
|
||||
console.log(`${req.method} ${req.path} ${req.status} ${req.timeInMS}ms`);
|
||||
}
|
||||
};
|
||||
esbuild.serve(s, {}).then(server => {
|
||||
const {host, port} = server;
|
||||
const livePort = port + 1;
|
||||
console.log(`serving content from http://${host}:${port}/`);
|
||||
console.log(`serving live reload at http://${host}:${livePort}/`);
|
||||
createServer((req, resp) => {
|
||||
const { url, method, headers } = req;
|
||||
if (url === '/_live') {
|
||||
clients.push(resp.writeHead(200, {
|
||||
'content-type': 'text/event-stream',
|
||||
'cache-control': 'no-cache',
|
||||
'connection': 'keep-alive',
|
||||
}));
|
||||
console.log(`>>> client connected; total is now ${clients.length}`);
|
||||
return;
|
||||
}
|
||||
const path = ~url.split('/').pop().indexOf('.') ? url : '/index.html';
|
||||
const proxyReq = request({hostname: host, port: port, path, method, headers}, (proxyResp) => {
|
||||
resp.writeHead(proxyResp.statusCode, proxyResp.headers);
|
||||
proxyResp.pipe(resp, {end: true});
|
||||
});
|
||||
req.pipe(proxyReq, {end: true});
|
||||
}).listen(livePort, host);
|
||||
});
|
Loading…
Reference in New Issue