This commit is contained in:
Spencer Flagg 2023-09-13 16:28:28 +02:00
parent ad30d14a66
commit 04a379ae58
20 changed files with 2111 additions and 2475 deletions

View file

@ -1,27 +1,25 @@
# What is NostriChat?
Nostri.chat is a chat widget you can easily embed in websites.
# What is DiSseNT?
It uses Nostr as the underlying protocol, which permits a few pretty cool features.
DiSseNT is my attempt at ressurecting a valuable [project](https://github.com/gab-ai-inc/gab-dissenter-extension/issues/117) to enable truely trustless and free commentary on the web. DiSseNT is a web app built on [NOSTR](https://github.com/nostr-protocol/nostr), and based on [Nostri.chat](https://github.com/pablof7z/nostr-chat-widget), that associates a set of NOSTR messages with a given URL.
## Operation Modes
### Classic chat: 1-to-1 encrypted chats
This mode implements the typical chat widget flow present in most websites. The visitor writes in the website and someone associated with the website responds.
# How do I use it?
1. Type or paste a url
No one else sees this communication
# Roadmap
- [x] basic posting, associated with a url
- [x] display meta info
- [ ] sort comments
- [ ] zap comments
- [ ] search comments
- [ ] filter comments
- [ ] chromium extension
- [ ] dark mode
- [ ] social media specific urls
- [ ] relay picker
- [ ] show dsnt'd urls
### Global chat: Topic/Website-based communication
In this mode, the user engages in a conversation around a topic and everybody connected to the same relays can see the communication happening and interact with it.
# Supporting the Roadmap
The communication can be scoped to one or multiple topics. (e.g. _#fasting_, _#bitcoin_, or your specific website).
<a href='https://ko-fi.com/O4O1OZX1V' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi2.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
When a visitor interacts with this mode, the chat widget is populated with the prior conversations that have already occurred around this topic.
> Imagine visiting a website about #fasting, and you can immediately interact with anyone interested in that topic; you can ask questions and receive immediate responses from others
# Features
- [x] NostrConnect key delegation
- [x] Ephemeral keys
- [x] Encrypted DMs mode
- [x] Tag-scoped chats mode
- [x] In-thread replies
- [ ] Root-replies mode: similar to global (publicly available) but visitor doesn't see any past history and only sees in-thread replies to the OP
<a href="https://liberapay.com/spencer.flagg/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>

347
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "dnst",
"version": "0.3.14159",
"name": "dsnt",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "dsnt",
"version": "0.3.14159",
"version": "0.1.0",
"dependencies": {
"@nostr-connect/connect": "^0.2.3",
"@nostr-dev-kit/ndk": "^0.3.32",
@ -21,6 +21,7 @@
"emoji-regex": "^10.2.1",
"eventemitter3": "^5.0.0",
"light-bolt11-decoder": "^3.0.0",
"marked": "^9.0.0",
"nostr": "^0.2.7",
"nostr-dev-kit": "file:../../nostr/ndk/nostr-dev-kit",
"nostr-tools": "^1.11.1",
@ -50,10 +51,12 @@
"rollup-plugin-svelte": "^7.1.5",
"sirv-cli": "^2.0.2",
"svelte": "^3.54.0",
"svelte-routing": "^2.3.0",
"tailwindcss": "^3.3.2",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.0"
"vite": "^4.0.0",
"vite-plugin-svelte-md": "^0.1.7"
}
},
"../../nostr/ndk/nostr-dev-kit": {
@ -3790,6 +3793,18 @@
"resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
"integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"dev": true,
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -4194,6 +4209,56 @@
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"dev": true,
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/gray-matter/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/gray-matter/node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/gray-matter/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/hard-rejection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
@ -4539,6 +4604,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -4880,6 +4954,15 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"dev": true,
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/livereload": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz",
@ -5105,15 +5188,43 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/markdown-it": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/markdown-it/node_modules/entities": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
"dev": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/marked": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-9.0.0.tgz",
"integrity": "sha512-37yoTpjU+TSXb9OBYY5n78z/CqXh76KiQj9xsKxEdztzU9fRLmbWO5YqKxgCVGKlNdexppnbKTkwB3RipVri8w==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 12"
"node": ">= 16"
}
},
"node_modules/mdn-data": {
@ -5121,6 +5232,12 @@
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
"dev": true
},
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@ -7379,6 +7496,19 @@
"rimraf": "^2.5.2"
}
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"dev": true,
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/semiver": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz",
@ -7587,6 +7717,12 @@
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz",
"integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w=="
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/stable": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
@ -7725,6 +7861,15 @@
"node": ">=4"
}
},
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@ -7958,6 +8103,12 @@
"resolved": "https://registry.npmjs.org/svelte-qr/-/svelte-qr-1.0.0.tgz",
"integrity": "sha512-7n/FPFhImPI68NCwChzYqzTbTpDhGCiFgGiCQY+IXS8sh0Xhzzd0wwQnN5n2BCJ0Uvti8s0RhKErwcw4Lp7RvQ=="
},
"node_modules/svelte-routing": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/svelte-routing/-/svelte-routing-2.3.0.tgz",
"integrity": "sha512-M4KY7YrJ9txzn1ssLUa0dfkAxg7IuNpYMMspm/KoQKh/pHMGpCTAMn1q+gSxyUZNGDX1pq12fF2VRUq4+gBfxA==",
"dev": true
},
"node_modules/svelte-scrollto": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/svelte-scrollto/-/svelte-scrollto-0.2.0.tgz",
@ -8343,6 +8494,17 @@
"typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x"
}
},
"node_modules/typedoc/node_modules/marked": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/typedoc/node_modules/minimatch": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
@ -8369,6 +8531,12 @@
"node": ">=4.2.0"
}
},
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"dev": true
},
"node_modules/unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@ -8528,6 +8696,23 @@
}
}
},
"node_modules/vite-plugin-svelte-md": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/vite-plugin-svelte-md/-/vite-plugin-svelte-md-0.1.7.tgz",
"integrity": "sha512-KtNqcuGyrr8EnTWxS+X9jCG6NnmONxYqoZJNr1VsLf+CKZrhykn+rpqxapcGr0g8KeDhYzrkkKASbQikCsQY4Q==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.0",
"gray-matter": "^4.0.3",
"markdown-it": "^13.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
},
"peerDependencies": {
"vite": "^2.0.0 || ^3.0.0 || ^4.0.0"
}
},
"node_modules/vite/node_modules/rollup": {
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz",
@ -11187,6 +11372,15 @@
}
}
},
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"dev": true,
"requires": {
"is-extendable": "^0.1.0"
}
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -11477,6 +11671,45 @@
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
},
"gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"dev": true,
"requires": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"dependencies": {
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
}
}
},
"hard-rejection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
@ -11709,6 +11942,12 @@
"has-tostringtag": "^1.0.0"
}
},
"is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"dev": true
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -11950,6 +12189,15 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"dev": true,
"requires": {
"uc.micro": "^1.0.1"
}
},
"livereload": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz",
@ -12118,16 +12366,43 @@
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="
},
"markdown-it": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
"dev": true,
"requires": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"dependencies": {
"entities": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
"dev": true
}
}
},
"marked": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-9.0.0.tgz",
"integrity": "sha512-37yoTpjU+TSXb9OBYY5n78z/CqXh76KiQj9xsKxEdztzU9fRLmbWO5YqKxgCVGKlNdexppnbKTkwB3RipVri8w=="
},
"mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
"dev": true
},
"memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@ -13685,6 +13960,16 @@
"rimraf": "^2.5.2"
}
},
"section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
}
},
"semiver": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz",
@ -13850,6 +14135,12 @@
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz",
"integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w=="
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"stable": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
@ -13953,6 +14244,12 @@
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="
},
"strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"dev": true
},
"strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@ -14082,6 +14379,12 @@
"resolved": "https://registry.npmjs.org/svelte-qr/-/svelte-qr-1.0.0.tgz",
"integrity": "sha512-7n/FPFhImPI68NCwChzYqzTbTpDhGCiFgGiCQY+IXS8sh0Xhzzd0wwQnN5n2BCJ0Uvti8s0RhKErwcw4Lp7RvQ=="
},
"svelte-routing": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/svelte-routing/-/svelte-routing-2.3.0.tgz",
"integrity": "sha512-M4KY7YrJ9txzn1ssLUa0dfkAxg7IuNpYMMspm/KoQKh/pHMGpCTAMn1q+gSxyUZNGDX1pq12fF2VRUq4+gBfxA==",
"dev": true
},
"svelte-scrollto": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/svelte-scrollto/-/svelte-scrollto-0.2.0.tgz",
@ -14360,6 +14663,11 @@
"shiki": "^0.14.1"
},
"dependencies": {
"marked": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="
},
"minimatch": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
@ -14375,6 +14683,12 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"dev": true
},
"unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@ -14473,6 +14787,17 @@
}
}
},
"vite-plugin-svelte-md": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/vite-plugin-svelte-md/-/vite-plugin-svelte-md-0.1.7.tgz",
"integrity": "sha512-KtNqcuGyrr8EnTWxS+X9jCG6NnmONxYqoZJNr1VsLf+CKZrhykn+rpqxapcGr0g8KeDhYzrkkKASbQikCsQY4Q==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^5.0.0",
"gray-matter": "^4.0.3",
"markdown-it": "^13.0.0"
}
},
"vitefu": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "dsnt",
"version": "0.3.14159",
"version": "0.1.0",
"scripts": {
"dev": "vite dev",
"build": "svelte-kit sync && svelte-package",
@ -18,10 +18,12 @@
"rollup-plugin-svelte": "^7.1.5",
"sirv-cli": "^2.0.2",
"svelte": "^3.54.0",
"svelte-routing": "^2.3.0",
"tailwindcss": "^3.3.2",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.0"
"vite": "^4.0.0",
"vite-plugin-svelte-md": "^0.1.7"
},
"type": "module",
"dependencies": {
@ -38,6 +40,7 @@
"emoji-regex": "^10.2.1",
"eventemitter3": "^5.0.0",
"light-bolt11-decoder": "^3.0.0",
"marked": "^9.0.0",
"nostr": "^0.2.7",
"nostr-dev-kit": "file:../../nostr/ndk/nostr-dev-kit",
"nostr-tools": "^1.11.1",

View file

@ -1,377 +1,657 @@
<script>
import { chatAdapter, chatData, selectedMessage, zapsPerMessage } from './lib/store';
import { onMount } from 'svelte';
import NostrNote from './NostrNote.svelte';
import * as animateScroll from "svelte-scrollto";
import {
chatAdapter,
chatData,
selectedMessage,
zapsPerMessage,
} from "./lib/store";
import { onMount, onDestroy } from "svelte";
import NostrNote from "./NostrNote.svelte";
import * as animateScroll from "svelte-scrollto";
import { browser } from '$app/environment';
let events = [];
let responseEvents = [];
let responses = {};
let profiles = {};
let events = [];
let responseEvents = [];
let responses = {};
let profiles = {};
export let websiteOwnerPubkey;
export let chatConfiguration;
let prevChatConfiguration;
export let websiteOwnerPubkey;
export let chatConfiguration;
let prevChatConfiguration;
$: {
if (chatConfiguration !== prevChatConfiguration && $chatAdapter) {
$chatAdapter.setChatConfiguration(
chatConfiguration.chatType,
chatConfiguration.chatTags,
chatConfiguration.chatReferenceTags,
chatConfiguration.chatId);
events = [];
responses = {};
rootNoteId = null;
localStorage.removeItem('rootNoteId');
export let mainElement;
// rootNoteId = localStorage.getItem('rootNoteId');
// if (rootNoteId) {
// $chatAdapter.subscribeToEventAndResponses(rootNoteId);
// }
$: {
if (chatConfiguration !== prevChatConfiguration && $chatAdapter) {
$chatAdapter.setChatConfiguration(
chatConfiguration.chatType,
chatConfiguration.chatTags,
chatConfiguration.chatReferenceTags,
chatConfiguration.chatId
);
events = [];
responses = {};
rootNoteId = null;
localStorage.removeItem("rootNoteId");
// rootNoteId = localStorage.getItem('rootNoteId');
// if (rootNoteId) {
// $chatAdapter.subscribeToEventAndResponses(rootNoteId);
// }
}
prevChatConfiguration = chatConfiguration;
}
function getEventById(eventId) {
let event = events.find((e) => e.id === eventId);
event = event || responseEvents.find((e) => e.id === eventId);
return event;
}
async function sendMessage() {
const input = document.getElementById("message-input");
const message = input.value;
input.value = "";
let extraParams = { tags: [], tagPubKeys: [] };
// if this is the rootLevel we want to tag the owner of the site's pubkey
if (!rootNoteId && websiteOwnerPubkey) {
extraParams.tagPubKeys = [websiteOwnerPubkey];
}
// if we are responding to an event, we want to tag the event and the pubkey
if ($selectedMessage) {
extraParams.tags.push(["e", $selectedMessage, "wss://nos.lol", "root"]);
extraParams.tagPubKeys.push(getEventById($selectedMessage).pubkey);
}
// if (rootNoteId) {
// // mark it as a response to the most recent event
// const mostRecentEvent = events[events.length - 1];
// // go through all the tags and add them to the new message
// if (mostRecentEvent) {
// mostRecentEvent.tags.forEach(tag => {
// if (tag[0] === 'e') {
// extraParams.tags.push(tag);
// }
// })
// extraParams.tags.push(['e', mostRecentEvent.id]);
// extraParams.tags.push(['p', mostRecentEvent.pubkey]);
// }
// }
const noteId = await $chatAdapter.send(message, extraParams);
if (!rootNoteId) {
rootNoteId = noteId;
localStorage.setItem("rootNoteId", rootNoteId);
}
}
async function inputKeyDown(event) {
autoExpandTextarea(event);
if (event.key === "Enter" && !event.ctrlKey) {
sendMessage();
event.preventDefault();
}
if (event.key === "Enter" && event.ctrlKey) {
messageInput += "\n";
}
}
function messageReceived(message) {
const messageLastEventTag = message.tags
.filter((tag) => tag[0] === "e")
.pop();
let isThread;
if (chatConfiguration.chatType === "GLOBAL") {
isThread = message.tags.filter((tag) => tag[0] === "e").length >= 1;
} else if (chatConfiguration.chatType === "GROUP") {
isThread =
message.tags.filter(
(tag) => tag[0] === "e" && tag[1] !== chatConfiguration.chatId
).length >= 1;
} else {
const pubkeysTagged = message.tags
.filter((tag) => tag[0] === "p")
.map((tag) => tag[1]);
isThread = new Set(pubkeysTagged).size >= 2;
}
if (!responses[message.id]) {
responses[message.id] = [];
}
if (isThread) {
// get the last "e" tag, which is tagging the immediate parent
const lastETag = message.tags.filter((tag) => tag[0] === "e").pop();
if (lastETag && lastETag[1]) {
// if there is one, add it to the response
if (!responses[lastETag[1]]) {
responses[lastETag[1]] = [];
}
prevChatConfiguration = chatConfiguration;
responses[lastETag[1]].push(message);
}
responseEvents.push(message);
responseEvents = responseEvents;
} else {
// insert message so that it's chronologically ordered by created_at
// let index = 0;
// while (
// index < events.length &&
// events[index].created_at < message.created_at
// ) {
// index++;
// }
let index = events.length;
while (index > 0 && events[index - 1].created_at < message.created_at) {
index--;
}
events.splice(index, 0, message);
events = events;
}
function getEventById(eventId) {
let event = events.find(e => e.id === eventId);
event = event || responseEvents.find(e => e.id === eventId);
return event;
responses = responses;
//scrollDown();
}
function scrollUp() {
animateScroll.scrollToTop({
container: mainElement,
offset: 0,
duration: 50,
});
}
function zapReceived(zap) {
const event = events.find((event) => event.id === zap.zappedEvent);
if (!event) {
return;
}
async function sendMessage() {
const input = document.getElementById('message-input');
const message = input.value;
input.value = '';
let extraParams = { tags: [], tagPubKeys: [] };
if (!$zapsPerMessage[event.id]) $zapsPerMessage[event.id] = [];
$zapsPerMessage[event.id].push(zap);
}
// if this is the rootLevel we want to tag the owner of the site's pubkey
if (!rootNoteId && websiteOwnerPubkey) { extraParams.tagPubKeys = [websiteOwnerPubkey] }
// if we are responding to an event, we want to tag the event and the pubkey
if ($selectedMessage) {
extraParams.tags.push(['e', $selectedMessage, "wss://nos.lol", "root"]);
extraParams.tagPubKeys.push(getEventById($selectedMessage).pubkey);
}
// if (rootNoteId) {
// // mark it as a response to the most recent event
// const mostRecentEvent = events[events.length - 1];
// // go through all the tags and add them to the new message
// if (mostRecentEvent) {
// mostRecentEvent.tags.forEach(tag => {
// if (tag[0] === 'e') {
// extraParams.tags.push(tag);
// }
// })
// extraParams.tags.push(['e', mostRecentEvent.id]);
// extraParams.tags.push(['p', mostRecentEvent.pubkey]);
// }
// }
const noteId = await $chatAdapter.send(message, extraParams);
if (!rootNoteId) {
rootNoteId = noteId;
localStorage.setItem('rootNoteId', rootNoteId);
}
function reactionReceived(reaction) {
const event = events.find((event) => event.id === reaction.id);
if (!event) {
return;
}
async function inputKeyDown(event) {
if (event.key === 'Enter') {
sendMessage();
event.preventDefault();
}
}
event.reactions = event.reactions || [];
event.reactions.push(reaction);
events = events;
}
function messageReceived(message) {
const messageLastEventTag = message.tags.filter(tag => tag[0] === 'e').pop();
let isThread;
let rootNoteId;
let channelMetadata = {};
if (chatConfiguration.chatType === 'GLOBAL') {
isThread = message.tags.filter(tag => tag[0] === 'e').length >= 1;
} else if (chatConfiguration.chatType === 'GROUP') {
isThread = message.tags.filter(tag => tag[0] === 'e' && tag[1] !== chatConfiguration.chatId).length >= 1;
} else {
const pubkeysTagged = message.tags.filter(tag => tag[0] === 'p').map(tag => tag[1]);
isThread = new Set(pubkeysTagged).size >= 2;
}
onMount(() => {
$chatAdapter.on("message", messageReceived);
if (!responses[message.id]) { responses[message.id] = [] };
if (isThread) {
// get the last "e" tag, which is tagging the immediate parent
const lastETag = message.tags.filter(tag => tag[0] === 'e').pop();
if (lastETag && lastETag[1]) {
// if there is one, add it to the response
if (!responses[lastETag[1]]) {
responses[lastETag[1]] = [];
}
responses[lastETag[1]].push(message);
}
responseEvents.push(message);
responseEvents = responseEvents;
} else {
// insert message so that it's chronologically ordered by created_at
let index = 0;
while (index < events.length && events[index].created_at < message.created_at) {
index++;
}
events.splice(index, 0, message);
events = events;
}
responses = responses;
scrollDown()
}
function scrollDown() {
animateScroll.scrollToBottom({
container: document.getElementById('messages-container'),
offset: 999999, // hack, oh well, browsers suck
duration: 50
})
}
function zapReceived(zap) {
const event = events.find(event => event.id === zap.zappedEvent);
if (!event) { return; }
if (!$zapsPerMessage[event.id]) $zapsPerMessage[event.id] = [];
$zapsPerMessage[event.id].push(zap);
}
function reactionReceived(reaction) {
const event = events.find(event => event.id === reaction.id);
if (!event) { return; }
event.reactions = event.reactions || [];
event.reactions.push(reaction);
events = events;
}
let rootNoteId;
let channelMetadata = {};
onMount(() => {
$chatAdapter.on('message', messageReceived);
$chatAdapter.on('connectivity', (e) => {
connectivityStatus = e;
})
$chatAdapter.on('reaction', reactionReceived);
$chatAdapter.on('zap', zapReceived);
$chatAdapter.on('deleted', (deletedEvents) => {
deletedEvents.forEach(deletedEventId => {
const index = events.findIndex(event => event.id === deletedEventId);
if (index !== -1) {
events[index].deleted = true;
events = events;
}
})
});
$chatAdapter.on('profile', ({pubkey, profile}) => {
let profiles = $chatData.profiles;
profiles[pubkey] = profile;
chatData.set({ profiles, ...$chatData })
})
$chatAdapter.on('channelMetadata', (event) => {
channelMetadata = JSON.parse(event.content);
})
$chatAdapter.on("connectivity", (e) => {
connectivityStatus = e;
});
let connectivityStatus = {};
let connectedRelays = 0;
let totalRelays = 0;
$: {
connectedRelays = Object.values(connectivityStatus).filter(status => status === 'connected').length;
totalRelays = Object.values(connectivityStatus).length;
if ($chatAdapter?.pubkey && !profiles[$chatAdapter.pubkey]) {
$chatAdapter.reqProfile($chatAdapter.pubkey)
$chatAdapter.on("reaction", reactionReceived);
$chatAdapter.on("zap", zapReceived);
$chatAdapter.on("deleted", (deletedEvents) => {
deletedEvents.forEach((deletedEventId) => {
const index = events.findIndex((event) => event.id === deletedEventId);
if (index !== -1) {
events[index].deleted = true;
events = events;
}
});
});
$chatAdapter.on("profile", ({ pubkey, profile }) => {
let profiles = $chatData.profiles;
profiles[pubkey] = profile;
chatData.set({ profiles, ...$chatData });
});
$chatAdapter.on("channelMetadata", (event) => {
channelMetadata = JSON.parse(event.content);
});
if (browser)
document.addEventListener('keydown', handleGlobalKeydown);
if (mainElement)
mainElement.addEventListener("scroll", checkVisibility);
checkVisibility(); // initial check
});
let connectivityStatus = {};
let connectedRelays = 0;
let totalRelays = 0;
$: {
connectedRelays = Object.values(connectivityStatus).filter(
(status) => status === "connected"
).length;
totalRelays = Object.values(connectivityStatus).length;
if ($chatAdapter?.pubkey && !profiles[$chatAdapter.pubkey]) {
$chatAdapter.reqProfile($chatAdapter.pubkey);
}
}
let connectedChatId;
$: if (connectedChatId !== $chatAdapter?.chatId) {
connectedChatId = $chatAdapter?.chatId;
channelMetadata = {};
}
$: profiles = $chatData.profiles;
function selectParent() {
if (chatConfiguration.chatType === "GROUP") {
$selectedMessage = null;
} else {
// get the last tagged event in the tags array of the current $selectedMessage
const lastETag = getEventById($selectedMessage)
.tags.filter((tag) => tag[0] === "e")
.pop();
const lastETagId = lastETag && lastETag[1];
$selectedMessage = lastETagId;
}
let connectedChatId;
scrollDown();
}
$: if (connectedChatId !== $chatAdapter?.chatId) {
connectedChatId = $chatAdapter?.chatId;
channelMetadata = {};
let ownName;
$: ownName = $chatAdapter?.pubkey ? pubkeyName($chatAdapter.pubkey) : "";
$: profiles = $chatData.profiles;
$: profilePicture =
(profiles[$chatAdapter.pubkey] && profiles[$chatAdapter.pubkey].picture) ||
`https://robohash.org/${$chatAdapter.pubkey.slice(0, 1)}.png?set=set1`;
function pubkeyName(pubkey) {
let name;
if (profiles[$chatAdapter.pubkey]) {
let self = profiles[$chatAdapter.pubkey];
// https://xkcd.com/927/
name = self.display_name || self.displayName || self.name || self.nip05;
}
$: profiles = $chatData.profiles;
function selectParent() {
if (chatConfiguration.chatType === 'GROUP') {
$selectedMessage = null;
} else {
// get the last tagged event in the tags array of the current $selectedMessage
const lastETag = getEventById($selectedMessage).tags.filter(tag => tag[0] === 'e').pop();
const lastETagId = lastETag && lastETag[1];
$selectedMessage = lastETagId;
}
scrollDown()
if (!name) {
name = `Anonymous [${pubkey.slice(0, 6)}]`;
}
let ownName;
$: ownName = $chatAdapter?.pubkey ? pubkeyName($chatAdapter.pubkey) : "";
return name;
}
function pubkeyName(pubkey) {
let name;
function autoExpandTextarea(event) {
//console.log('autoExpandTextarea');
const textarea = event.target;
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight - 16}px`;
}
if (profiles[$chatAdapter.pubkey]) {
let self = profiles[$chatAdapter.pubkey];
let messageInput = "";
let messageElement;
// https://xkcd.com/927/
name = self.display_name ||
self.displayName ||
self.name ||
self.nip05;
onDestroy(() => {
if (browser)
document.removeEventListener('keydown', handleGlobalKeydown);
mainElement.removeEventListener("scroll", checkVisibility);
});
}
let anchor;
if (!name) { name = `Anonymous [${pubkey.slice(0, 6)}]`; }
function isElementInViewport(el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (mainElement.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (mainElement.innerWidth || document.documentElement.clientWidth)
);
}
return name;
function checkVisibility() {
//console.log('check')
if (isElementInViewport(messageElement)) {
anchor.style.display = 'none';
} else {
anchor.style.display = 'block';
anchor.style.transform = 'translateY(6rem)';
}
}
function handleGlobalKeydown(event) {
// Check for Ctrl + /
if (event.ctrlKey && event.key === ' ') {
// Focus the input element
messageElement.focus();
}
}
</script>
<div class="
bg-purple-700 text-white
-mx-4 -mt-5 mb-3
px-4 py-3
overflow-clip
flex flex-row justify-between items-center
">
<!-- TOP -->
{#if $chatAdapter?.pubkey}
<section class="profile">
<a class="toolbar__avatar" href="nostr:{$chatAdapter.pubkey}">
<p class="hide-on-mobile">
{ownName}
</p>
<img src={profilePicture} alt="{ownName}'s avatar" />
</a>
</section>
{/if}
{#if events}
<section class="stats">
<p class="stats__count">
{events.length} comments
</p>
</section>
{/if}
{#if totalRelays}
<section class="relays">
<!-- <Relays bind:relays on:update={handleUpdate} /> -->
<div class="stats__relays">
{connectedRelays}/{totalRelays} relays
<div class="relay-dots">
{#each Array(totalRelays) as _, i}
<span
class="relay {connectedRelays > i ? 'relay--active' : ''}"
/>
{/each}
</div>
</div>
</section>
{/if}
<div class="text-lg font-semibold">
{#if $chatAdapter?.pubkey}
{ownName}
{/if}
</div>
<section class="input">
<textarea
type="text"
id="message-input"
class=""
placeholder="leave a comment"
rows="4"
on:keydown={inputKeyDown}
bind:value={messageInput}
bind:this={messageElement}
/>
<button type="button" class="btn btn--comment" on:click|preventDefault={sendMessage} disabled={!messageInput}>
<svg width="250" height="250" viewBox="0 0 250 250" fill="none" xmlns="http://www.w3.org/2000/svg">
<path vector-effect="non-scaling-stroke" d="M177.203 67.4466C175.78 68.8692 174.784 71.1454 174.784 72.4258C174.784 73.9907 171.939 75.5556 165.253 77.5473C74.9157 104.293 24.1278 118.946 21.8516 118.946C18.295 118.946 15.023 119.48 15.023 114.251C13.8849 111.264 3.7842 110.837 1.36573 113.824C0.227621 115.105 -0.199168 120.226 0.0853578 127.766L0.512147 139.574C0.512147 139.574 0.938937 141.85 7.6253 142.277C14.3117 142.704 15.4498 140.143 15.4498 137.582C15.4498 134.595 16.3033 134.595 21.5671 134.595C26.8308 134.595 50.3042 140.57 52.0114 142.277C52.4382 142.561 51.8691 145.549 51.0155 148.963C48.8816 156.788 50.1619 163.19 54.8566 167.884C58.2709 171.299 78.6145 178.696 100.523 184.387C111.62 187.232 120.298 182.111 123.712 170.445C124.708 167.173 126.13 164.47 126.984 164.47C127.838 164.47 138.934 167.458 151.595 171.156C169.947 176.42 174.784 178.412 174.784 180.261C174.784 186.378 190.433 186.343 190.433 180.83C190.433 178.341 190.433 175.709 190.433 126.059V69.296C190.433 63.4633 180.048 64.3168 177.203 67.4466ZM90.5647 153.231L116.741 160.913L115.887 165.75C114.856 172.295 110.766 176.136 106.214 176.136C103.084 176.278 73.9199 167.742 64.815 164.185C58.9823 161.909 57.5596 158.21 59.5513 150.67C60.5471 147.256 62.112 144.695 62.9656 144.98C63.8192 145.264 76.3383 148.963 90.5647 153.231Z" fill="black"/>
<path vector-effect="non-scaling-stroke" d="M222.869 85.5143C210.35 92.7697 209.212 94.1924 212.2 97.1799C214.191 99.1716 215.756 98.6025 227.422 91.9161C236.527 86.6524 240.225 83.8072 239.941 82.1C239.087 77.9744 233.966 78.9702 222.869 85.5143Z" fill="black"/>
<path vector-effect="non-scaling-stroke" d="M214.191 120.795C212.484 125.348 215.329 126.201 232.543 125.775C248.903 125.348 249.472 125.205 249.899 122.076C250.326 118.946 250.184 118.946 232.685 118.946C219.17 118.946 214.76 119.373 214.191 120.795Z" fill="black"/>
<path vector-effect="non-scaling-stroke" d="M211.204 148.252C209.923 151.524 210.635 152.235 223.296 159.491C235.104 166.32 239.514 167.031 239.514 162.478C239.514 160.629 235.957 157.641 227.422 152.805C214.334 145.407 212.484 144.838 211.204 148.252Z" fill="black"/>
</svg>
</button>
</section>
<span class="text-xs flex flex-col items-end mt-2 text-gray-200 gap-1">
<div class="flex flex-row gap-1 overflow-clip">
{#each Array(totalRelays) as _, i}
<span class="
inline-block
rounded-full
{connectedRelays > i ? 'bg-green-500' : 'bg-gray-300'}
w-2 h-2
"></span>
{/each}
</div>
<button class="btn btn--scroll-to-top" bind:this={anchor} on:click={scrollUp}>leave a comment 👆️</button>
{connectedRelays}/{totalRelays} relays
</span>
</div>
{#if channelMetadata.name}
<div class="flex flex-row gap-2 mb-3 bg-zinc-300 text-zinc-800 px-4 py-2 -mx-4 -mt-3">
{#if channelMetadata.picture}
<img src={channelMetadata.picture} class="w-12 h-12 rounded-full" />
{/if}
<div class="flex flex-col">
<div class="font-extrabold text-xl">{channelMetadata.name}</div>
{#if channelMetadata.about}
<div class="text-sm truncate font-regular">{channelMetadata.about}</div>
{/if}
</div>
</div>
{/if}
{#if $selectedMessage}
{#if !getEventById($selectedMessage)}
<h1>Couldn't find event with ID {$selectedMessage}</h1>
<section id="messages-container" class="comments">
<div class="events">
{#if $selectedMessage}
<NostrNote
event={getEventById($selectedMessage)}
{responses}
{websiteOwnerPubkey}
/>
{:else}
<div class="flex flex-row mb-3">
<a href='#' on:click|preventDefault={selectParent}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
</svg>
{#each events as event}
<NostrNote {event} {responses} {websiteOwnerPubkey} />
{#if event.deleted}
👆 deleted
{/if}
{:else}
<div class="events--empty"><svg width="5411" height="2123" viewBox="0 0 5411 2123" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3380.65 973.032C3418.5 958.188 3455.05 962.571 3463.09 983.057C3471.12 1003.56 3457.08 1044.01 3419.24 1058.85C3381.39 1073.71 3351.98 1063.54 3343.94 1043.05C3335.9 1022.57 3342.8 987.889 3380.65 973.032ZM4573.98 1400.11L4627.9 1410.25C4627.3 1343.87 4635.19 1279.91 4651.56 1218.35C4664.87 1168.39 4622.82 1178.14 4597.83 1199.7C4532.73 1255.89 4482.65 1295.84 4398.44 1323.92C4452.51 1308.23 4494.79 1292.51 4525.34 1276.73C4548.65 1304.36 4564.04 1332.94 4568.68 1362.16C4572.11 1375.77 4573.87 1388.42 4573.98 1400.11ZM4631.05 1474.38L4554.13 1459.41C4540.53 1475.44 4519.95 1487.5 4492.38 1495.55C4545.74 1499.86 4593.27 1504.8 4634.99 1510.38C4633.41 1498.3 4632.1 1486.3 4631.05 1474.38ZM4111.98 1470.93C4130.81 1468.83 4150.26 1465.16 4170.34 1459.96C4246.91 1498.24 4350.64 1523.59 4481.54 1536C4541.88 1545.67 4595.36 1552.07 4642 1555.22C4649.15 1594.76 4659.02 1635.07 4671.63 1676.15C4686.1 1718.95 4725.49 1805.03 4789.82 1934.38C4884.53 1932.78 4948.47 1949.91 4981.63 1985.77C4959.63 1925.21 4904.08 1896.91 4824.5 1898.92C4799.19 1870.42 4767.43 1797.59 4729.22 1680.4C4716.52 1642.51 4707.18 1601.54 4701.15 1557.51C4743.56 1557.7 4778.42 1554.33 4805.75 1547.35C4779.24 1537.11 4742.95 1527.96 4696.89 1519.85C4695.91 1508.99 4695.11 1497.95 4694.5 1486.73L4916.99 1530.06L4693.42 1459.91C4693.26 1454.1 4693.15 1448.24 4693.08 1442.35L4828.01 1447.91L4693.07 1422.52C4693.53 1363.01 4698.74 1299.16 4708.68 1230.93C4713.38 1198.78 4711.9 1146.9 4691.37 1120.9C4662.99 1084.95 4612.76 1079.02 4570.51 1082.47C4509.35 1087.45 4449.72 1097.2 4391.64 1111.7C4440.19 1063.71 4483.02 1007.98 4529.75 962.192C4555.13 937.327 4588.51 924.173 4609.88 965.206C4715.84 1168.84 4865.92 1345.13 5029.49 1457.19C5086.57 1410.5 5141.77 1388.92 5195.07 1392.46C5151.96 1368.34 5094.67 1371.64 5034.25 1408.11C4926.14 1352.32 4798.13 1195.33 4639.99 931.808C4613.8 888.183 4558.12 865.244 4509.68 883.117C4460.51 901.275 4315.82 985.788 4192.91 1070.24C4080.21 1023.03 3967.15 987.108 3893.34 967.221C3883.16 1009.89 3865.53 1047.66 3840.47 1080.49C3854.52 1054.98 3866.84 1024.09 3876.66 989.325C3880.67 975.103 3886.44 964.062 3878.72 950.73C3871.68 938.544 3854.39 932.05 3826.88 931.248C3731.85 921.424 3632.36 916.093 3578.95 919.712C3584.86 949.635 3581.66 985.95 3566.18 1011.73C3574.99 980.99 3574.87 955.709 3564.07 934.415C3552.16 910.948 3532.78 907.867 3509.77 901.687C3482.31 894.316 3456.79 889.891 3433.3 888.486C3562.71 629.059 3669.88 377.849 3725.05 159.076C3690.14 281.186 3515.99 712.504 3410.25 888.092C3387.96 888.719 3367.8 892.426 3349.87 899.287C3350.85 897.038 3351.72 894.618 3352.45 892.027C3410.8 512.544 3366.43 215.374 3219.34 0.491378C3369.8 271.908 3394.35 590.242 3327.45 910.229C3299.68 927.057 3278.9 953.526 3265.74 989.962C3255.13 1019.63 3247.47 1046.15 3242.75 1069.55C3219.44 1053.93 3196.62 1045.4 3174.28 1043.96C3135.32 1037.91 3103.4 1039.61 3078.51 1049.06C3040.8 1052.4 3006.51 1049.88 2975.65 1041.5C2937.13 1031.04 2930.67 1066.82 2970.77 1072.25C3011.16 1077.7 3046.4 1074.21 3076.5 1061.8C3105.03 1074.98 3137.01 1073.06 3172.44 1056.05C3188.29 1072.41 3210.55 1083.93 3239.19 1090.63C3237.54 1102.94 3236.86 1114.21 3237.15 1124.45C3231.8 1124.83 3226.59 1125.45 3221.51 1126.32C3193.98 1137.16 3172.7 1148.72 3157.69 1160.96C3134.87 1233.43 3123.91 1281.3 3124.8 1304.58C3164.41 1314.8 3176.55 1274.7 3161.23 1184.28C3170.59 1184.77 3179.31 1187.39 3185.62 1191.12C3193.88 1196.01 3199.57 1195.71 3207.37 1190.21C3219.25 1181.83 3229.39 1168.96 3237.79 1151.65L3241 1152.4C3256.75 1209.57 3319.99 1216.33 3430.76 1172.63C3472.25 1169.49 3511.98 1144.71 3549.97 1098.28C3540.59 1160.87 3555.3 1201.62 3594.12 1220.6C3605.78 1229.74 3617.35 1238.56 3628.83 1247.02C3603.06 1278.47 3584.12 1398.61 3582.72 1438C3574.41 1472.99 3558.7 1507.24 3530.65 1543.62C3491.22 1519.83 3455 1527.14 3426.96 1562.68C3471.97 1550.53 3508.83 1553.88 3537.54 1572.73C3569.07 1553.5 3597.2 1511.15 3621.92 1445.7C3638.27 1353.91 3654.57 1307 3670.82 1304.98C3675.88 1353.33 3698.91 1355.27 3716.6 1342.96C3725.79 1336.58 3732.57 1327.93 3736.73 1318.18C3740.3 1320.24 3743.87 1322.27 3747.42 1324.26C3747.01 1336.26 3751.17 1350.73 3761.93 1366.66C3795.38 1416.15 3832.07 1443.51 3872.95 1442.68C3913.73 1441.84 3930.41 1444.09 3930.49 1497.35C3930.61 1581.12 3942.1 1655.31 3964.92 1719.93C3987.99 1759.24 4020.17 1800.96 4061.44 1845.1C4117.43 1845.98 4155.67 1865.08 4176.15 1902.4C4168.52 1844.84 4134.75 1820.65 4073.79 1822.52C4048.9 1797.11 4019.1 1754.39 3996.26 1707.36C3978.13 1643.55 3971.08 1571.49 3963.24 1478.25C3963.91 1442.74 3958.22 1417.63 3945.11 1398.75C3955 1400.43 3964.79 1401.76 3974.47 1402.71C3981.98 1447.36 4024.15 1480.75 4111.98 1470.93Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2031.03 973.032C1993.18 958.188 1956.63 962.571 1948.59 983.057C1940.55 1003.55 1954.6 1044.01 1992.44 1058.85C2030.29 1073.71 2059.7 1063.54 2067.74 1043.05C2075.78 1022.56 2068.88 987.889 2031.03 973.032ZM837.693 1400.11L783.773 1410.25C784.374 1343.87 776.483 1279.91 760.114 1218.35C746.809 1168.39 788.86 1178.14 813.851 1199.7C878.945 1255.89 929.03 1295.84 1013.23 1323.92C959.172 1308.23 916.888 1292.51 886.337 1276.73C863.026 1304.36 847.633 1332.94 842.999 1362.16C839.563 1375.77 837.803 1388.42 837.693 1400.11ZM780.625 1474.38L857.543 1459.41C871.144 1475.44 891.726 1487.5 919.294 1495.55C865.94 1499.86 818.412 1504.8 776.686 1510.38C778.269 1498.3 779.58 1486.3 780.625 1474.38ZM1299.7 1470.93C1280.87 1468.83 1261.42 1465.16 1241.34 1459.96C1164.77 1498.24 1061.04 1523.59 930.134 1536C869.793 1545.67 816.322 1552.07 769.682 1555.22C762.523 1594.76 752.661 1635.07 740.049 1676.15C725.581 1718.95 686.184 1805.03 621.857 1934.38C527.15 1932.78 463.206 1949.91 430.05 1985.77C452.046 1925.21 507.6 1896.91 587.177 1898.92C612.491 1870.42 644.243 1797.59 682.454 1680.4C695.161 1642.51 704.501 1601.54 710.523 1557.51C668.116 1557.7 633.255 1554.33 605.931 1547.35C632.438 1537.11 668.731 1527.96 714.79 1519.85C715.771 1508.99 716.569 1497.95 717.179 1486.73L494.692 1530.06L718.261 1459.91C718.415 1454.1 718.53 1448.24 718.593 1442.35L583.662 1447.91L718.609 1422.52C718.151 1363.01 712.94 1299.16 702.995 1230.93C698.302 1198.78 699.777 1146.9 720.307 1120.9C748.691 1084.95 798.92 1079.02 841.172 1082.46C902.332 1087.45 961.954 1097.2 1020.03 1111.7C971.492 1063.71 928.661 1007.98 881.925 962.192C856.545 937.327 823.163 924.172 801.802 965.206C695.841 1168.84 545.755 1345.13 382.185 1457.19C325.104 1410.5 269.903 1388.92 216.604 1392.46C259.722 1368.34 317.008 1371.64 377.426 1408.11C485.537 1352.32 613.548 1195.33 771.691 931.808C797.88 888.183 853.56 865.244 901.996 883.117C951.171 901.275 1095.85 985.787 1218.77 1070.24C1331.47 1023.03 1444.53 987.108 1518.34 967.221C1528.52 1009.89 1546.14 1047.66 1571.2 1080.49C1557.16 1054.98 1544.84 1024.09 1535.01 989.325C1531.01 975.103 1525.24 964.062 1532.95 950.73C1540 938.544 1557.28 932.05 1584.8 931.247C1679.83 921.424 1779.32 916.092 1832.73 919.712C1826.82 949.635 1830.02 985.95 1845.5 1011.73C1836.68 980.99 1836.81 955.709 1847.61 934.415C1859.51 910.948 1878.9 907.866 1901.91 901.687C1929.36 894.316 1954.89 889.891 1978.37 888.486C1848.97 629.058 1741.8 377.849 1686.62 159.076C1721.54 281.185 1895.68 712.504 2001.43 888.092C2023.71 888.719 2043.88 892.426 2061.8 899.287C2060.82 897.038 2059.96 894.618 2059.23 892.027C2000.88 512.544 2045.25 215.374 2192.33 0.491134C2041.87 271.908 2017.32 590.242 2084.23 910.229C2111.99 927.057 2132.78 953.525 2145.93 989.962C2156.55 1019.63 2164.21 1046.15 2168.92 1069.55C2192.24 1053.93 2215.06 1045.4 2237.4 1043.96C2276.36 1037.91 2308.28 1039.61 2333.16 1049.06C2370.88 1052.4 2405.17 1049.88 2436.02 1041.5C2474.55 1031.04 2481 1066.82 2440.9 1072.25C2400.52 1077.7 2365.28 1074.21 2335.18 1061.8C2306.64 1074.98 2274.66 1073.06 2239.24 1056.05C2223.38 1072.41 2201.13 1083.93 2172.49 1090.63C2174.14 1102.94 2174.82 1114.21 2174.53 1124.45C2179.87 1124.83 2185.09 1125.45 2190.17 1126.32C2217.7 1137.16 2238.98 1148.72 2253.99 1160.95C2276.81 1233.43 2287.77 1281.3 2286.88 1304.58C2247.27 1314.8 2235.13 1274.7 2250.45 1184.28C2241.09 1184.77 2232.36 1187.39 2226.05 1191.12C2217.79 1196.01 2212.11 1195.71 2204.31 1190.21C2192.43 1181.83 2182.29 1168.96 2173.88 1151.65L2170.68 1152.4C2154.93 1209.57 2091.69 1216.33 1980.91 1172.63C1939.43 1169.49 1899.7 1144.71 1861.71 1098.28C1871.09 1160.87 1856.38 1201.62 1817.55 1220.6C1805.9 1229.74 1794.33 1238.56 1782.85 1247.01C1808.61 1278.47 1827.55 1398.61 1828.96 1438C1837.27 1472.99 1852.97 1507.24 1881.03 1543.62C1920.45 1519.83 1956.67 1527.14 1984.72 1562.68C1939.71 1550.53 1902.85 1553.88 1874.14 1572.73C1842.61 1553.5 1814.47 1511.15 1789.76 1445.7C1773.41 1353.91 1757.11 1307 1740.86 1304.98C1735.8 1353.33 1712.77 1355.27 1695.08 1342.96C1685.88 1336.58 1679.11 1327.93 1674.95 1318.18C1671.37 1320.24 1667.81 1322.27 1664.25 1324.26C1664.66 1336.26 1660.51 1350.73 1649.75 1366.66C1616.29 1416.15 1579.61 1443.51 1538.73 1442.68C1497.95 1441.84 1481.27 1444.09 1481.19 1497.35C1481.06 1581.12 1469.58 1655.31 1446.75 1719.93C1423.69 1759.24 1391.51 1800.96 1350.23 1845.1C1294.25 1845.98 1256.01 1865.08 1235.53 1902.4C1243.16 1844.84 1276.93 1820.65 1337.89 1822.52C1362.78 1797.11 1392.58 1754.39 1415.42 1707.36C1433.55 1643.54 1440.59 1571.49 1448.43 1478.25C1447.77 1442.74 1453.46 1417.63 1466.57 1398.75C1456.68 1400.43 1446.89 1401.76 1437.21 1402.71C1429.7 1447.36 1387.53 1480.75 1299.7 1470.93Z" fill="black"/>
</svg></div>
{/each}
{/if}
</div>
{#if channelMetadata.name}
<div class="">
{#if channelMetadata.picture}
<img src={channelMetadata.picture} class="" alt="{channelMetadata.name} thumbnail" />
{/if}
<div class="">
<div class="">{channelMetadata.name}</div>
{#if channelMetadata.about}
<div class="">{channelMetadata.about}</div>
{/if}
</div>
</div>
{/if}
{#if $selectedMessage}
{#if !getEventById($selectedMessage)}
<h1>Couldn't find event with ID {$selectedMessage}</h1>
{:else}
<div class="">
<a href="#" on:click|preventDefault={selectParent}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class=""
>
<path
vector-effect="non-scaling-stroke"
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75"
/>
</svg>
</a>
<div class="flex flex-col ml-2">
<span class="text-lg text-black overflow-hidden whitespace-nowrap text-ellipsis">
{getEventById($selectedMessage).content}
</span>
</div>
</div>
<!-- <div class="flex flex-col ml-2">
<span class="text-lg text-black overflow-hidden whitespace-nowrap text-ellipsis">
{getEventById($selectedMessage).content}
</span>
</div> -->
</div>
{/if}
{/if}
{/if}
</section>
<div id="messages-container" class="overflow-auto -mx-4 px-4" style="height: 50vh; min-height: 300px;">
<div id="messages-container-inner" class="flex flex-col gap-4">
{#if $selectedMessage}
<NostrNote event={getEventById($selectedMessage)} {responses} {websiteOwnerPubkey} />
{:else}
{#each events as event}
<NostrNote {event} {responses} {websiteOwnerPubkey} />
{#if event.deleted}
👆 deleted
{/if}
{:else}
<p>no comments</p>
{/each}
{/if}
</div>
</div>
<div class="flex flex-col">
<!-- <div class="
border-y border-y-slate-200
-mx-4 my-2 bg-slate-100 text-black text-sm
px-4 py-2
">
{#if chatConfiguration.chatType === 'DM'}
<b>Encrypted chat:</b>
only your chat partner can see these messages.
{:else if chatConfiguration.chatType === 'GROUP'}
<b>Public chat:</b>
anyone can see these messages.
{:else}
<b>Public notes:</b>
your followers see your messages on their timeline
{/if}
</div> -->
<div class="flex flex-row gap-2 -mx-1">
<textarea
type="text"
id="message-input"
class="
-mb-2
p-2
w-full
resize-none
rounded-xl
text-gray-600
border
" placeholder="leave a comment"
rows=1
on:keydown={inputKeyDown}
></textarea>
<button type="button" class="inline-flex items-center rounded-full border border-transparent bg-purple-700 p-3 text-white shadow-sm hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" on:click|preventDefault={sendMessage}>
<!-- Heroicon name: outline/plus -->
<svg aria-hidden="true" class="w-6 h-6 rotate-90" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path></svg>
</button>
</div>
</div>
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
section.input {
display: flex;
}
section.input textarea {
height: auto;
padding: 1rem;
flex-grow: 1;
}
section.input button {
padding: 1rem;
}
.events {
display: flex;
flex-direction: column;
}
section.profile {
position: fixed;
top: 1rem;
right: 1rem;
}
section.relays {
position: fixed;
bottom: 1rem;
right: 2rem;
}
.toolbar__avatar{
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1rem;
color: var(--c-bright);
}
.toolbar__avatar img{
width: 2rem;
border-radius: 2rem;
outline: 1px solid var(--c-lines);
}
section.stats {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.stats__count{
font-family: 'Barlow Condensed', sans-serif;
font-size: 55px;
font-weight: 100;
line-height: .8;
color: var(--c-lines);
}
.relay-dots {
display: flex;
gap:.5rem;
}
.relay{
width: 11px;
height: 11px;
border-radius: 11px;
border: 1px solid var(--c-lines);
}
.relay--active{
border-color: var(--c-marker);
background-color: var(--c-marker);
}
p {
margin: 0;
}
section.input textarea{
resize: none;
background-color: var(--c-lines);
border: none;
}
section.input textarea::placeholder{
color: var(--c-bright);
}
section.input textarea:focus{
outline: none;
background-color: var(--c-bright);
}
section.input textarea:focus + button{
border-color: var(--c-bright);
}
section.input textarea:focus + button{
--color: var(--c-bright);
}
section.input textarea:focus + button svg path{
stroke: var(--c-bright);
}
.btn--comment {
--color: var(--c-lines);
padding: 1rem;
}
.btn--comment:disabled {
cursor: not-allowed;
}
.btn--comment:disabled svg path{
fill:none;
stroke: var(--c-lines);
}
.btn--comment svg{
width: 3rem;
height: 3rem;
}
.btn--comment svg path{
fill: var(--color);
}
.events--empty {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100%;
justify-content: center;
}
.events--empty svg{
max-width: 50%;
height: auto;
}
.events--empty svg path{
fill: rgba(0,0,0,.3);
}
.comments {
flex-grow: 1;
}
.events {
min-height: 100%;
}
.stats__relays{
display: flex;
flex-direction: column;
gap: .5rem;
align-items: end;
color: var(--c-lines);
}
.btn.btn--scroll-to-top {
transition: all .3s;
transform: translateY(-3rem);
position: sticky;
top: 0;
left: 5rem;
/* z-index: 2; */
background: var(--c-bright);
padding: 0.25em 0.75em;
/* display: inline-block; */
align-self: center;
border-radius: 2rem;
font-size: 1.5rem;
color: black;
font-weight: 300;
text-decoration: none;
z-index: 1;
}
</style>

View file

@ -1,480 +0,0 @@
<script>
import {
chatAdapter,
chatData,
selectedMessage,
zapsPerMessage,
} from "./lib/store";
import { onMount } from "svelte";
import NostrNote from "./NostrNote2.svelte";
import * as animateScroll from "svelte-scrollto";
let events = [];
let responseEvents = [];
let responses = {};
let profiles = {};
export let websiteOwnerPubkey;
export let chatConfiguration;
let prevChatConfiguration;
$: {
if (chatConfiguration !== prevChatConfiguration && $chatAdapter) {
$chatAdapter.setChatConfiguration(
chatConfiguration.chatType,
chatConfiguration.chatTags,
chatConfiguration.chatReferenceTags,
chatConfiguration.chatId
);
events = [];
responses = {};
rootNoteId = null;
localStorage.removeItem("rootNoteId");
// rootNoteId = localStorage.getItem('rootNoteId');
// if (rootNoteId) {
// $chatAdapter.subscribeToEventAndResponses(rootNoteId);
// }
}
prevChatConfiguration = chatConfiguration;
}
function getEventById(eventId) {
let event = events.find((e) => e.id === eventId);
event = event || responseEvents.find((e) => e.id === eventId);
return event;
}
async function sendMessage() {
const input = document.getElementById("message-input");
const message = input.value;
input.value = "";
let extraParams = { tags: [], tagPubKeys: [] };
// if this is the rootLevel we want to tag the owner of the site's pubkey
if (!rootNoteId && websiteOwnerPubkey) {
extraParams.tagPubKeys = [websiteOwnerPubkey];
}
// if we are responding to an event, we want to tag the event and the pubkey
if ($selectedMessage) {
extraParams.tags.push(["e", $selectedMessage, "wss://nos.lol", "root"]);
extraParams.tagPubKeys.push(getEventById($selectedMessage).pubkey);
}
// if (rootNoteId) {
// // mark it as a response to the most recent event
// const mostRecentEvent = events[events.length - 1];
// // go through all the tags and add them to the new message
// if (mostRecentEvent) {
// mostRecentEvent.tags.forEach(tag => {
// if (tag[0] === 'e') {
// extraParams.tags.push(tag);
// }
// })
// extraParams.tags.push(['e', mostRecentEvent.id]);
// extraParams.tags.push(['p', mostRecentEvent.pubkey]);
// }
// }
const noteId = await $chatAdapter.send(message, extraParams);
if (!rootNoteId) {
rootNoteId = noteId;
localStorage.setItem("rootNoteId", rootNoteId);
}
}
async function inputKeyDown(event) {
if (event.key === "Enter") {
sendMessage();
event.preventDefault();
}
}
function messageReceived(message) {
const messageLastEventTag = message.tags
.filter((tag) => tag[0] === "e")
.pop();
let isThread;
if (chatConfiguration.chatType === "GLOBAL") {
isThread = message.tags.filter((tag) => tag[0] === "e").length >= 1;
} else if (chatConfiguration.chatType === "GROUP") {
isThread =
message.tags.filter(
(tag) => tag[0] === "e" && tag[1] !== chatConfiguration.chatId
).length >= 1;
} else {
const pubkeysTagged = message.tags
.filter((tag) => tag[0] === "p")
.map((tag) => tag[1]);
isThread = new Set(pubkeysTagged).size >= 2;
}
if (!responses[message.id]) {
responses[message.id] = [];
}
if (isThread) {
// get the last "e" tag, which is tagging the immediate parent
const lastETag = message.tags.filter((tag) => tag[0] === "e").pop();
if (lastETag && lastETag[1]) {
// if there is one, add it to the response
if (!responses[lastETag[1]]) {
responses[lastETag[1]] = [];
}
responses[lastETag[1]].push(message);
}
responseEvents.push(message);
responseEvents = responseEvents;
} else {
// insert message so that it's chronologically ordered by created_at
let index = 0;
while (
index < events.length &&
events[index].created_at < message.created_at
) {
index++;
}
events.splice(index, 0, message);
events = events;
}
responses = responses;
scrollDown();
}
function scrollDown() {
animateScroll.scrollToBottom({
container: document.getElementById("messages-container"),
offset: 999999, // hack, oh well, browsers suck
duration: 50,
});
}
function zapReceived(zap) {
const event = events.find((event) => event.id === zap.zappedEvent);
if (!event) {
return;
}
if (!$zapsPerMessage[event.id]) $zapsPerMessage[event.id] = [];
$zapsPerMessage[event.id].push(zap);
}
function reactionReceived(reaction) {
const event = events.find((event) => event.id === reaction.id);
if (!event) {
return;
}
event.reactions = event.reactions || [];
event.reactions.push(reaction);
events = events;
}
let rootNoteId;
let channelMetadata = {};
onMount(() => {
$chatAdapter.on("message", messageReceived);
$chatAdapter.on("connectivity", (e) => {
connectivityStatus = e;
});
$chatAdapter.on("reaction", reactionReceived);
$chatAdapter.on("zap", zapReceived);
$chatAdapter.on("deleted", (deletedEvents) => {
deletedEvents.forEach((deletedEventId) => {
const index = events.findIndex((event) => event.id === deletedEventId);
if (index !== -1) {
events[index].deleted = true;
events = events;
}
});
});
$chatAdapter.on("profile", ({ pubkey, profile }) => {
let profiles = $chatData.profiles;
profiles[pubkey] = profile;
chatData.set({ profiles, ...$chatData });
});
$chatAdapter.on("channelMetadata", (event) => {
channelMetadata = JSON.parse(event.content);
});
});
let connectivityStatus = {};
let connectedRelays = 0;
let totalRelays = 0;
$: {
connectedRelays = Object.values(connectivityStatus).filter(
(status) => status === "connected"
).length;
totalRelays = Object.values(connectivityStatus).length;
if ($chatAdapter?.pubkey && !profiles[$chatAdapter.pubkey]) {
$chatAdapter.reqProfile($chatAdapter.pubkey);
}
}
let connectedChatId;
$: if (connectedChatId !== $chatAdapter?.chatId) {
connectedChatId = $chatAdapter?.chatId;
channelMetadata = {};
}
$: profiles = $chatData.profiles;
function selectParent() {
if (chatConfiguration.chatType === "GROUP") {
$selectedMessage = null;
} else {
// get the last tagged event in the tags array of the current $selectedMessage
const lastETag = getEventById($selectedMessage)
.tags.filter((tag) => tag[0] === "e")
.pop();
const lastETagId = lastETag && lastETag[1];
$selectedMessage = lastETagId;
}
scrollDown();
}
let ownName;
$: ownName = $chatAdapter?.pubkey ? pubkeyName($chatAdapter.pubkey) : "";
$: profiles = $chatData.profiles;
$: profilePicture =
(profiles[$chatAdapter.pubkey] && profiles[$chatAdapter.pubkey].picture) ||
`https://robohash.org/${$chatAdapter.pubkey.slice(0, 1)}.png?set=set1`;
function pubkeyName(pubkey) {
let name;
if (profiles[$chatAdapter.pubkey]) {
let self = profiles[$chatAdapter.pubkey];
// https://xkcd.com/927/
name = self.display_name || self.displayName || self.name || self.nip05;
}
if (!name) {
name = `Anonymous [${pubkey.slice(0, 6)}]`;
}
return name;
}
</script>
<!-- TOP -->
<div class="toolbar">
{#if $chatAdapter?.pubkey}
<a class="toolbar__avatar" href="https://iris.to/{$chatAdapter.pubkey}">
<p class="">
{ownName}
</p>
<img src={profilePicture} alt="{ownName}'s avatar" />
</a>
{/if}
<div class="toolbar__stats">
{#if events}
<p class="stats__count">
{events.length}
</p>
{/if}
<!-- <Relays bind:relays on:update={handleUpdate} /> -->
{#if totalRelays}
<div class="stats__relays">
{connectedRelays}/{totalRelays} relays
<div class="relay-dots">
{#each Array(totalRelays) as _, i}
<span
class="relay {connectedRelays > i ? 'relay--active' : ''}"
/>
{/each}
</div>
</div>
{/if}
</div>
</div>
<!-- BOTTOM -->
<div id="messages-container" class="content content--scrolling">
<div class="events">
{#if $selectedMessage}
<NostrNote
event={getEventById($selectedMessage)}
{responses}
{websiteOwnerPubkey}
/>
{:else}
{#each events as event}
<NostrNote {event} {responses} {websiteOwnerPubkey} />
{#if event.deleted}
👆 deleted
{/if}
{:else}
<p>no comments</p>
{/each}
{/if}
</div>
{#if channelMetadata.name}
<div class="">
{#if channelMetadata.picture}
<img src={channelMetadata.picture} class="" />
{/if}
<div class="">
<div class="">{channelMetadata.name}</div>
{#if channelMetadata.about}
<div class="">{channelMetadata.about}</div>
{/if}
</div>
</div>
{/if}
{#if $selectedMessage}
{#if !getEventById($selectedMessage)}
<h1>Couldn't find event with ID {$selectedMessage}</h1>
{:else}
<div class="">
<a href="#" on:click|preventDefault={selectParent}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class=""
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75"
/>
</svg>
</a>
<!-- <div class="flex flex-col ml-2">
<span class="text-lg text-black overflow-hidden whitespace-nowrap text-ellipsis">
{getEventById($selectedMessage).content}
</span>
</div> -->
</div>
{/if}
{/if}
</div>
<!-- MESSAGE INPUT -->
<div class="message-input">
<textarea
type="text"
id="message-input"
class=""
placeholder="leave a comment"
rows="1"
on:keydown={inputKeyDown}
/>
<button type="button" class="" on:click|preventDefault={sendMessage}>
<!-- Heroicon name: outline/plus -->
<svg
aria-hidden="true"
class="w-6 h-6 rotate-90"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
><path
d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"
/></svg
>
</button>
</div>
<style>
.content {
flex-grow: 1;
overflow-y: auto;
}
.message-input {
display: flex;
}
.message-input textarea {
height: auto;
padding: 1rem;
flex-grow: 1;
}
.message-input button {
padding: 2rem;
}
.events {
display: flex;
flex-direction: column;
}
.toolbar__avatar{
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1rem;
color: var(--c-bright);
}
.toolbar__avatar img{
width: 2rem;
border-radius: 2rem;
outline: 1px solid var(--c-lines);
}
.toolbar__stats {
display: flex;
justify-content: space-between;
flex-grow: 1;
align-items: flex-end;
}
.stats__count{
font-family: 'Barlow Condensed', sans-serif;
font-size: 55px;
font-weight: 100;
line-height: .8;
}
.relay-dots {
display: flex;
gap:.5rem;
}
.relay{
width: 13px;
height: 13px;
border-radius: 13px;
border: 1px solid var(--c-lines);
}
.relay--active{
border-color: var(--c-bright);
background-color: var(--c-marker);
}
p {
margin: 0;
}
</style>

View file

@ -107,9 +107,13 @@
</script>
<h1 class="">
How would you like to connect?
Welcome to DiSseNT
</h1>
<h2 class="">
How would you like to connect?
</h2>
{#if publicKey}
<p class="">
Nostr Connect is a WIP, not fully implemented yet!
@ -135,21 +139,21 @@
Cancel
</button>
{:else if !publicKey}
<div class="">
<div class="btn-list">
{#if hasNostrNip07}
<button class="" on:click|preventDefault={useNip07}>
Browser Extension (NIP-07)
<button class="btn" on:click|preventDefault={useNip07}>
Browser Extension <span class="btn__subheading">(NIP-07)</span>
</button>
{/if}
<button class="" on:click|preventDefault={useNip46}>
Nostr Connect (NIP-46)
<button class="btn" on:click|preventDefault={useNip46}>
Nostr Connect<span class="btn__subheading">(NIP-46)</span>
</button>
<button class="" on:click|preventDefault={useDiscardableKeys}>
<button class="btn" on:click|preventDefault={useDiscardableKeys}>
Anonymous
<span class="">
<span class="btn__subheading">
(Ephemeral Keys)
</span>
</button>

View file

@ -1,8 +1,8 @@
<script>
// Imports: Library and Svelte Store
import { onMount } from 'svelte';
export let url;
// Local Variables
let title = null;
let description = null;
let thumbnail = null;
@ -10,29 +10,36 @@
let isLoading = true;
let isError = false;
let urlAbbr;
let imageLoaded = false;
let imageError = false;
let showContent = true;
function onImageLoad() {
imageLoaded = true;
const threshold = 100;
const buffer = 0; // Adjust this value as needed
export let scrollPosition;
export let url;
// Reactive Statements
$: {
if (scrollPosition <= threshold - buffer) {
showContent = true;
} else if (scrollPosition >= threshold + buffer) {
showContent = false;
}
}
function onImageError() {
imageError = true;
}
$: {
const cleanedUrl = typeof url === 'string' ? url.replace(/^https?:\/\//, '') : '';
urlAbbr = cleanedUrl.length > 20 ? cleanedUrl.substring(0, 20) + '...' : cleanedUrl;
}
$: fetchMetaData(url);
// Lifecycle Methods
onMount(async () => {
fetchMetaData(url);
});
// Helper Functions
async function fetchMetaData(url){
isError = false;
try {
@ -61,7 +68,6 @@
isLoading = false;
}
}
function getMetaContent(doc, tagNames) {
for (const tagName of tagNames) {
const element = doc.querySelector(`meta[name="${tagName}"], meta[property="${tagName}"]`);
@ -71,41 +77,52 @@
}
return null;
}
// Event Handlers and UI Logic
function onImageLoad() {
imageLoaded = true;
}
function onImageError() {
imageError = true;
}
</script>
{#if isLoading}
<div class="metadata__text">
<h1>{urlAbbr}</h1>
<h1 class="{showContent ? '' : 'content--shown'}">{urlAbbr}</h1>
<div class="metadata__content{showContent ? '' : ' content--hidden'}">
<p>Loading...</p>
</div>
{:else if isError}
<div class="metadata__text">
<h1>{urlAbbr}</h1>
<h1 class="{showContent ? '' : 'content--shown'}">{urlAbbr}</h1>
<div class="metadata__content{showContent ? '' : ' content--hidden'}">
<p>There was a problem displaying info from this website.</p>
</div>
{:else if !title && !description && !thumbnail && !publishDate}
<div class="metadata__text">
<h1>{urlAbbr}</h1>
<h1 class="{showContent ? '' : 'content--shown'}">{urlAbbr}</h1>
<div class="metadata__content{showContent ? '' : ' content--hidden'}">
<p>No data found.</p>
</div>
{:else}
<div class="metadata__text">
{#if title}
<h1>{title}</h1>
{/if}
<a href="{url}" target="_blank" rel="noreferrer">{urlAbbr}</a>
{#if description}
<p>{description}</p>
{/if}
{#if publishDate}
<p>Published on: {new Date(publishDate).toLocaleDateString()}</p>
{/if}
</div>
<div class="metadata__thumbnail">
{#if thumbnail}
<img src="{thumbnail}" alt="Thumbnail for {title}" on:load={onImageLoad} on:error={onImageError} />
{imageLoaded}
{imageError}
{/if}
{#if title}
<h1 class="{showContent ? '' : 'content--shown'}">{title}</h1>
{/if}
<div class="metadata__content{showContent ? '' : ' content--hidden'}">
<div class="metadata__text">
<a href="{url}" target="_blank" rel="noreferrer">{urlAbbr}</a>
{#if description}
<p>{description}</p>
{/if}
{#if publishDate}
<p>Published on: {new Date(publishDate).toLocaleDateString()}</p>
{/if}
</div>
<div class="metadata__thumbnail">
{#if thumbnail}
<img src="{thumbnail}" alt="Thumbnail for {title}" on:load={onImageLoad} on:error={onImageError} />
<!-- {imageLoaded}
{imageError} -->
{/if}
</div>
</div>
{/if}
@ -118,7 +135,14 @@ h1 {
font-size: 1.5rem;
color: var(--c-3);
font-weight: 500;
margin: 0;
margin: -1rem -1rem 0 -1rem;
padding: 1rem 1rem 1px 1rem;
background: linear-gradient(to bottom, #381100 0%, #3f1300 100%);
z-index: 1;
}
h1.content--shown{
box-shadow: #3f1300 0 0 10px 10px;
}
a{
@ -140,6 +164,21 @@ p {
overflow-wrap: anywhere;
}
.metadata__content {
display: flex;
gap: 1rem;
transition: all .2s ease-in-out;
opacity: 1;
}
.metadata__content.content--hidden {
overflow:hidden;
opacity: 0;
transform: translateY(-300px);
}
.metadata__text {
flex-grow: 1;
}
@ -153,7 +192,7 @@ p {
}
img{
max-height: 100%;
max-height: 6rem;
outline: 1px solid rgba(255, 255, 255, .5);
border-radius: .5rem;
}

View file

@ -1,207 +1,331 @@
<script>
import { afterUpdate, onMount } from 'svelte';
import { selectedMessage, zappingMessage, zapsPerMessage } from './lib/store';
import { chatData, chatAdapter } from './lib/store';
import { nip19 } from 'nostr-tools';
import ZapAmountButton from './ZapAmountButton.svelte';
// import { prettifyContent } from '$lib/utils';
export let event;
export let responses;
export let websiteOwnerPubkey;
import { afterUpdate, onMount } from "svelte";
import { selectedMessage, zappingMessage, zapsPerMessage } from "./lib/store";
import { chatData, chatAdapter } from "./lib/store";
import { nip19 } from "nostr-tools";
import ZapAmountButton from "./ZapAmountButton.svelte";
// import { prettifyContent } from '$lib/utils';
let profiles = {};
let profilePicture;
let npub;
let zappingIt;
let hovering;
let mobilePR;
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
let zappedAmount = 0;
dayjs.extend(relativeTime);
function selectMessage() {
if ($selectedMessage === event.id) {
$selectedMessage = null;
} else {
$selectedMessage = event.id;
}
export let event;
export let responses;
export let websiteOwnerPubkey;
let profiles = {};
let profilePicture;
let npub;
let zappingIt;
let hovering;
let mobilePR;
let relativeTimeFromNow;
let zappedAmount = 0;
function selectMessage() {
if ($selectedMessage === event.id) {
$selectedMessage = null;
} else {
$selectedMessage = event.id;
}
}
// delay-fetch responses
onMount(() => {
$chatAdapter.delayedSubscribe(
{kinds: [1, 42, 9735], '#e': [event.id]}
, 'responses', 500)
})
// delay-fetch responses
onMount(() => {
$chatAdapter.delayedSubscribe(
{ kinds: [1, 42, 9735], "#e": [event.id] },
"responses",
500
);
});
const byWebsiteOwner = !!websiteOwnerPubkey === event.pubkey;
const byWebsiteOwner = !!websiteOwnerPubkey === event.pubkey;
$: profiles = $chatData.profiles;
$: displayName = profiles[event.pubkey] && profiles[event.pubkey].display_name || `[${event.pubkey.slice(0, 6)}]`;
// $: nip05 = profiles[event.pubkey] && profiles[event.pubkey].nip05;
$: zappingIt = $zappingMessage === event.id;
$: {
try {
npub = nip19.npubEncode(event.pubkey);
} catch (e) {
npub = event.pubkey;
}
$: profiles = $chatData.profiles;
$: displayName =
(profiles[event.pubkey] && profiles[event.pubkey].display_name) ||
`[${event.pubkey.slice(0, 6)}]`;
// $: nip05 = profiles[event.pubkey] && profiles[event.pubkey].nip05;
$: zappingIt = $zappingMessage === event.id;
$: {
try {
npub = nip19.npubEncode(event.pubkey);
} catch (e) {
npub = event.pubkey;
}
}
$chatAdapter.on('zap', () => {
zappedAmount = $zapsPerMessage[event.id]?.reduce((acc, zap) => acc + zap.amount, 0) || 0;
});
$chatAdapter.on("zap", () => {
zappedAmount =
$zapsPerMessage[event.id]?.reduce((acc, zap) => acc + zap.amount, 0) || 0;
});
$: {
zappedAmount = $zapsPerMessage[event.id]?.reduce((acc, zap) => acc + zap.amount, 0) || 0;
$: {
zappedAmount =
$zapsPerMessage[event.id]?.reduce((acc, zap) => acc + zap.amount, 0) || 0;
}
afterUpdate(() => {
zappedAmount =
$zapsPerMessage[event.id]?.reduce((acc, zap) => acc + zap.amount, 0) || 0;
});
$: profilePicture =
(profiles[event.pubkey] && profiles[event.pubkey].picture) ||
`https://robohash.org/${event.pubkey.slice(0, 1)}.png?set=set1`;
// const repliedIds = event.tags.filter(e => e[0] === 'e').map(e => e[1]);
let timestamp = new Date(event.created_at * 1000);
$: {
const now = dayjs();
const then = dayjs(timestamp);
const diffInSeconds = now.diff(then, 'second');
const diffInMinutes = now.diff(then, 'minute');
const diffInHours = now.diff(then, 'hour');
const diffInDays = now.diff(then, 'day');
const diffInMonths = now.diff(then, 'month');
const diffInYears = now.diff(then, 'year');
if (diffInSeconds < 10) {
relativeTimeFromNow = "Now";
} else if (diffInSeconds < 60) {
relativeTimeFromNow = `${diffInSeconds}s`;
} else if (diffInMinutes < 60) {
relativeTimeFromNow = `${diffInMinutes}m`;
} else if (diffInHours < 24) {
relativeTimeFromNow = `${diffInHours}h`;
} else if (diffInDays < 365) {
relativeTimeFromNow = then.format('MMM D');
} else {
if (diffInYears >= 1 && diffInMonths >= 6) {
relativeTimeFromNow = then.format('MMM D, YYYY');
} else {
relativeTimeFromNow = then.format('MMM D');
}
}
}
afterUpdate(() => {
zappedAmount = $zapsPerMessage[event.id]?.reduce((acc, zap) => acc + zap.amount, 0) || 0;
});
$: profilePicture = profiles[event.pubkey] && profiles[event.pubkey].picture || `https://robohash.org/${event.pubkey.slice(0, 1)}.png?set=set1`;
// const repliedIds = event.tags.filter(e => e[0] === 'e').map(e => e[1]);
let timestamp = new Date(event.created_at * 1000);
</script>
<div
class="
flex flex-col gap-4
p-2-lg mb-3
text-wrap
relative
"
on:mouseenter={() => (hovering = true)}
on:mouseleave={() => (hovering = false)}
>
<div class="flex flex-row gap-3">
<div class="min-w-fit flex flex-col gap-2">
<a href={`nostr:${npub}`}>
<img src="{profilePicture}" class="
block w-8 h-8 rounded-full
{byWebsiteOwner ? 'ring-purple-700 ring-4' : ''}
" alt="" />
</a>
<button
class="
rounded-full
{zappedAmount > 0 ? 'opacity-100 text-base' : 'bg-orange-500 opacity-10 text-xl'}
w-8 h-8
flex items-center
justify-center
hover:opacity-100
"
on:click|preventDefault={() => $zappingMessage = $zappingMessage === event.id ? null : event.id}
>
{#if zappedAmount > 0}
<p class="flex flex-col items-center my-4">
⚡️
<span class="text-orange-500 font-semibold">
{zappedAmount/1000}
</span>
</p>
{:else}
⚡️
{/if}
</button>
<div class="
{zappingIt ?
'w-full rounded-full bg-white drop-shadow-xl justify-between border-2 border-gray-200' :
' rounded-full w-8 h-8 justify-center'
}
flex items-center ml-5 mt-10 z-10">
{#if zappingIt}
{#if mobilePR}
<div class="flex flex-col gap-3 w-full">
<a href={`lightning:${mobilePR}`} class="text-center w-full p-3 bg-black text-white rounded-t-xl">Open in wallet</a>
<button class="bg-white rounder-b-xl p-3" on:click={() => { $zappingMessage = null; }}>
Cancel
</button>
</div>
{:else}
<div class="flex flex-row items-stretch justify-between w-full">
<div class="flex flex-col hover:bg-orange-500 text-white rounded-full w-12 h-12 items-center justify-center cursor-pointer">
<ZapAmountButton icon="👍" amount={500} {event} bind:mobilePR={mobilePR} />
</div>
<div class="flex flex-col hover:bg-orange-500 text-white rounded-full w-12 h-12 items-center justify-center cursor-pointer">
<ZapAmountButton icon="🤙" amount={2500} amountDisplay={'2.5k'} {event} bind:mobilePR={mobilePR} />
</div>
<div class="flex flex-col hover:bg-orange-500 text-white rounded-full w-12 h-12 items-center justify-center cursor-pointer">
<ZapAmountButton icon="🙌" amount={5000} amountDisplay={'5k'} {event} bind:mobilePR={mobilePR} />
</div>
<div class="flex flex-col hover:bg-orange-500 text-white rounded-full w-12 h-12 items-center justify-center cursor-pointer">
<ZapAmountButton icon="🧡" amount={10000} amountDisplay={'10k'} {event} bind:mobilePR={mobilePR} />
</div>
<div class="flex flex-col hover:bg-orange-500 text-white rounded-full w-12 h-12 items-center justify-center cursor-pointer">
<ZapAmountButton icon="🤯" amount={100000} amountDisplay={'100k'} {event} bind:mobilePR={mobilePR} />
</div>
<div class="flex flex-col hover:bg-orange-500 text-white rounded-full w-12 h-12 items-center justify-center cursor-pointer">
<ZapAmountButton icon="😎" amount={1000000} amountDisplay={'1M'} {event} bind:mobilePR={mobilePR} />
</div>
</div>
{/if}
{/if}
<article class="event">
<div class="event__content">
<!-- AVATAR-->
<div class="event__avatar">
<a href={`nostr:${npub}`}
><img
src={profilePicture}
alt="{displayName}'s avatar"
/></a
>
<!-- <button
class="zap-btn {zappedAmount > 0
? 'zap-btn--zapped'
: ''}"
on:click|preventDefault={() =>
($zappingMessage = $zappingMessage === event.id ? null : event.id)}
>
{#if zappedAmount > 0}
<p>
⚡️
<span>
{zappedAmount / 1000}
</span>
</p>
{:else}
⚡️
{/if}
</button> -->
<!-- <div
class="
{zappingIt
? 'w-full rounded-full bg-white drop-shadow-xl justify-between border-2 border-white/50'
: ' rounded-full w-8 h-8 justify-center'}
flex items-center ml-5 mt-10 z-10"
>
{#if zappingIt}
{#if mobilePR}
<div class="">
<a
href={`lightning:${mobilePR}`}
class=""
>Open in wallet</a
>
<button
class=""
on:click={() => {
$zappingMessage = null;
}}
>
Cancel
</button>
</div>
<!-- <span class="text-base font-semibold text-clip">{displayName}</span>
{#if nip05}
<span class="text-sm text-gray-400">{nip05}</span>
{/if} -->
</div>
<div class="w-full overflow-hidden">
<div class="flex flex-row justify-between text-center overflow-clip text-clip w-full">
{:else}
<div>
<div>
<ZapAmountButton icon="👍" amount={500} {event} bind:mobilePR />
</div>
<div>
<ZapAmountButton
icon="🤙"
amount={2500}
amountDisplay={"2.5k"}
{event}
bind:mobilePR
/>
</div>
<div >
<ZapAmountButton
icon="🙌"
amount={5000}
amountDisplay={"5k"}
{event}
bind:mobilePR
/>
</div>
<div>
<ZapAmountButton
icon="🧡"
amount={10000}
amountDisplay={"10k"}
{event}
bind:mobilePR
/>
</div>
<div>
<ZapAmountButton
icon="🤯"
amount={100000}
amountDisplay={"100k"}
{event}
bind:mobilePR
/>
</div>
<div>
<ZapAmountButton
icon="😎"
amount={1000000}
amountDisplay={"1M"}
{event}
bind:mobilePR
/>
</div>
</div>
<div class="
max-h-64 text-base
cursor-pointer
border border-slate-200
{$selectedMessage === event.id ? 'bg-purple-700 text-white' : 'bg-white text-gray-900 hover:bg-slate-100'}
p-4 py-2 overflow-auto rounded-2xl
shadow-sm
" on:click|preventDefault={()=>{selectMessage(event.id)}}
on:keydown|preventDefault={()=>{selectMessage(event.id)}}
on:keyup|preventDefault={()=>{selectMessage(event.id)}}
>
{event.content}
</div>
<div class="flex flex-row-reverse justify-between mt-1 overflow-clip items-center relative">
<div class="text-xs text-gray-400 text-ellipsis overflow-clip whitespace-nowrap">
<span class="py-2">
{timestamp.toLocaleString()}
</span>
</div>
{#if byWebsiteOwner}
<div class="text-purple-500 text-xs">
Website owner
</div>
{:else}
<div class="text-xs text-gray-400">
{displayName}
</div>
{/if}
</div>
</div>
{/if}
{/if}
</div> -->
</div>
</div>
{#if responses[event.id].length > 0}
<div class="pl-5 border-l border-l-gray-400 flex flex-col gap-4">
{#each responses[event.id] as response}
<svelte:self {websiteOwnerPubkey} event={response} {responses} />
{/each}
<!-- TEXT-->
<div class="event__text">
<header>
<h1>
{displayName}
</h1>
<span title="{timestamp.toLocaleString()}">
{relativeTimeFromNow}
</span>
</header>
<p class="event__message">
{event.content}
</p>
</div>
{/if}
</div>
{#if responses[event.id].length > 0}
<div class="event__responses">
{#each responses[event.id] as response}
<svelte:self {websiteOwnerPubkey} event={response} {responses} />
{/each}
</div>
{/if}
</article>
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
.event{
display: flex;
flex-direction: column;
padding: 1rem 1rem 1rem 1rem;
border-bottom: 1px solid var(--c-lines);
color: var(--c-3);
margin: 0 -1rem;
}
.event:last-of-type {
border-bottom: none;
}
.event__content{
display: flex;
gap: 1rem;
}
.event__avatar {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
.event__avatar img {
aspect-ratio: 1;
width: 40px;
border-radius: 40px;
outline: 1px solid var(--c-lines);
}
.event__text {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: .5rem;
}
header{
display: flex;
justify-content: space-between;
}
header h1 {
font-weight: 900;
font-size: 1rem;
margin: 0;
}
p{
margin: 0;
}
header span{
margin-right: .5rem;
color: var(--c-bright);
}
.zap-btn {
width: 40px;
height: 40px;
border-radius: 50px;
background-color: orange;
color: white;
outline: 0;
}
.event__responses{
border-left: 1px solid var(--c-lines);
margin-left: calc((40px - 1rem) / 2);
}
.event__message {
overflow-wrap: anywhere;
line-height: 1.4em;
white-space: pre-wrap;
}
</style>

View file

@ -1,325 +0,0 @@
<script>
import { afterUpdate, onMount } from "svelte";
import { selectedMessage, zappingMessage, zapsPerMessage } from "./lib/store";
import { chatData, chatAdapter } from "./lib/store";
import { nip19 } from "nostr-tools";
import ZapAmountButton from "./ZapAmountButton.svelte";
// import { prettifyContent } from '$lib/utils';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
export let event;
export let responses;
export let websiteOwnerPubkey;
let profiles = {};
let profilePicture;
let npub;
let zappingIt;
let hovering;
let mobilePR;
let relativeTimeFromNow;
let zappedAmount = 0;
function selectMessage() {
if ($selectedMessage === event.id) {
$selectedMessage = null;
} else {
$selectedMessage = event.id;
}
}
// delay-fetch responses
onMount(() => {
$chatAdapter.delayedSubscribe(
{ kinds: [1, 42, 9735], "#e": [event.id] },
"responses",
500
);
});
const byWebsiteOwner = !!websiteOwnerPubkey === event.pubkey;
$: profiles = $chatData.profiles;
$: displayName =
(profiles[event.pubkey] && profiles[event.pubkey].display_name) ||
`[${event.pubkey.slice(0, 6)}]`;
// $: nip05 = profiles[event.pubkey] && profiles[event.pubkey].nip05;
$: zappingIt = $zappingMessage === event.id;
$: {
try {
npub = nip19.npubEncode(event.pubkey);
} catch (e) {
npub = event.pubkey;
}
}
$chatAdapter.on("zap", () => {
zappedAmount =
$zapsPerMessage[event.id]?.reduce((acc, zap) => acc + zap.amount, 0) || 0;
});
$: {
zappedAmount =
$zapsPerMessage[event.id]?.reduce((acc, zap) => acc + zap.amount, 0) || 0;
}
afterUpdate(() => {
zappedAmount =
$zapsPerMessage[event.id]?.reduce((acc, zap) => acc + zap.amount, 0) || 0;
});
$: profilePicture =
(profiles[event.pubkey] && profiles[event.pubkey].picture) ||
`https://robohash.org/${event.pubkey.slice(0, 1)}.png?set=set1`;
// const repliedIds = event.tags.filter(e => e[0] === 'e').map(e => e[1]);
let timestamp = new Date(event.created_at * 1000);
$: {
const now = dayjs();
const then = dayjs(timestamp);
const diffInSeconds = now.diff(then, 'second');
const diffInMinutes = now.diff(then, 'minute');
const diffInHours = now.diff(then, 'hour');
const diffInDays = now.diff(then, 'day');
const diffInMonths = now.diff(then, 'month');
const diffInYears = now.diff(then, 'year');
if (diffInSeconds < 10) {
relativeTimeFromNow = "Now";
} else if (diffInSeconds < 60) {
relativeTimeFromNow = `${diffInSeconds}s`;
} else if (diffInMinutes < 60) {
relativeTimeFromNow = `${diffInMinutes}m`;
} else if (diffInHours < 24) {
relativeTimeFromNow = `${diffInHours}h`;
} else if (diffInDays < 365) {
relativeTimeFromNow = then.format('MMM D');
} else {
if (diffInYears >= 1 && diffInMonths >= 6) {
relativeTimeFromNow = then.format('MMM D, YYYY');
} else {
relativeTimeFromNow = then.format('MMM D');
}
}
}
</script>
<article class="event">
<div class="event__content">
<!-- AVATAR-->
<div class="event__avatar">
<a href={`nostr:${npub}`}
><img
src={profilePicture}
alt="{displayName}'s avatar"
/></a
>
<button
class="zap-btn {zappedAmount > 0
? 'zap-btn--zapped'
: ''}"
on:click|preventDefault={() =>
($zappingMessage = $zappingMessage === event.id ? null : event.id)}
>
{#if zappedAmount > 0}
<p>
⚡️
<span>
{zappedAmount / 1000}
</span>
</p>
{:else}
⚡️
{/if}
</button>
<!-- <div
class="
{zappingIt
? 'w-full rounded-full bg-white drop-shadow-xl justify-between border-2 border-white/50'
: ' rounded-full w-8 h-8 justify-center'}
flex items-center ml-5 mt-10 z-10"
>
{#if zappingIt}
{#if mobilePR}
<div class="">
<a
href={`lightning:${mobilePR}`}
class=""
>Open in wallet</a
>
<button
class=""
on:click={() => {
$zappingMessage = null;
}}
>
Cancel
</button>
</div>
{:else}
<div>
<div>
<ZapAmountButton icon="👍" amount={500} {event} bind:mobilePR />
</div>
<div>
<ZapAmountButton
icon="🤙"
amount={2500}
amountDisplay={"2.5k"}
{event}
bind:mobilePR
/>
</div>
<div >
<ZapAmountButton
icon="🙌"
amount={5000}
amountDisplay={"5k"}
{event}
bind:mobilePR
/>
</div>
<div>
<ZapAmountButton
icon="🧡"
amount={10000}
amountDisplay={"10k"}
{event}
bind:mobilePR
/>
</div>
<div>
<ZapAmountButton
icon="🤯"
amount={100000}
amountDisplay={"100k"}
{event}
bind:mobilePR
/>
</div>
<div>
<ZapAmountButton
icon="😎"
amount={1000000}
amountDisplay={"1M"}
{event}
bind:mobilePR
/>
</div>
</div>
{/if}
{/if}
</div> -->
</div>
<!-- TEXT-->
<div class="event__text">
<header>
<h1>
{displayName}
</h1>
<span title="{timestamp.toLocaleString()}">
{relativeTimeFromNow}
</span>
</header>
<p class="event__message">
{event.content}
</p>
</div>
</div>
{#if responses[event.id].length > 0}
<div class="event__responses">
{#each responses[event.id] as response}
<svelte:self {websiteOwnerPubkey} event={response} {responses} />
{/each}
</div>
{/if}
</article>
<style>
.event{
display: flex;
flex-direction: column;
padding: 1rem;
border-bottom: 1px solid var(--c-lines);
color: var(--c-3);
}
.event__content{
display: flex;
gap: 1rem;
}
.event__avatar {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
.event__avatar img {
aspect-ratio: 1;
width: 40px;
border-radius: 40px;
outline: 1px solid var(--c-lines);
}
.event__text {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: .5rem;
}
header{
display: flex;
justify-content: space-between;
}
header h1 {
font-weight: 900;
font-size: 1rem;
margin: 0;
}
p{
margin: 0;
}
header span{
margin-right: .5rem;
color: var(--c-bright);
}
.zap-btn {
width: 40px;
height: 40px;
border-radius: 50px;
background-color: orange;
color: white;
outline: 0;
}
.event__responses{
border-left: 1px solid var(--c-lines);
margin-left: calc((40px - 1rem) / 2);
}
.event__message {
overflow-wrap: anywhere;
line-height: 1.4em;
}
</style>

View file

@ -12,6 +12,16 @@
body {
margin: 0;
background: linear-gradient(to bottom right, #000, #0e1217);
padding: 0 1rem 0 1rem;
}
body > div {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100vh;
overflow: hidden;
}
/*
@ -22,25 +32,46 @@ div {
}
*/
/* main > section {
outline: 1px dotted orange;
} */
.page-outer {
border-radius: 1rem 1rem 0 0;
border-top: 1px solid var(--c-lines);
border-left: 1px solid var(--c-lines);
border-right: 1px solid var(--c-lines);
/* padding: 0 1rem 0 1rem; */
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(
to bottom,
#381100 0%,
#a13000 99.98%,
#b93700 99.99%,
#ff4c00 100%
),
linear-gradient(to left, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.3) 100%);
flex-grow: 1;
overflow-y: auto;
overflow-x: hidden;
}
main {
font-family: Work Sans, sans-serif;
display: flex;
height: 100vh;
overflow: hidden;
background: linear-gradient(
to bottom,
#381100 0%,
#a13000 99.98%,
#b93700 99.99%,
#ff4c00 100%
),
linear-gradient(to left, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.3) 100%);
}
.panel {
flex-grow: 1;
display: flex;
align-items: stretch;
flex-direction: column;
height: 100vh;
width: 566px;
overflow-y: auto;
background-color: rgb(56, 17, 0, 0.2);
padding: 1rem;
gap: 1rem;
border-left: 1px solid var(--c-lines);
border-right: 1px solid var(--c-lines);
}
.panel .toolbar {
@ -71,7 +102,7 @@ main {
}
button {
outline: 1px solid var(--c-lines);
border: 1px solid var(--c-lines);
color: var(--c-lines);
padding: 1em 2rem;
}
@ -101,7 +132,7 @@ button {
position: relative;
}
.content--scrolling::before {
/* .content--scrolling::before {
content: "";
position: sticky;
top: 0;
@ -110,15 +141,135 @@ button {
height: 100%;
background-color: var(--c-lines);
z-index: 1;
}
} */
button {
padding: 0.5em 1em;
background-color: transparent;
border: 1px solid var(--c-lines);
cursor: pointer;
font-family: 'Work Sans', sans-serif;
font-size: 1rem;
}
button:hover {
border: 1px solid var(--c-bright);
color: var(--c-bright)
}
h1 {
color: var(--c-bright);
font-weight: 500;
font-size: 1.5rem;
}
h2 {
color: var(--c-bright);
font-weight: 500;
font-size: 1.2rem;
}
@media only screen and (max-width: 598px) {
.hide-on-mobile {
display: none;
}
}
@media only screen and (max-width: 1140px) {
section.metadata h1{
background: none;
}
}
.event .event {
padding-right: 0 !important;
margin: 0 !important;
}
.event .event .event__responces {
margin-left: calc((40px) / 2) !important;
}
.btn__subheading {
display: block;
font-size: .8rem;
font-weight: 700;
text-transform: uppercase;
}
.btn-list{
display: flex;
gap: 1rem;
align-items: stretch;
}
.btn-list .btn {
flex-grow: 1;
}
.sheet p {
color: var(--c-bright)
}
.sheet a{
color: var(--c-marker)
}
.sheet li {
color: var(--c-bright)
}
.sheet blockquote {
font-size: 1.1rem;
font-style: italic;
}
section.nav ul{
list-style: none;
padding: 0;
margin: 0;
}
code{
color: #bbb;
letter-spacing: 0.05em;
}
section.nav a{
color: var(--c-bright);
text-decoration: none;
}
section.nav a:hover{
color: var(--c-bright);
text-decoration: underline;
}
.sheet .btn{
display: flex;
}
.btn.btn--lightning {
color: var(--c-bright);
background-color: var(--c-bright);
color: black;
padding: .25em .75em;
border-radius: 2em;
display: inline-flex;
align-items: center;
text-decoration: none;
font-weight: 400;
gap: .5em
}
.sheet pre code {
line-break: anywhere;
color: var(--c-bright);
white-space: normal;
}
.sheet .support-links {
display: flex;
gap: 1rem;
align-items: center;
}

80
src/lib/README.md Normal file
View file

@ -0,0 +1,80 @@
# What is DiSseNT?
[DiSseNT](https://dsnt.chat) is my attempt at ressurecting a valuable [project](https://github.com/gab-ai-inc/gab-dissenter-extension/issues/117) to enable truely trustless and free commentary on the web. DiSseNT is a web app built on [NOSTR](https://github.com/nostr-protocol/nostr), and based on [Nostri.chat](https://github.com/pablof7z/nostr-chat-widget), that [associates](#how-does-it-work) a set of NOSTR messages with a given URL.
This standalone web app is a first step - browser plugins and native apps will be the next (and perhaps more important) steps.
# But why?
It's valuable to have public debate on important issues. In the web-centric world that we live in, most important issues are represented by urls (a news article on CNN, a post on X.com, a video on YouTube, etc) and consequently, much of that debate is going to happen online and refer directly to those urls.
**Also** in the world we live in, most of that debate is tightly controlled by third parties. Most public commentary is hosted on servers owned by the likes of X.com, Facebook, YouTube and Reddit. Commentary on press articles and blogs is frequently stored on Disqus servers or Medium servers. Even alternative media outlets like Substack control their own commentary. With this control, necessarily (and perhaps understandably) comes censorship. With this censorship, the effectiveness of the public forum to suss out truth is diminished.
The goal of DiSseNT is to be a universal comment section for the web. The commentary will be controlled by no one, stored everywhere and nowhere, and tied [*](#archival)forever to the source material.
# How do I use it?
1. In the top bar, type the url of a real webpage and hit enter, or paste a url.
- If the url represents a real page, the title of the page and other meta data should appear.
- If there are comments [associated](#how-does-it-work) with this page on NOSTR, they should appear.
1. Type a comment and then submit it by clicking send, or hitting enter.
- Hold control while hitting enter to get a new line.
<h1 id="how-does-it-work">How does it work?</h1>
At the moment, DiSsenT is fundamentally a fork of [Nostri.chat](https://github.com/pablof7z/nostr-chat-widget), an in-page support chat widget designed by Pablof7z, with trivial cosmetic changes, and one important functional change:
> All chats are created with "GROUP" as the `chatType` and a hex version of the provided url as the `chatId`.
# Roadmap
- [x] basic posting, associated with a url
- [x] display meta info
- [ ] log out *(sept 2023)*
- [ ] reply to comments *(sept 2023)*
- [ ] zap comments *(sept 2023)*
- [ ] search comments *(fall 2023)*
- [ ] filter comments *(fall 2023)*
- [ ] sort comments
- [ ] cache comments
- [ ] relay picker
- [ ] **chromium extension**
- [ ] **PWA**
- [ ] view all dsnt'd urls
- [ ] search dsnt'd urls
- [ ] markdown comments
- [ ] automatic PDF storage of referenced webpages
- [ ] dark mode / light mode
# Supporting the Roadmap
<div class="support-links">
<a class="btn" href='https://ko-fi.com/O4O1OZX1V' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi2.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
<a class="btn" href="https://liberapay.com/spencer.flagg/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
<a class="btn btn--lightning" href="lightning:crimsonbird599@getalby.com" title="tip on the lightning network">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_21_2)">
<mask id="mask0_21_2" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_21_2)">
<path d="M7.99902 16.0002C12.4173 16.0002 15.999 12.4185 15.999 8.00024C15.999 3.58197 12.4173 0.000244141 7.99902 0.000244141C3.58075 0.000244141 -0.000976562 3.58197 -0.000976562 8.00024C-0.000976562 12.4185 3.58075 16.0002 7.99902 16.0002Z" fill="#7B1AF7"/>
<path d="M4.52538 8.17306L9.85872 3.5773C10.0911 3.42847 10.3126 3.5773 10.1708 3.83261L8.46865 7.18015H11.5041C11.5041 7.18015 11.9864 7.18015 11.5041 7.57732L6.25588 12.2014C5.88708 12.5135 5.63177 12.3433 5.88708 11.861L7.53247 8.59859H4.52538C4.52538 8.59859 4.04311 8.59859 4.52538 8.17306Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_21_2">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
Tip Lightning
</a>
</div>
**Tip Monero**
```
84ENScA84suRz2pF1eptxS5FRfyYJcMX9VWrpfUQsUfiY8RVDFkfZCJEKHUQLNu5GJUiuwVtjJGSSiPnNX4PVz2dHPQe44T
```
<h1 id="archival">Archival</h1>
Ultimately I'd love to incorporate some automatic PDF storage of referenced webpages. Perhaps on IPFS?

16
src/lightning.svg Normal file
View file

@ -0,0 +1,16 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_21_2)">
<mask id="mask0_21_2" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_21_2)">
<path d="M7.99902 16.0002C12.4173 16.0002 15.999 12.4185 15.999 8.00024C15.999 3.58197 12.4173 0.000244141 7.99902 0.000244141C3.58075 0.000244141 -0.000976562 3.58197 -0.000976562 8.00024C-0.000976562 12.4185 3.58075 16.0002 7.99902 16.0002Z" fill="#7B1AF7"/>
<path d="M4.52538 8.17306L9.85872 3.5773C10.0911 3.42847 10.3126 3.5773 10.1708 3.83261L8.46865 7.18015H11.5041C11.5041 7.18015 11.9864 7.18015 11.5041 7.57732L6.25588 12.2014C5.88708 12.5135 5.63177 12.3433 5.88708 11.861L7.53247 8.59859H4.52538C4.52538 8.59859 4.04311 8.59859 4.52538 8.17306Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_21_2">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1,007 B

View file

@ -1,430 +1,414 @@
<script>
import 'websocket-polyfill';
import Container from '../Container.svelte';
import Widget from '../Widget.svelte';
import { chatAdapter } from '../lib/store';
// Imports: Library and Svelte Store
import { onMount, onDestroy, afterUpdate } from "svelte";
import { fade } from 'svelte/transition';
import { browser } from '$app/environment';
import { chatAdapter, url, relays } from '../lib/store';
let chatStarted;
let chatType = 'GROUP';
let websiteOwnerPubkey = 'fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52';
let chatTags = [];
let chatId = '9cef2eead5d91df42eba09be363f1272107e911685126ea5e261ac2d93299478';
let chatReferenceTags = [];
const relays = [
'wss://relay.f7z.io',
'wss://nos.lol',
'wss://relay.nostr.band',
];
// Imports: Polyfills and Components
import "websocket-polyfill";
import KeyPrompt from "../KeyPrompt.svelte";
import ConnectedWidget from "../ConnectedWidget.svelte";
import MetaData from "../MetaData.svelte";
import Brand from "../Brand.svelte";
$: currentTopic = [...chatTags, ...chatReferenceTags][0]
// Local Variables
let chatStarted;
let chatType = "GROUP";
let websiteOwnerPubkey = "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52";
let chatTags = [];
let chatReferenceTags = [];
let searchElement;
let inputUrl = "";
let refreshKey = 1;
let scrollPosition = 0;
let searchIsFocused;
let mainElement;
function currentTopic(topic) {
return [...chatTags, ...chatReferenceTags].includes(topic)
}
// Reactive Statements
//$: currentTopic = [...chatTags, ...chatReferenceTags][0];
$: chatId = $url && stringToHex($url);
$: chatStarted = !!$chatAdapter;
$: if (chatStarted) {
afterUpdate(() => {
//if (searchElement) searchElement.focus();
});
refreshWidget();
}
// Lifecycle Methods
onMount(() => {
if (browser) document.addEventListener('keydown', handleGlobalKeydown);
});
onDestroy(() => {
if (browser) document.removeEventListener('keydown', handleGlobalKeydown);
});
// Helper Functions
function stringToHex(str) {
let result = "";
for (let i = 0; i < str.length; i++) {
// Convert each character to its char code
const charCode = str.charCodeAt(i);
// Convert the char code to its hex representation
result += charCode.toString(16).padStart(2, "0");
}
return result;
}
const isValidUrl = (str) => {
const pattern = new RegExp(
"^(https?:\\/\\/)?" + // protocol
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
"(\\#[-a-z\\d_]*)?$",
"i"
); // fragment locator
return !!pattern.test(str);
};
const startsWithProtocol = (str) => {
return str.startsWith("http://") || str.startsWith("https://");
};
// Event Handlers and UI Logic
function handleEnterKey(event) {
if (event.keyCode === 13 && isValidUrl(inputUrl)) {
showComments(inputUrl);
}
}
function handlePaste(event) {
let thisEvent = event.clipboardData.getData('Text');
showComments(thisEvent);
}
function handleScroll(event) {
scrollPosition = event.target.scrollTop;
}
function handleGlobalKeydown(event) {
// Check for Ctrl + /
if (event.ctrlKey && event.key === '/') {
searchIsFocused = true;
// Focus the input element
searchElement.focus();
// Clear the input
searchElement.value = '';
}
}
function searchFocused() {
searchIsFocused = true;
}
function searchBlurred(e) {
//console.log(e);
searchIsFocused = false;
return true;
}
function updateFocusState(event) {
setTimeout(() => {
searchIsFocused = (event.type === "focus");
//console.log("isFocused: ", searchIsFocused);
}, 50);
}
function showComments(str) {
console.log(str);
if (!str.startsWith("http://") && !str.startsWith("https://")) {
$url = "https://" + str;
} else {
$url = str;
}
inputUrl = "";
chatType = "GROUP";
chatTags = [];
chatReferenceTags = [$url];
}
function refreshWidget() {
refreshKey++;
}
// function currentTopic(topic) {
// return [...chatTags, ...chatReferenceTags].includes(topic);
// }
</script>
<svelte:head>
<title>Nostri.chat / A NOSTR chat widget you control</title>
<meta property="og:url" content="https://nostri.chat/">
<meta name="description" content="A chat widget you own, powered by nostr" />
<meta property="og:description" content="A chat widget you own, powered by nostr" />
<title>DiSseNT - The web's comment section</title>
<meta property="og:url" content="https://dsnt.chat/" />
<meta name="description" content="The web's comment section." />
<meta
property="og:description"
content="The web's comment section."
/>
</svelte:head>
<section class="
lg:min-h-screen
text-white
bg-gradient-to-b from-orange-500 to-orange-800
">
<div class="lg:min-h-screen mx-auto w-full lg:max-w-7xl py-5 xl:py-10
flex flex-col md:flex-row
gap-20 items-center px-4 lg:px-0
relative
">
<div class="
md:w-7/12 gap-10
">
<section id="hero" style="min-height: 50vh;">
<h1 class="
text-6xl
font-black
my-2
">Nostri.chat</h1>
<nav class="content">
<div class="search-wrapper">
<div class="search">
<div class="search__bar {searchIsFocused ? 'search__bar--focused' : ''}">
{#if !startsWithProtocol(inputUrl)}
<span>https://</span>
{/if}
<input
type="search"
bind:value={inputUrl}
bind:this={searchElement}
placeholder="type or paste a URL"
on:keyup={handleEnterKey}
on:paste={handlePaste}
on:focus={updateFocusState}
on:blur={updateFocusState}
/>
</div>
{#if isValidUrl(inputUrl)}
<button transition:fade={{ delay: 250, duration: 300 }} class="search__btn" on:click={showComments(inputUrl)} title="Show Comments">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--color)" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M8 9h8" />
<path d="M8 13h6" />
<path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z" />
</svg>
</button>
{/if}
</div>
</div>
</nav>
<h2 class="
text-2xl lg:text-4xl
text-bold
">A chat widget for your site, powered by nostr</h2>
<div class="page-outer">
<main class="content--scrolling" on:scroll={handleScroll} bind:this={mainElement}>
<p class="
max-w-prose
text-2xl
text-gray-200
tracking-wide
leading-9
my-5
">
Simple, interoperable
communication with your visitors, in a way
that gives you and them complete ownership
over the data.
</p>
</section>
</div>
<section class="nav">
<nav>
<ul>
<li><a href='/about'>About</a></li>
</ul>
</nav>
</section>
<div class="
flex-row items-center justify-center
min-h-screen
hidden md:flex
md:w-5/12
">
<div class="
shadow-2xl
bg-gray-100/90 backdrop-blur-md mb-5 w-96 max-w-screen-sm text-black rounded-3xl px-4 py-5 overflow-auto
flex flex-col justify-end
fixed
" style="{chatStarted ? 'max-height: 80vh;' : 'padding: 4rem 2rem !important;'}">
<Container chatConfiguration={{
chatType,
chatId,
chatTags,
chatReferenceTags,
}} {relays} bind:chatStarted={chatStarted} />
</div>
</div>
</div>
</section>
{#if !chatStarted}
<section class="brand">
<Brand />
</section>
<section class="key-prompt">
<KeyPrompt
{websiteOwnerPubkey}
chatConfiguration={{
chatType,
chatId,
chatTags,
chatReferenceTags,
}}
relays={$relays}
/>
</section>
{/if}
{#if chatStarted && $url}
<section class="brand hide-on-mobile">
<Brand />
</section>
<section class="
min-h-screen
py-5
lg:py-16
" style="min-height: 50vh;">
<div class="mx-auto w-full lg:max-w-7xl py-5 xl:py-10
flex flex-col lg:flex-row
gap-20 px-4 lg:px-0
" style="min-height: 50vh;">
<div class="md:w-7/12 flex flex-col gap-8">
<div>
<h1 class="text-6xl lg:text-7xl font-black">
Innovative modes
</h1>
<section class="shortcuts hide-on-mobile">
<ul>
<li>
<strong>Ctrl + /</strong> to change the URL
</li>
<li>
<strong>Ctrl + space</strong> to type a comment
</li>
</ul>
</section>
<p class="
text-2xl font-extralight
">
Because we use Nostr for communicating,
<b>Nostri.chat</b>
can use some new, creative approaches to using chat widget,
depending on what you want to achieve.
</p>
</div>
<section class="metadata">
{#if $url}
<MetaData url={$url} scrollPosition={scrollPosition}/>
{/if}
</section>
<div class="flex flex-col gap-3">
<h2 class="text-3xl text-orange-600 font-black">
Classic mode
<span class="text-2xl text-slate-500 font-extralight block">encrypted 1-on-1 chats</span>
</h2>
<p class="
text-xl text-gray-500 text-justify
font-light
leading-8
">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sapiente quae eveniet placeat, obcaecati nesciunt nam iure. Culpa omnis hic eaque illum alias iure autem atque? Distinctio facilis recusandae omnis expedita.
</p>
{#if $chatAdapter}
{#if chatType === 'DM'}
<button class="px-4 rounded border-2 border-orange-700 py-2 text-orange-700 text-lg w-full font-semibold">
Active
</button>
{:else}
<button class="px-4 rounded bg-orange-700 py-2 text-white text-lg w-full font-semibold" on:click={()=>{ chatType='DM'; chatTags=[]; chatReferenceTags=[] }}>
Try it
</button>
{/if}
{/if}
</div>
<div class="flex flex-col gap-3">
<h2 class="text-3xl text-orange-600 font-black">
<div class="flex flex-row gap-2">
<span>🫂</span>
<span class="flex flex-col">
<span>Public chat groups</span>
<span class="text-2xl text-slate-500 font-extralight block">public groups</span>
</span>
</div>
</h2>
<p class="
text-xl text-gray-500 text-justify
font-light
leading-8
">
Embed NIP-28 public chat groups on your site.
</p>
<div class="flex flex-col lg:flex-row justify-between mt-10 gap-10 mb-6">
<div class="flex flex-col lg:w-1/2 items-center gap-4 border p-4 shadow-md rounded-lg w-fit">
<h3 class="
text-black
text-lg
font-semibold
">Group chat</h3>
<span class="inline-flex rounded-md">
<button type="button" class="
inline-flex items-center rounded-l-md border px-4 py-2 text-md font-medium
{chatType === 'GROUP' && chatId === '9cef2eead5d91df42eba09be363f1272107e911685126ea5e261ac2d93299478' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
:ring-indigo-500"
on:click={()=>{ chatType='GROUP'; chatTags=[]; chatId='9cef2eead5d91df42eba09be363f1272107e911685126ea5e261ac2d93299478' }}
>
#Test
</button>
<button type="button" class="
inline-flex items-center rounded-r-md border px-4 py-2 text-md font-medium
{chatType === 'GROUP' && chatId === 'a6f436a59fdb5e23c757b1e30478742996c54413df777843e0a731af56a96eea' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
:ring-indigo-500"
on:click={()=>{ chatType='GROUP'; chatTags=[]; chatId='a6f436a59fdb5e23c757b1e30478742996c54413df777843e0a731af56a96eea' }}
>
#NDK
</button>
</span>
</div>
</div>
<h2 class="text-3xl text-orange-600 font-black">
<div class="flex flex-row gap-2">
<span>🔖</span>
<span class="flex flex-col">
<span>Tagged Global Chat</span>
<span class="text-2xl text-slate-500 font-extralight block">public discussion/support</span>
</span>
</div>
</h2>
<p class="
text-xl text-gray-500 text-justify
font-light
leading-8
">
Imagine having a global chat on your website about a certain topic.
Anyone can participate, from your website or from any Nostr client.
</p>
<div class="flex flex-col lg:flex-row justify-between mt-10 gap-10">
<div class="flex flex-col items-center gap-4 border p-4 shadow-md rounded-lg w-fit lg:w-full">
<h3 class="
text-black
text-lg
font-semibold
">🔖 Topic-based chats</h3>
<span class="inline-flex rounded-md">
<button type="button" class="
inline-flex items-center rounded-l-md border px-4 py-2 text-md font-medium
{currentTopic === 'nostrica' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
" on:click={()=>{ chatType='GLOBAL'; chatTags=['nostrica']; chatReferenceTags=[] }}>
#nostrica
</button>
<button type="button" class="
inline-flex items-center rounded-r-md border px-4 py-2 text-md font-medium
{currentTopic === 'bitcoin' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
" on:click={()=>{ chatType='GLOBAL'; chatTags=['bitcoin']; chatReferenceTags=[] }}>
#bitcoin
</button>
</span>
</div>
<div class="flex flex-col items-center gap-4 border p-4 shadow-md rounded-lg w-fit lg:w-full">
<h3 class="
text-black
text-lg
font-semibold
">🌎 Website-based chats</h3>
<span class="inline-flex rounded-md">
<button type="button" class="
inline-flex items-center rounded-l-md border px-4 py-2 text-md font-medium
{currentTopic === 'https://nostri.chat' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
:ring-indigo-500"
on:click={()=>{ chatType='GLOBAL'; chatTags=[]; chatReferenceTags=['https://nostri.chat'] }}
>
<span class="opacity-50 font-normal">https://</span>nostri.chat
</button>
<button type="button" class="
inline-flex items-center rounded-r-md border px-4 py-2 text-md font-medium
{currentTopic === 'https://psbt.io' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
:ring-indigo-500"
on:click={()=>{ chatType='GLOBAL'; chatTags=[]; chatReferenceTags=['https://psbt.io'] }}
>
<span class="opacity-50 font-normal">https://</span>psbt.io
</button>
<button type="button" class="
inline-flex items-center rounded-r-md border px-4 py-2 text-md font-medium
{currentTopic === 'https://a.com' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
:ring-indigo-500"
on:click={()=>{ chatType='GLOBAL'; chatTags=[]; chatReferenceTags=['https://a.com'] }}
>
<span class="opacity-50 font-normal">a 🌎️</span>
</button>
<button type="button" class="
inline-flex items-center rounded-r-md border px-4 py-2 text-md font-medium
{currentTopic === 'https://a.com' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
:ring-indigo-500"
on:click={()=>{ chatType='GROUP'; chatTags=[]; chatReferenceTags=['https://a.com'] }}
>
<span class="opacity-50 font-normal">a 👥</span>
</button>
<button type="button" class="
inline-flex items-center rounded-r-md border px-4 py-2 text-md font-medium
{currentTopic === 'https://b.com' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
:ring-indigo-500"
on:click={()=>{ chatType='GLOBAL'; chatTags=[]; chatReferenceTags=['https://b.com'] }}
>
<span class="opacity-50 font-normal">b 🌎️</span>
</button>
<button type="button" class="
inline-flex items-center rounded-r-md border px-4 py-2 text-md font-medium
{currentTopic === 'https://b.com' ?
'text-white bg-orange-700 border-orange-900'
:
'border-gray-300 bg-white text-gray-700'}
:ring-indigo-500"
on:click={()=>{ chatType='GROUP'; chatTags=[]; chatReferenceTags=['https://b.com'] }}
>
<span class="opacity-50 font-normal">b 👥</span>
</button>
</span>
</div>
</div>
</div>
</div>
</section>
<section class="
min-h-screen
py-5
lg:py-16
bg-slate-100
" style="min-height: 50vh;">
<div class="mx-auto w-full lg:max-w-7xl py-5 xl:py-10
flex flex-col lg:flex-row
gap-20 items-center px-4 lg:px-0
" style="min-height: 50vh;">
<div class="md:w-4/5 lg:w-3/5 grid grid-cols-1 gap-8">
<div>
<h1 class="text-7xl font-black">
Easy-peasy setup
</h1>
<p class="
text-2xl font-extralight
">
Just drop this snippet on your website and you're good to go.
</p>
</div>
<div class="text-xl font-semibold">Public group chat (GROUP)</div>
<pre class ="
p-4
bg-white
overflow-auto
">
&lt;script
src="https://nostri.chat/public/bundle.js"
<b>data-chat-type</b>="<span class="text-orange-500">GROUP</span>"
<b>data-chat-id</b>="<span class="text-orange-500">&lt;GROUP_ID_IN_HEX_FORMAT&gt;</span>"
<b>data-relays</b>="<span class="text-orange-500">wss://relay.f7z.io,wss://nos.lol,wss://relay.nostr.band</span>"
&gt;&lt;/script&gt;
&lt;link rel="stylesheet" href="https://nostri.chat/public/bundle.css"&gt;</pre>
<div class="text-xl font-semibold">Public global notes (kind-1 short notes)</div>
<pre class ="
p-4
bg-white
overflow-auto
">
&lt;script
src="https://nostri.chat/public/bundle.js"
<b>data-chat-type</b>="<span class="text-orange-500">GLOBAL</span>"
<b>data-chat-tags</b>="<span class="text-orange-500">bitcoin</span>"
<b>data-relays</b>="<span class="text-orange-500">wss://relay.f7z.io,wss://nos.lol,wss://relay.nostr.band</span>"
&gt;&lt;/script&gt;
&lt;link rel="stylesheet" href="https://nostri.chat/public/bundle.css"&gt;</pre>
<div class="text-xl font-semibold">Encrypted DMs</div>
<pre class ="
p-4
bg-white
overflow-auto
">
&lt;script
src="https://nostri.chat/public/bundle.js"
<b>data-chat-type</b>="<span class="text-orange-500">DM</span>"
<b>data-website-owner-pubkey</b>="<span class="text-orange-500">YOUR_PUBKEY_IN_HEX_FORMAT</span>"
<b>data-relays</b>="<span class="text-orange-500">wss://relay.f7z.io,wss://nos.lol,wss://relay.nostr.band</span>"
&gt;&lt;/script&gt;
&lt;link rel="stylesheet" href="https://nostri.chat/public/bundle.css"&gt;</pre>
</div>
</div>
</section>
<div class="md:hidden">
<Widget chatConfiguration={{
chatTags,
chatReferenceTags,
}} {websiteOwnerPubkey} {chatType} {chatId} {relays} bind:chatStarted={chatStarted} />
{#each [refreshKey] as key (key)}
<ConnectedWidget
{websiteOwnerPubkey}
chatConfiguration={{
chatType,
chatId,
chatTags,
chatReferenceTags,
}}
relays={$relays}
mainElement={mainElement}
/>
{/each}
{/if}
</main>
</div>
<footer class="py-6 bg-orange-900 font-mono text-white text-center mt-12 px-10">
<div class="flex justify-center flex-row">
<div class="text-sm">
NOSTRI.CHAT
by
<a class="text-purple-50 hover:text-orange-400" href="https://pablof7z.com">
@pablof7z
</a>
</div>
</div>
</footer>
<style>
/* div { border: solid red 1px; } */
@tailwind base;
@tailwind components;
@tailwind utilities;
@media only screen and (max-width: 598px) {
main {
width: fit-content;
border: none;
}
nav {
width: 100% !important;
}
section {
position: relative !important;
top: auto !important;
left: auto !important;
right: auto !important;
}
.search__bar{
font-size: 5vmin !important;
}
}
@media only screen and (max-width: 1140px) {
section {
position: relative !important;
top: auto !important;
left: auto !important;
right: auto !important;
}
section.brand {
order: -1;
margin-top: 1rem;
}
section.shortcuts {
order: 0;
}
}
nav{
width: 566px;
align-self: center;
flex-shrink: 0;
}
.content {
display: flex;
flex-direction: column;
padding: 1rem;
}
section.brand {
position: fixed;
bottom: 1rem;
left: 2rem;
}
section.metadata {
display: flex;
flex-direction: column;
gap: 1rem;
position: sticky;
top:0;
z-index: 1;
}
section.nav{
position: fixed;
top: 1rem;
left: 1rem;
}
section.shortcuts {
position: fixed;
top:6rem;
left: 2rem;
}
section.shortcuts ul {
padding: 0;
}
section.shortcuts li {
list-style-type: none;
color: var(--c-lines);
}
.search-wrapper {
flex-grow: 1;
display: flex;
justify-content: center;
flex-direction: column;
}
.search {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
}
.search button {
position: absolute;
height: 100%;
text-transform: uppercase;
width: max-content;
}
.search__btn svg {
width: 2rem;
--color: var(--c-bright);
}
.search__btn {
border: 0;
}
.search__bar {
display: flex;
border-bottom: 1px solid var(--c-lines);
font-size: 2rem;
align-items: center;
padding: .5rem;
flex-grow: 1;
}
.search__bar span{
color: rgba(255, 255, 255, 0.25);
font-family: Work Sans, sans-serif;
}
.search__bar input {
color: rgba(255, 255, 255);
transition: all .3s ease;
background-color: transparent;
font-size: inherit;
border: none;
font-weight: inherit;
font-family: Work Sans, sans-serif;
width: calc(100% - 10rem);
}
.search__bar input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.search__bar input:focus {
outline: none;
}
.search__bar.search__bar--focused {
border-bottom: 1px solid var(--c-bright);
}
</style>

View file

@ -0,0 +1,60 @@
<script>
import Readme from '$lib/README.md';
</script>
<svelte:head>
<title>About DiSseNT</title>
<meta property="og:url" content="https://dsnt.chat/about" />
<meta name="description" content="The web's comment section." />
<meta
property="og:description"
content="The web's comment section."
/>
</svelte:head>
<section class="nav">
<nav>
<ul>
<li><a href='/'>👈️ back</a></li>
</ul>
</nav>
</section>
<div class="page">
<div class="sheet content--scrolling">
<Readme />
</div>
</div>
<style>
section.nav{
font-family: Work Sans, sans-serif;
position: fixed;
top: 1rem;
left: 1rem;
}
.page {
font-family: Work Sans, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
flex-grow: 1;
overflow-y: auto;
overflow-x: hidden;
}
.sheet{
font-family: Work Sans, sans-serif;
display: flex;
align-items: stretch;
flex-direction: column;
width: 566px;
overflow-y: auto;
background-color: rgb(56, 17, 0, 0.2);
padding: 1rem;
gap: 1rem;
}
</style>

View file

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>Demo page</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="public/bundle.css">
<body>
<h1>Svelte embedding demo</h1>
<p>Below,we have inserted a <code>script</code> tag that should
renter a Svelte component upon loading this page.</p>
<script
src="/public/bundle.js"
data-website-owner-pubkey="fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"
data-chat-type="GROUP"
data-chat-id="a6f436a59fdb5e23c757b1e30478742996c54413df777843e0a731af56a96eea"
data-relays="wss://relay.f7z.io,wss://nos.lol,wss://relay.nostr.info,wss://nostr-pub.wellorder.net,wss://relay.current.fyi,wss://relay.nostr.band"
></script>
<p>This text will come after the embedded content.</p>
</body>
</html>

View file

@ -1,254 +0,0 @@
<script>
import { afterUpdate } from 'svelte';
import 'websocket-polyfill';
import Container from '../../Container.svelte';
import KeyPrompt from '../../KeyPrompt.svelte';
import ConnectedWidget from '../../ConnectedWidget.svelte';
import Widget from '../../Widget.svelte';
import { chatAdapter } from '../../lib/store';
import Relays from '../../Relays.svelte';
let chatStarted;
let chatType = 'GROUP';
let websiteOwnerPubkey = 'fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52';
let chatTags = [];
//let chatId = '9cef2eead5d91df42eba09be363f1272107e911685126ea5e261ac2d93299478';
let chatReferenceTags = [];
let relays = [
'wss://relay.f7z.io',
// 'wss://nos.lol',
// 'wss://relay.nostr.band',
// 'wss://nostr-pub.wellorder.net',
// 'wss://relay.damus.io',
// 'wss://relay.f7z.io'
];
$: currentTopic = [...chatTags, ...chatReferenceTags][0]
$: chatId = url && stringToHex(url)
function handleUpdate(event) {
relays = event.detail;
refreshWidget();
}
function stringToHex(str) {
let result = '';
for (let i = 0; i < str.length; i++) {
// Convert each character to its char code
const charCode = str.charCodeAt(i);
// Convert the char code to its hex representation
result += charCode.toString(16).padStart(2, '0');
}
return result;
}
$: chatStarted = !!$chatAdapter
function currentTopic(topic) {
return [...chatTags, ...chatReferenceTags].includes(topic)
}
$: if (chatStarted) {
afterUpdate(() => {
if (searchElement) searchElement.focus();
});
}
let searchElement;
let inputUrl = "";
let url = null;
const isValidUrl = (str) => {
const pattern = new RegExp(
"^(https?:\\/\\/)?" + // protocol
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
"(\\#[-a-z\\d_]*)?$",
"i"
); // fragment locator
return !!pattern.test(str);
};
const startsWithProtocol = (str) => {
return (str.startsWith("http://") || str.startsWith("https://"));
};
function handleEnterKey(event) {
if (event.keyCode === 13 && isValidUrl(inputUrl)) {
showComments(inputUrl);
}
}
function showComments(str) {
if (!str.startsWith("http://") && !str.startsWith("https://")) {
url = "https://" + str;
} else {
url = str;
}
inputUrl = "";
chatType='GROUP';
chatTags=[];
chatReferenceTags=[url];
}
function handlePaste(event) {
showComments(event);
inputUrl = "";
}
let refreshKey = 1;
function refreshWidget() {
refreshKey++;
}
</script>
<svelte:head>
<title>Nostri.chat / A NOSTR chat widget you control</title>
<meta property="og:url" content="https://nostri.chat/">
<meta name="description" content="A chat widget you own, powered by nostr" />
<meta property="og:description" content="A chat widget you own, powered by nostr" />
</svelte:head>
<main>
<section id="hero">
<h1 class="
text-6xl
font-black
my-2
">Dissent</h1>
<h2 class="
text-2xl lg:text-4xl
text-bold
">The comments section of the internet.</h2>
<pre>
{chatType}
{chatId}
{chatTags}
{chatReferenceTags}
</pre>
</section>
{#if !chatStarted}
<section>
<KeyPrompt {websiteOwnerPubkey} chatConfiguration={{
chatType,
chatId,
chatTags,
chatReferenceTags,
}} {relays} />
</section>
{:else}
<section>
<div class="max-w-prose text-2xl text-gray-200 tracking-wide leading-9">
<div class="search-container p-2 w-full resize-none rounded-xl text-gray-600 border back bg-white">
{#if !startsWithProtocol(inputUrl)}
<span>https://</span>
{/if}
<input
type="text"
bind:value={inputUrl}
bind:this="{searchElement}"
placeholder="paste or enter a URL"
on:keyup={handleEnterKey}
on:paste={handlePaste(inputUrl)}
/>
</div>
{#if isValidUrl(inputUrl)}
<div class="p-2">
<button class="bg-purple-900 hover:bg-purple-700 w-full p-4 rounded-xl text-center font-regular text-gray-200" on:click={showComments(inputUrl)}>Show Comments</button>
</div>
{/if}
</div>
</section>
<section class="justify-center">
{#if url}
<h3 class="text-2xl lg:text-3xl text-bold url">{url}</h3>
<div class="shadow-2xl bg-gray-100/90 backdrop-blur-md mb-5 w-96 max-w-screen-sm text-black rounded-3xl px-4 py-5 overflow-auto flex flex-col justify-end ">
{#each [refreshKey] as key (key)}
<ConnectedWidget {websiteOwnerPubkey} chatConfiguration={{
chatType,
chatId,
chatTags,
chatReferenceTags,
}} {relays} />
{/each}
</div>
{/if}
</section>
{/if}
<Relays bind:relays on:update={handleUpdate} />
<div
class="flex flex-col justify-start items-start gap-[25px] px-[23px] py-[17px] bg-gradient-to-br from-[#f10e0e] to-[#0c84e0]"
>
<div class="flex flex-col justify-start items-start flex-grow-0 flex-shrink-0 relative">
<p class="flex-grow-0 flex-shrink-0 text-4xl font-bold text-left text-[#eee]">Heading</p>
<p class="flex-grow-0 flex-shrink-0 text-2xl text-left text-[#eee]">sub heading</p>
</div>
<div class="flex flex-col justify-start items-start flex-grow-0 flex-shrink-0 relative gap-2">
<p class="flex-grow-0 flex-shrink-0 text-xs text-left text-white">Label</p>
<div class="flex justify-start items-center flex-grow-0 flex-shrink-0 gap-4">
<div
class="flex justify-start items-center flex-grow-0 flex-shrink-0 w-[203px] relative overflow-hidden gap-2.5 p-2.5 rounded-[20px] bg-white"
>
<p class="flex-grow-0 flex-shrink-0 text-xs text-left text-black">search by typing</p>
</div>
<div
class="flex flex-col justify-center items-center flex-grow-0 flex-shrink-0 w-[42px] h-[42px] overflow-hidden gap-2.5 px-px py-[3px] rounded-[30px] bg-[#03d403]"
></div>
</div>
</div>
</div>
</main>
<style>
/* div { border: solid red 1px; } */
@tailwind base;
@tailwind components;
@tailwind utilities;
main{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
section{
width: 90vw;
max-width: 600px;
display: flex;
flex-direction: column;
padding: 1rem;
}
.search-container {
display: flex;
}
.search-container input {
flex-grow: 1;
}
h3.url{
display: block;
}
</style>

View file

@ -1,235 +0,0 @@
<script>
import { onMount, afterUpdate } from "svelte";
import "websocket-polyfill";
import KeyPrompt from "../../KeyPrompt.svelte";
import ConnectedWidget from "../../ConnectedWidget2.svelte";
//import Relays from "../../Relays.svelte";
import MetaData from "../../MetaData.svelte";
import Brand from "../../Brand.svelte";
import { chatAdapter, url, relays } from '../../lib/store';
let chatStarted;
let chatType = "GROUP";
let websiteOwnerPubkey =
"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52";
let chatTags = [];
//let chatId = '9cef2eead5d91df42eba09be363f1272107e911685126ea5e261ac2d93299478';
let chatReferenceTags = [];
$: currentTopic = [...chatTags, ...chatReferenceTags][0];
$: chatId = $url && stringToHex($url);
function handleUpdate(event) {
relays = event.detail;
refreshWidget();
}
function stringToHex(str) {
let result = "";
for (let i = 0; i < str.length; i++) {
// Convert each character to its char code
const charCode = str.charCodeAt(i);
// Convert the char code to its hex representation
result += charCode.toString(16).padStart(2, "0");
}
return result;
}
$: chatStarted = !!$chatAdapter;
function currentTopic(topic) {
return [...chatTags, ...chatReferenceTags].includes(topic);
}
$: if (chatStarted) {
afterUpdate(() => {
if (searchElement) searchElement.focus();
});
refreshWidget();
}
let searchElement;
let inputUrl = "";
const isValidUrl = (str) => {
const pattern = new RegExp(
"^(https?:\\/\\/)?" + // protocol
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
"(\\#[-a-z\\d_]*)?$",
"i"
); // fragment locator
return !!pattern.test(str);
};
const startsWithProtocol = (str) => {
return str.startsWith("http://") || str.startsWith("https://");
};
function handleEnterKey(event) {
if (event.keyCode === 13 && isValidUrl(inputUrl)) {
showComments(inputUrl);
}
}
function showComments(str) {
if (!str.startsWith("http://") && !str.startsWith("https://")) {
$url = "https://" + str;
} else {
$url = str;
}
inputUrl = "";
chatType = "GROUP";
chatTags = [];
chatReferenceTags = [$url];
}
function handlePaste(event) {
showComments(event);
inputUrl = "";
}
let refreshKey = 1;
function refreshWidget() {
refreshKey++;
}
</script>
<svelte:head>
<title>DiSseNT - The web's comment section</title>
<meta property="og:url" content="https://dsnt.chat/" />
<meta name="description" content="The web's comment section." />
<meta
property="og:description"
content="The web's comment section."
/>
</svelte:head>
<main>
<div class="panel">
<section class="toolbar">
{#if $url}
<MetaData url={$url} />
{/if}
</section>
<section class="content">
<div class="search-wrapper">
<div class="search">
<div class="search__bar">
{#if !startsWithProtocol(inputUrl)}
<span>https://</span>
{/if}
<input
type="text"
bind:value={inputUrl}
bind:this={searchElement}
placeholder="paste or enter a URL"
on:keyup={handleEnterKey}
on:paste={handlePaste(inputUrl)}
/>
</div>
{#if isValidUrl(inputUrl)}
<button class="search__btn" on:click={showComments(inputUrl)}>show comments</button>
{/if}
</div>
</div>
<div class="brand">
<Brand />
</div>
</section>
</div>
<!-- RIGHT -->
<div class="sidebar">
{#if !chatStarted}
<section>
<KeyPrompt
{websiteOwnerPubkey}
chatConfiguration={{
chatType,
chatId,
chatTags,
chatReferenceTags,
}}
relays={$relays}
/>
</section>
{/if}
{#if chatStarted && $url}
{#each [refreshKey] as key (key)}
<ConnectedWidget
{websiteOwnerPubkey}
chatConfiguration={{
chatType,
chatId,
chatTags,
chatReferenceTags,
}}
relays={$relays}
/>
{/each}
{/if}
</div>
</main>
<style>
.content {
display: flex;
flex-direction: column;
padding: 1rem;
flex-grow: 1;
}
.brand {
flex-grow: 0;
}
.search-wrapper {
flex-grow: 1;
display: flex;
justify-content: center;
flex-direction: column;
}
.search__bar {
display: flex;
border-bottom: 1px solid var(--c-lines);
font-size: 2rem;
align-items: center;
padding: .5rem;
}
.search__bar span{
color: rgba(255, 255, 255, 0.25);
}
.search__bar input {
color: rgba(255, 255, 255);
transition: all .3s ease;
background-color: transparent;
flex-grow: 1;
font-size: inherit;
border: none;
font-weight: inherit;
}
.search__bar input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.search__bar input:focus {
outline: none;
}
</style>

View file

@ -1,123 +0,0 @@
<div
class="w-[1440px] h-[1024px] relative overflow-hidden"
style="background: linear-gradient(to bottom, #381100 0%, #a13000 99.98%, #b93700 99.99%, #ff4c00 100%);"
>
<div class="w-[566px] h-[1024px] absolute left-[874px] top-0 overflow-hidden">
<div
class="flex flex-col justify-start items-start w-[566px] h-[822px] absolute left-0 top-[202px] overflow-hidden bg-[#381100]/20 border-t-0 border-r-0 border-b-0 border-l border-white/50"
>
<div
class="flex flex-col justify-start items-start self-stretch flex-grow-0 flex-shrink-0 overflow-hidden gap-2.5 p-[30px] border-t-0 border-r-0 border-b border-l-0 border-white/50"
>
<div
class="flex justify-start items-start self-stretch flex-grow-0 flex-shrink-0 gap-[26px]"
>
<div
class="flex flex-col justify-start items-center flex-grow-0 flex-shrink-0 relative gap-[9px]"
>
<img
class="flex-grow-0 flex-shrink-0 w-[70px] h-[70px] rounded-[118.52px]"
src=""
/><svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="flex-grow-0 flex-shrink-0"
preserveAspectRatio="xMidYMid meet"
>
<circle cx="20" cy="20" r="19.5" stroke="#FF8A00"></circle>
</svg>
</div>
<div class="flex flex-col justify-start items-start flex-grow relative gap-2.5">
<div
class="flex justify-between items-start self-stretch flex-grow-0 flex-shrink-0 relative pr-[15px]"
>
<p class="flex-grow-0 flex-shrink-0 text-[15px] font-bold text-left text-white">
Name
</p>
<p class="flex-grow-0 flex-shrink-0 text-[15px] font-bold text-left text-white">
17h
</p>
</div>
<p
class="self-stretch flex-grow-0 flex-shrink-0 w-[410px] text-[15px] text-left text-white"
>
Hahaha. There are so many people on the “system” and do side work for cash and do very
very well. Tons of people selling their food cards for cash. Single moms with multiple
baby daddies and a cash side business while also on the system. And so much more. Its
so wide spread.
</p>
</div>
</div>
</div>
</div>
<div
class="w-[566px] h-[202px] absolute left-0 top-0 overflow-hidden bg-[#381100]/20 border-t-0 border-r-0 border-b border-l-0 border-white/50"
>
<p class="absolute left-3.5 top-[122px] text-[55px] font-thin text-left text-white">35</p>
<div class="flex justify-start items-start absolute left-[474px] top-[164px] gap-[7px]">
<div class="flex-grow-0 flex-shrink-0 w-[13px] h-[13px] rounded-[13px] bg-[#d9d9d9]"></div>
<div class="flex-grow-0 flex-shrink-0 w-[13px] h-[13px] rounded-[13px] bg-[#d9d9d9]"></div>
<div class="flex-grow-0 flex-shrink-0 w-[13px] h-[13px] rounded-[13px] bg-[#d9d9d9]"></div>
<div class="flex-grow-0 flex-shrink-0 w-[13px] h-[13px] rounded-[13px] bg-[#d9d9d9]"></div>
</div>
<div class="flex justify-end items-center w-52 absolute left-[339px] top-[17px] gap-2.5">
<p class="flex-grow-0 flex-shrink-0 text-[15px] font-bold text-left text-white">Name</p>
<img
class="flex-grow-0 flex-shrink-0 w-[30px] h-[30px] rounded-[118.52px]"
src=""
/>
</div>
</div>
</div>
<div
class="flex flex-col justify-start items-start w-[874px] h-[1024px] absolute left-0 top-0 overflow-hidden border-t-0 border-r-0 border-b-0 border-l border-white/50"
>
<div
class="flex flex-col justify-start items-start flex-grow-0 flex-shrink-0 h-[202px] w-[874px] relative overflow-hidden gap-2.5 p-4 bg-[#381100]/20 border-t-0 border-r-0 border-b border-l-0 border-white/50"
>
<p class="flex-grow-0 flex-shrink-0 text-3xl text-left text-white">
FileGator - Simple Self-Hosted File Server
</p>
<p class="flex-grow-0 flex-shrink-0 text-[15px] text-left text-white/50">
https://noted.lol/filegator/
</p>
<p class="flex-grow-0 flex-shrink-0 w-[510px] text-[15px] text-left text-white">
FileGator is your personal file butler, serving up a self-hosted buffet of file management
goodness where you're the chef, and your files are the main course all with the added
bonus of keeping your data more locked down than a treasure chest in a dragon's den.
</p>
</div>
<div class="flex-grow-0 flex-shrink-0 w-[874px] h-[822px] relative">
<p class="absolute left-9 top-[657px] text-[81.52728271484375px] text-left">
<span class="text-[81.52728271484375px] text-left text-white">D</span
><span class="text-[81.52728271484375px] text-left text-white/50">i</span
><span class="text-[81.52728271484375px] text-left text-white">s</span
><span class="text-[81.52728271484375px] text-left text-white/50">se</span
><span class="text-[81.52728271484375px] text-left text-white">nt</span>
</p>
<p class="absolute left-9 top-[766px] text-[18.14524269104004px] text-left text-white">
The webs comment section.
</p>
<div
class="flex justify-start items-center w-[659px] h-[86px] absolute left-[55px] top-[211px] gap-2.5 px-5 py-[21px] border-t-0 border-r-0 border-b border-l-0 border-white/50"
>
<p class="flex-grow-0 flex-shrink-0 text-4xl font-extralight text-left text-white/50">
type or paste a web address
</p>
</div>
<div class="w-[86px] h-[86px] absolute left-[713px] top-[210px] bg-black/20"></div>
</div>
</div>
</div>
<style>
/* div { border: solid skyblue 1px; } */
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View file

@ -6,7 +6,8 @@ const config = {
kit: {
adapter: adapter({ out: 'build' }),
},
preprocess: vitePreprocess()
preprocess: vitePreprocess(),
extensions: [".svelte", ".md"],
};
export default config;

View file

@ -1,7 +1,19 @@
import { sveltekit } from '@sveltejs/kit/vite';
// import { sveltekit } from '@sveltejs/kit/vite';
const config = {
plugins: [sveltekit()]
};
// const config = {
// plugins: [sveltekit()]
// };
export default config;
// export default config;
// vite.config.js
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite"
import svelteMd from "vite-plugin-svelte-md";
export default defineConfig({
plugins: [
svelteMd(), // <--
sveltekit(),
],
});