1st commit of dsnt changes

This commit is contained in:
Spencer Flagg 2023-08-29 14:30:41 +02:00
parent 053a1c8ed2
commit ad30d14a66
21 changed files with 2902 additions and 72 deletions

19
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "nostri.chat",
"version": "0.1.5",
"name": "dnst",
"version": "0.3.14159",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nostri.chat",
"version": "0.1.5",
"name": "dsnt",
"version": "0.3.14159",
"dependencies": {
"@nostr-connect/connect": "^0.2.3",
"@nostr-dev-kit/ndk": "^0.3.32",
@ -17,6 +17,7 @@
"@sveltejs/adapter-node": "^1.1.7",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"dayjs": "^1.11.9",
"emoji-regex": "^10.2.1",
"eventemitter3": "^5.0.0",
"light-bolt11-decoder": "^3.0.0",
@ -2440,6 +2441,11 @@
"node": ">= 12"
}
},
"node_modules/dayjs": {
"version": "1.11.9",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -10273,6 +10279,11 @@
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
},
"dayjs": {
"version": "1.11.9",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

View file

@ -1,5 +1,5 @@
{
"name": "nostri.chat",
"name": "dsnt",
"version": "0.3.14159",
"scripts": {
"dev": "vite dev",
@ -34,6 +34,7 @@
"@sveltejs/adapter-node": "^1.1.7",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"dayjs": "^1.11.9",
"emoji-regex": "^10.2.1",
"eventemitter3": "^5.0.0",
"light-bolt11-decoder": "^3.0.0",

34
src/Brand.svelte Normal file
View file

@ -0,0 +1,34 @@
<p class="logo">
<span>D</span><span class="letter--dim">i</span><span>s</span><span class="letter--dim">se</span><span>nt</span>
</p>
<p class="motto">The webs comment section.</p>
<style>
p {
line-height: 1;
}
.logo {
font-family: 'Koulen', sans-serif;
letter-spacing: -.1em;
color: var(--c-bright);
font-size: 82px;
margin: 0;
line-height: .8;
}
.logo span{
z-index: 1;
position: relative;
display: inline-block;
}
.logo span.letter--dim{
color: #cc9680;
z-index: 0;
}
.motto{
color: var(--c-2);
margin: 0;
}
</style>

View file

@ -239,7 +239,7 @@
}
if (!name) { name = `[${pubkey.slice(0, 6)}]`; }
if (!name) { name = `Anonymous [${pubkey.slice(0, 6)}]`; }
return name;
}
@ -321,6 +321,8 @@
{#if event.deleted}
👆 deleted
{/if}
{:else}
<p>no comments</p>
{/each}
{/if}
</div>
@ -328,7 +330,7 @@
<div class="flex flex-col">
<div class="
<!-- <div class="
border-y border-y-slate-200
-mx-4 my-2 bg-slate-100 text-black text-sm
px-4 py-2
@ -343,7 +345,7 @@
<b>Public notes:</b>
your followers see your messages on their timeline
{/if}
</div>
</div> -->
<div class="flex flex-row gap-2 -mx-1">
<textarea
@ -357,7 +359,7 @@
rounded-xl
text-gray-600
border
" placeholder="Say hello!"
" placeholder="leave a comment"
rows=1
on:keydown={inputKeyDown}
></textarea>

480
src/ConnectedWidget2.svelte Normal file
View file

@ -0,0 +1,480 @@
<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

@ -106,94 +106,52 @@
}
</script>
<h1 class="font-bold text-xl mb-3">
<h1 class="">
How would you like to connect?
</h1>
{#if publicKey}
<p class="text-gray-400 mb-3 font-bold">
<p class="">
Nostr Connect is a WIP, not fully implemented yet!
</p>
<p class="text-gray-400 mb-3">
<p class="">
You are currently connected with the following public key:
<span>{publicKey}</span>
</p>
{/if}
{#if nip46URI}
<p class="text-gray-600 mb-3">
<p class="">
Scan this with your Nostr Connect (click to copy to clipboard)
</p>
<div class="bg-white w-full p-3"
<div class=""
on:click|preventDefault={Nip46Copy}>
<!-- <QR text={nip46URI} /> -->
</div>
<button class="
bg-purple-900
hover:bg-purple-700
w-full
p-2
rounded-xl
text-center
font-regular
text-white
" on:click|preventDefault={() => { nip46URI = null; }}>
<button class="" on:click|preventDefault={() => { nip46URI = null; }}>
Cancel
</button>
{:else if !publicKey}
<div class="flex flex-col gap-1">
<div class="">
{#if hasNostrNip07}
<button class="
bg-purple-900
hover:bg-purple-700
w-full
p-4
rounded-xl
text-center
font-regular
text-gray-200
" on:click|preventDefault={useNip07}>
<button class="" on:click|preventDefault={useNip07}>
Browser Extension (NIP-07)
</button>
{/if}
<button class="
bg-purple-900
hover:bg-purple-700
w-full
p-4
rounded-xl
text-center
font-regular
text-gray-200
" on:click|preventDefault={useNip46}>
<button class="" on:click|preventDefault={useNip46}>
Nostr Connect (NIP-46)
</button>
<button class="
bg-purple-900
hover:bg-purple-700
w-full
p-4
rounded-xl
text-center
font-regular
text-gray-200
" on:click|preventDefault={useDiscardableKeys}>
<button class="" on:click|preventDefault={useDiscardableKeys}>
Anonymous
<span class="text-xs text-gray-300">
<span class="">
(Ephemeral Keys)
</span>
</button>
</div>
{/if}
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>
{/if}

160
src/MetaData.svelte Normal file
View file

@ -0,0 +1,160 @@
<script>
import { onMount } from 'svelte';
export let url;
let title = null;
let description = null;
let thumbnail = null;
let publishDate = null;
let isLoading = true;
let isError = false;
let urlAbbr;
let imageLoaded = false;
let imageError = false;
function onImageLoad() {
imageLoaded = true;
}
function onImageError() {
imageError = true;
}
$: {
const cleanedUrl = typeof url === 'string' ? url.replace(/^https?:\/\//, '') : '';
urlAbbr = cleanedUrl.length > 20 ? cleanedUrl.substring(0, 20) + '...' : cleanedUrl;
}
$: fetchMetaData(url);
onMount(async () => {
fetchMetaData(url);
});
async function fetchMetaData(url){
isError = false;
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error("Couldn't fetch URL");
}
const text = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// Check multiple possible meta tags for each property
const titleTags = ['title', 'og:title', 'twitter:title'];
const descriptionTags = ['description', 'og:description', 'twitter:description'];
const thumbnailTags = ['image', 'og:image', 'twitter:image'];
const publishDateTags = ['article:published_time', 'published_time', 'date'];
title = getMetaContent(doc, titleTags);
description = getMetaContent(doc, descriptionTags);
thumbnail = getMetaContent(doc, thumbnailTags);
publishDate = getMetaContent(doc, publishDateTags);
} catch (error) {
isError = true;
console.log('Error fetching URL:', error);
} finally {
isLoading = false;
}
}
function getMetaContent(doc, tagNames) {
for (const tagName of tagNames) {
const element = doc.querySelector(`meta[name="${tagName}"], meta[property="${tagName}"]`);
if (element && element.content) {
return element.content;
}
}
return null;
}
</script>
{#if isLoading}
<div class="metadata__text">
<h1>{urlAbbr}</h1>
<p>Loading...</p>
</div>
{:else if isError}
<div class="metadata__text">
<h1>{urlAbbr}</h1>
<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>
<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}
</div>
{/if}
<style>
h1 {
/* font-size: 2.5rem; */
color: var(--c-3);
font-size: 1.5rem;
color: var(--c-3);
font-weight: 500;
margin: 0;
}
a{
color: var(--c-3);
}
a:visited{
color: var(--c-3);
}
a:hover{
color: var(--c-bright);
}
p {
color: var(--c-3);
max-width: 60ch;
overflow: hidden;
overflow-wrap: anywhere;
}
.metadata__text {
flex-grow: 1;
}
.metadata__thumbnail {
flex-grow:0;
width: fit-content;
display: flex;
align-items: center;
}
img{
max-height: 100%;
outline: 1px solid rgba(255, 255, 255, .5);
border-radius: .5rem;
}
</style>

View file

@ -113,7 +113,7 @@
'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 absolute ml-5 mt-10 z-10">
flex items-center ml-5 mt-10 z-10">
{#if zappingIt}
{#if mobilePR}
<div class="flex flex-col gap-3 w-full">

325
src/NostrNote2.svelte Normal file
View file

@ -0,0 +1,325 @@
<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>

47
src/Relays.svelte Normal file
View file

@ -0,0 +1,47 @@
<script>
import { createEventDispatcher } from 'svelte';
// Accept relays as a prop
export let relays = [];
// Create a local copy
let localRelays = [...relays];
const dispatch = createEventDispatcher();
let showRelays = false;
let newRelay = '';
function toggleRelays() {
showRelays = !showRelays;
}
function addRelay() {
if (newRelay) {
localRelays.push(newRelay);
newRelay = '';
dispatch('update', localRelays);
}
}
function removeRelay(index) {
localRelays.splice(index, 1);
dispatch('update', localRelays);
}
</script>
<button on:click={toggleRelays}>Relays <span>{relays.length}</span></button>
{#if showRelays}
<ul>
{#each relays as relay, index}
<li>
{relay} <button on:click={() => removeRelay(index)}>Remove</button>
</li>
{/each}
<li>
<input type="text" bind:value={newRelay} placeholder="Add new relay..." />
<button on:click={addRelay}>Add</button>
</li>
</ul>
{/if}

124
src/app.css Normal file
View file

@ -0,0 +1,124 @@
@import url("https://fonts.googleapis.com/css2?family=Work+Sans:wght@300;400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@100;300;400;500;600;700&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Koulen&display=swap');
:root {
--c-lines: rgba(255, 255, 255, 0.3);
--c-2: #cc9680;
--c-3: rgba(255, 255, 255, 0.7);
--c-bright: white;
--c-marker: orange;
}
body {
margin: 0;
}
/*
main,
section,
div {
border: dotted orange .5px;
}
*/
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;
flex-direction: column;
}
.panel .toolbar {
background-color: rgb(56, 17, 0, 0.2);
}
.sidebar {
flex-shrink: 0;
width: 566px;
display: flex;
flex-direction: column;
background-color: rgb(56, 17, 0, 0.2);
border-left: 1px solid var(--c-lines);
}
.toolbar {
display: flex;
flex-shrink: 0;
gap: 0.5rem;
padding: 1rem;
height: 10rem;
border-bottom: 1px solid var(--c-lines);
color: var(--c-bright);
}
.sidebar .toolbar {
flex-direction: column;
}
button {
outline: 1px solid var(--c-lines);
color: var(--c-lines);
padding: 1em 2rem;
}
/* Webkit browsers like Chrome, Safari */
.content--scrolling::-webkit-scrollbar {
width: 1rem; /* Set the width */
}
.content--scrolling::-webkit-scrollbar-track {
background: transparent; /* Make the track transparent */
}
.content--scrolling::-webkit-scrollbar-thumb {
background: var(--c-lines); /* Set the thumb color */
}
.content--scrolling::-webkit-scrollbar-thumb:hover {
background: var(--c-bright); /* Change the thumb color when hovered */
}
/* Internet Explorer */
.content--scrolling {
position: relative;
scrollbar-face-color: var(--c-lines);
scrollbar-track-color: transparent;
position: relative;
}
.content--scrolling::before {
content: "";
position: sticky;
top: 0;
right: 0;
width: 1px;
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;
}
h1 {
color: var(--c-bright);
}

View file

@ -6,10 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<!-- bg-white dark:bg-black -->
<body class="
">
<body>
<div>%sveltekit.body%</div>
</body>
</html>

View file

@ -1,7 +1,49 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
// Your other stores
export const chatAdapter = writable(null);
export const chatData = writable({ events: [], profiles: {}});
export const selectedMessage = writable(null);
export const zappingMessage = writable(null);
export const zapsPerMessage = writable({});
export const zapsPerMessage = writable({});
// Default values
const defaultRelays = [
"wss://relay.f7z.io",
"wss://nos.lol",
"wss://relay.nostr.band",
"wss://nostr-pub.wellorder.net",
"wss://relay.damus.io",
"wss://relay.f7z.io",
"wss://eden.nostr.land",
"wss://offchain.pub",
"wss://soloco.nl"
];
// Read initial state from sessionStorage if available, otherwise use default values
const initialUrl = browser && sessionStorage.getItem('url') !== null
? sessionStorage.getItem('url')
: null;
const initialRelays = browser && sessionStorage.getItem('relays') !== null
? JSON.parse(sessionStorage.getItem('relays'))
: defaultRelays;
// Create the writable stores
export const url = writable(initialUrl);
export const relays = writable(initialRelays);
// Function to synchronize the URL with sessionStorage
url.subscribe((currentUrl) => {
if (browser) {
sessionStorage.setItem("url", currentUrl);
}
});
// Function to synchronize the relays with sessionStorage
relays.subscribe((currentRelays) => {
if (browser) {
sessionStorage.setItem("relays", JSON.stringify(currentRelays));
}
});

635
src/milligram.css Normal file
View file

@ -0,0 +1,635 @@
/*!
* Milligram v1.4.1
* https://milligram.io
*
* Copyright (c) 2020 CJ Patoilo
* Licensed under the MIT license
*/
*,
*:after,
*:before {
box-sizing: inherit;
}
html {
box-sizing: border-box;
font-size: 62.5%;
}
body {
color: #606c76;
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
font-size: 1.6em;
font-weight: 300;
letter-spacing: .01em;
line-height: 1.6;
}
blockquote {
border-left: 0.3rem solid #d1d1d1;
margin-left: 0;
margin-right: 0;
padding: 1rem 1.5rem;
}
blockquote *:last-child {
margin-bottom: 0;
}
.button,
button,
input[type='button'],
input[type='reset'],
input[type='submit'] {
background-color: #9b4dca;
border: 0.1rem solid #9b4dca;
border-radius: .4rem;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 1.1rem;
font-weight: 700;
height: 3.8rem;
letter-spacing: .1rem;
line-height: 3.8rem;
padding: 0 3.0rem;
text-align: center;
text-decoration: none;
text-transform: uppercase;
white-space: nowrap;
}
.button:focus, .button:hover,
button:focus,
button:hover,
input[type='button']:focus,
input[type='button']:hover,
input[type='reset']:focus,
input[type='reset']:hover,
input[type='submit']:focus,
input[type='submit']:hover {
background-color: #606c76;
border-color: #606c76;
color: #fff;
outline: 0;
}
.button[disabled],
button[disabled],
input[type='button'][disabled],
input[type='reset'][disabled],
input[type='submit'][disabled] {
cursor: default;
opacity: .5;
}
.button[disabled]:focus, .button[disabled]:hover,
button[disabled]:focus,
button[disabled]:hover,
input[type='button'][disabled]:focus,
input[type='button'][disabled]:hover,
input[type='reset'][disabled]:focus,
input[type='reset'][disabled]:hover,
input[type='submit'][disabled]:focus,
input[type='submit'][disabled]:hover {
background-color: #9b4dca;
border-color: #9b4dca;
}
.button.button-outline,
button.button-outline,
input[type='button'].button-outline,
input[type='reset'].button-outline,
input[type='submit'].button-outline {
background-color: transparent;
color: #9b4dca;
}
.button.button-outline:focus, .button.button-outline:hover,
button.button-outline:focus,
button.button-outline:hover,
input[type='button'].button-outline:focus,
input[type='button'].button-outline:hover,
input[type='reset'].button-outline:focus,
input[type='reset'].button-outline:hover,
input[type='submit'].button-outline:focus,
input[type='submit'].button-outline:hover {
background-color: transparent;
border-color: #606c76;
color: #606c76;
}
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
button.button-outline[disabled]:focus,
button.button-outline[disabled]:hover,
input[type='button'].button-outline[disabled]:focus,
input[type='button'].button-outline[disabled]:hover,
input[type='reset'].button-outline[disabled]:focus,
input[type='reset'].button-outline[disabled]:hover,
input[type='submit'].button-outline[disabled]:focus,
input[type='submit'].button-outline[disabled]:hover {
border-color: inherit;
color: #9b4dca;
}
.button.button-clear,
button.button-clear,
input[type='button'].button-clear,
input[type='reset'].button-clear,
input[type='submit'].button-clear {
background-color: transparent;
border-color: transparent;
color: #9b4dca;
}
.button.button-clear:focus, .button.button-clear:hover,
button.button-clear:focus,
button.button-clear:hover,
input[type='button'].button-clear:focus,
input[type='button'].button-clear:hover,
input[type='reset'].button-clear:focus,
input[type='reset'].button-clear:hover,
input[type='submit'].button-clear:focus,
input[type='submit'].button-clear:hover {
background-color: transparent;
border-color: transparent;
color: #606c76;
}
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
button.button-clear[disabled]:focus,
button.button-clear[disabled]:hover,
input[type='button'].button-clear[disabled]:focus,
input[type='button'].button-clear[disabled]:hover,
input[type='reset'].button-clear[disabled]:focus,
input[type='reset'].button-clear[disabled]:hover,
input[type='submit'].button-clear[disabled]:focus,
input[type='submit'].button-clear[disabled]:hover {
color: #9b4dca;
}
code {
background: #f4f5f6;
border-radius: .4rem;
font-size: 86%;
margin: 0 .2rem;
padding: .2rem .5rem;
white-space: nowrap;
}
pre {
background: #f4f5f6;
border-left: 0.3rem solid #9b4dca;
overflow-y: hidden;
}
pre > code {
border-radius: 0;
display: block;
padding: 1rem 1.5rem;
white-space: pre;
}
hr {
border: 0;
border-top: 0.1rem solid #f4f5f6;
margin: 3.0rem 0;
}
input[type='color'],
input[type='date'],
input[type='datetime'],
input[type='datetime-local'],
input[type='email'],
input[type='month'],
input[type='number'],
input[type='password'],
input[type='search'],
input[type='tel'],
input[type='text'],
input[type='url'],
input[type='week'],
input:not([type]),
textarea,
select {
-webkit-appearance: none;
background-color: transparent;
border: 0.1rem solid #d1d1d1;
border-radius: .4rem;
box-shadow: none;
box-sizing: inherit;
height: 3.8rem;
padding: .6rem 1.0rem .7rem;
width: 100%;
}
input[type='color']:focus,
input[type='date']:focus,
input[type='datetime']:focus,
input[type='datetime-local']:focus,
input[type='email']:focus,
input[type='month']:focus,
input[type='number']:focus,
input[type='password']:focus,
input[type='search']:focus,
input[type='tel']:focus,
input[type='text']:focus,
input[type='url']:focus,
input[type='week']:focus,
input:not([type]):focus,
textarea:focus,
select:focus {
border-color: #9b4dca;
outline: 0;
}
select {
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%23d1d1d1" d="M0,0l6,8l6-8"/></svg>') center right no-repeat;
padding-right: 3.0rem;
}
select:focus {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%239b4dca" d="M0,0l6,8l6-8"/></svg>');
}
select[multiple] {
background: none;
height: auto;
}
textarea {
min-height: 6.5rem;
}
label,
legend {
display: block;
font-size: 1.6rem;
font-weight: 700;
margin-bottom: .5rem;
}
fieldset {
border-width: 0;
padding: 0;
}
input[type='checkbox'],
input[type='radio'] {
display: inline;
}
.label-inline {
display: inline-block;
font-weight: normal;
margin-left: .5rem;
}
.container {
margin: 0 auto;
max-width: 112.0rem;
padding: 0 2.0rem;
position: relative;
width: 100%;
}
.row {
display: flex;
flex-direction: column;
padding: 0;
width: 100%;
}
.row.row-no-padding {
padding: 0;
}
.row.row-no-padding > .column {
padding: 0;
}
.row.row-wrap {
flex-wrap: wrap;
}
.row.row-top {
align-items: flex-start;
}
.row.row-bottom {
align-items: flex-end;
}
.row.row-center {
align-items: center;
}
.row.row-stretch {
align-items: stretch;
}
.row.row-baseline {
align-items: baseline;
}
.row .column {
display: block;
flex: 1 1 auto;
margin-left: 0;
max-width: 100%;
width: 100%;
}
.row .column.column-offset-10 {
margin-left: 10%;
}
.row .column.column-offset-20 {
margin-left: 20%;
}
.row .column.column-offset-25 {
margin-left: 25%;
}
.row .column.column-offset-33, .row .column.column-offset-34 {
margin-left: 33.3333%;
}
.row .column.column-offset-40 {
margin-left: 40%;
}
.row .column.column-offset-50 {
margin-left: 50%;
}
.row .column.column-offset-60 {
margin-left: 60%;
}
.row .column.column-offset-66, .row .column.column-offset-67 {
margin-left: 66.6666%;
}
.row .column.column-offset-75 {
margin-left: 75%;
}
.row .column.column-offset-80 {
margin-left: 80%;
}
.row .column.column-offset-90 {
margin-left: 90%;
}
.row .column.column-10 {
flex: 0 0 10%;
max-width: 10%;
}
.row .column.column-20 {
flex: 0 0 20%;
max-width: 20%;
}
.row .column.column-25 {
flex: 0 0 25%;
max-width: 25%;
}
.row .column.column-33, .row .column.column-34 {
flex: 0 0 33.3333%;
max-width: 33.3333%;
}
.row .column.column-40 {
flex: 0 0 40%;
max-width: 40%;
}
.row .column.column-50 {
flex: 0 0 50%;
max-width: 50%;
}
.row .column.column-60 {
flex: 0 0 60%;
max-width: 60%;
}
.row .column.column-66, .row .column.column-67 {
flex: 0 0 66.6666%;
max-width: 66.6666%;
}
.row .column.column-75 {
flex: 0 0 75%;
max-width: 75%;
}
.row .column.column-80 {
flex: 0 0 80%;
max-width: 80%;
}
.row .column.column-90 {
flex: 0 0 90%;
max-width: 90%;
}
.row .column .column-top {
align-self: flex-start;
}
.row .column .column-bottom {
align-self: flex-end;
}
.row .column .column-center {
align-self: center;
}
@media (min-width: 40rem) {
.row {
flex-direction: row;
margin-left: -1.0rem;
width: calc(100% + 2.0rem);
}
.row .column {
margin-bottom: inherit;
padding: 0 1.0rem;
}
}
a {
color: #9b4dca;
text-decoration: none;
}
a:focus, a:hover {
color: #606c76;
}
dl,
ol,
ul {
list-style: none;
margin-top: 0;
padding-left: 0;
}
dl dl,
dl ol,
dl ul,
ol dl,
ol ol,
ol ul,
ul dl,
ul ol,
ul ul {
font-size: 90%;
margin: 1.5rem 0 1.5rem 3.0rem;
}
ol {
list-style: decimal inside;
}
ul {
list-style: circle inside;
}
.button,
button,
dd,
dt,
li {
margin-bottom: 1.0rem;
}
fieldset,
input,
select,
textarea {
margin-bottom: 1.5rem;
}
blockquote,
dl,
figure,
form,
ol,
p,
pre,
table,
ul {
margin-bottom: 2.5rem;
}
table {
border-spacing: 0;
display: block;
overflow-x: auto;
text-align: left;
width: 100%;
}
td,
th {
border-bottom: 0.1rem solid #e1e1e1;
padding: 1.2rem 1.5rem;
}
td:first-child,
th:first-child {
padding-left: 0;
}
td:last-child,
th:last-child {
padding-right: 0;
}
@media (min-width: 40rem) {
table {
display: table;
overflow-x: initial;
}
}
b,
strong {
font-weight: bold;
}
p {
margin-top: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 300;
letter-spacing: -.1rem;
margin-bottom: 2.0rem;
margin-top: 0;
}
h1 {
font-size: 4.6rem;
line-height: 1.2;
}
h2 {
font-size: 3.6rem;
line-height: 1.25;
}
h3 {
font-size: 2.8rem;
line-height: 1.3;
}
h4 {
font-size: 2.2rem;
letter-spacing: -.08rem;
line-height: 1.35;
}
h5 {
font-size: 1.8rem;
letter-spacing: -.05rem;
line-height: 1.5;
}
h6 {
font-size: 1.6rem;
letter-spacing: 0;
line-height: 1.4;
}
img {
max-width: 100%;
}
.clearfix:after {
clear: both;
content: ' ';
display: table;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
/*# sourceMappingURL=milligram.css.map */

351
src/normalize.css vendored Normal file
View file

@ -0,0 +1,351 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
appearance: textfield;
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View file

@ -0,0 +1,7 @@
<script>
//import '../normalize.css'
//import '../milligram.css'
import '../app.css'
</script>
<slot />

View file

@ -281,6 +281,50 @@
>
<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>

View file

@ -0,0 +1,254 @@
<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

@ -0,0 +1,235 @@
<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

@ -0,0 +1,123 @@
<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

@ -10,9 +10,9 @@ let chatReferenceTags = script.getAttribute('data-chat-reference-tags');
let relays = script.getAttribute('data-relays');
script.parentNode.insertBefore(div, script);
if (!relays) {
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'
}
// if (!relays) {
// 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'
// }
relays = relays.split(',');
chatTags = chatTags ? chatTags.split(',') : [];