Improve join/leave, publish messages

This commit is contained in:
Jon Staab
2024-08-23 13:33:36 -07:00
parent d6fa0a85bc
commit f12e7ef77c
24 changed files with 412 additions and 297 deletions

244
package-lock.json generated
View File

@@ -12,7 +12,15 @@
"@noble/hashes": "^1.4.0",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sveltejs/adapter-static": "^3.0.4",
"@tiptap/starter-kit": "^2.6.4",
"@tiptap/extension-code": "^2.6.6",
"@tiptap/extension-code-block": "^2.6.6",
"@tiptap/extension-document": "^2.6.6",
"@tiptap/extension-dropcursor": "^2.6.6",
"@tiptap/extension-gapcursor": "^2.6.6",
"@tiptap/extension-hard-break": "^2.6.6",
"@tiptap/extension-history": "^2.6.6",
"@tiptap/extension-paragraph": "^2.6.6",
"@tiptap/extension-text": "^2.6.6",
"@tiptap/suggestion": "^2.6.4",
"@types/throttle-debounce": "^5.0.2",
"@welshman/lib": "^0.0.15",
@@ -1116,39 +1124,15 @@
}
},
"node_modules/@tiptap/core": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.6.4.tgz",
"integrity": "sha512-lv+JyBI+5C6C7BMLYg2bloB00HvAZkcvgO3CzmFia28Vtt1P9yhS44elvBemhUf7IP7Hu12FUzDWY+2GQqiqkw==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.6.6.tgz",
"integrity": "sha512-VO5qTsjt6rwworkuo0s5AqYMfDA0ZwiTiH6FHKFSu2G/6sS7HKcc/LjPq+5Legzps4QYdBDl3W28wGsGuS1GdQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "^2.6.4"
}
},
"node_modules/@tiptap/extension-blockquote": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.6.4.tgz",
"integrity": "sha512-BzeQ52qHL4AEryPqgvPNRJ2siSTfSi2s3k7hVC29QYUTOidLSSDWVihn7lzJoBnqDMAOYj7yUhnEUEdjvOFGqw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
}
},
"node_modules/@tiptap/extension-bold": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.6.4.tgz",
"integrity": "sha512-DIKUiO2aqO9D3dAQngBacWk/vYwDY13+q3t5dlawRTCIHxgV571vGb+YbcLswbWPQjOziIBc5QgwUVZLjA8OkA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
"@tiptap/pm": "^2.6.6"
}
},
"node_modules/@tiptap/extension-bubble-menu": {
@@ -1168,66 +1152,54 @@
"@tiptap/pm": "^2.6.4"
}
},
"node_modules/@tiptap/extension-bullet-list": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.6.4.tgz",
"integrity": "sha512-SsEqWNvbcLjgPYQXWT+gm8Mdtd6SnM9kr5xdfOvfe9W1RCYi7U7SQjaYGLGQXuy3E8NDugNiG+ss2POMj4RaUQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
}
},
"node_modules/@tiptap/extension-code": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.6.4.tgz",
"integrity": "sha512-qCt/CRhV+s1E9XVCDxGgFwyQRjcLsqBuY5UTwH3Zp8MIBniyLyJDD0Rv9DgvVqalzRC8RoRxVey9Al3YhYNqsw==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.6.6.tgz",
"integrity": "sha512-JrEFKsZiLvfvOFhOnnrpA0TzCuJjDeysfbMeuKUZNV4+DhYOL28d39H1++rEtJAX0LcbBU60oC5/PrlU9SpvRQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
"@tiptap/core": "^2.6.6"
}
},
"node_modules/@tiptap/extension-code-block": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.6.4.tgz",
"integrity": "sha512-dnZYiKVNdHfqZqYgoCElLk8ETLlV3Q0rw3IVDKDTwrhanSSooGfkVts/Gn/jtJUIulRdu8lH/0qZCgM4ihznfw==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.6.6.tgz",
"integrity": "sha512-1YLp/zHMHSkE2xzht8nPR6T4sQJJ3ket798czxWuQEbetFv/l0U/mpiPpYSLObj6oTAoqYZ0kWXZj5eQSpPB8Q==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4",
"@tiptap/pm": "^2.6.4"
"@tiptap/core": "^2.6.6",
"@tiptap/pm": "^2.6.6"
}
},
"node_modules/@tiptap/extension-document": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.6.4.tgz",
"integrity": "sha512-fEQzou6J/w7GWiMqxxiwX2TEB6hgjBsImkHCxU05a4IOnIkzC8C9pV+NWa8u1LGvbERmVPBQqWYJG6phDhtYkg==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.6.6.tgz",
"integrity": "sha512-6qlH5VWzLHHRVeeciRC6C4ZHpMsAGPNG16EF53z0GeMSaaFD/zU3B239QlmqXmLsAl8bpf8Bn93N0t2ABUvScw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
"@tiptap/core": "^2.6.6"
}
},
"node_modules/@tiptap/extension-dropcursor": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.6.4.tgz",
"integrity": "sha512-maTQi2R63i1S3CCJTjyuHMpk0BvnFuUxq7krZ3LBCOJgUeS78PF/XPirbbR7s2jOVsHK77LYsgdoS3ApDu1zdQ==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.6.6.tgz",
"integrity": "sha512-O6CeKriA9uyHsg7Ui4z5ZjEWXQxrIL+1zDekffW0wenGC3G4LUsCzAiFS4LSrR9a3u7tnwqGApW10rdkmCGF4w==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4",
"@tiptap/pm": "^2.6.4"
"@tiptap/core": "^2.6.6",
"@tiptap/pm": "^2.6.6"
}
},
"node_modules/@tiptap/extension-floating-menu": {
@@ -1261,66 +1233,41 @@
}
},
"node_modules/@tiptap/extension-gapcursor": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.6.4.tgz",
"integrity": "sha512-g5fa1RLNpFZoiE5PIvG/pFIz88CvtiWkBUp5OOYrPxNzByazcbBsBI8Sa5ptDVrbDqerayUZYAVFPhXnq7MSlQ==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.6.6.tgz",
"integrity": "sha512-O2lQ2t0X0Vsbn3yLWxFFHrXY6C2N9Y6ZF/M7LWzpcDTUZeWuhoNkFE/1yOM0h6ZX1DO2A9hNIrKpi5Ny8yx+QA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4",
"@tiptap/pm": "^2.6.4"
"@tiptap/core": "^2.6.6",
"@tiptap/pm": "^2.6.6"
}
},
"node_modules/@tiptap/extension-hard-break": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.6.4.tgz",
"integrity": "sha512-kBGGSBtp9oQlRBH7PfRvhbrauEphiJEuFUP9n/amAbrrNSabwmvBgyMl6wFXgMdfHF6CSv2YDgndE1sk8SjPSg==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.6.6.tgz",
"integrity": "sha512-bsUuyYBrMDEiudx1dOQSr9MzKv13m0xHWrOK+DYxuIDYJb5g+c9un5cK7Js+et/HEYYSPOoH/iTW6h+4I5YeUg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
}
},
"node_modules/@tiptap/extension-heading": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.6.4.tgz",
"integrity": "sha512-GHwDguzRXRrB5htGPx6T0f0uN9RPAkjbjrl28T7LFXX5Lb2XO+Esr1l4LNsTU49H4wR9nL/89ZjEcd36BUWkog==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
"@tiptap/core": "^2.6.6"
}
},
"node_modules/@tiptap/extension-history": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.6.4.tgz",
"integrity": "sha512-Hr3SrvMsyDHKcsF4u3QPdY/NBYG9V0g5pPmZs/tdysXot3NUdkEYowjs9K9o5osKom364KjxQS0c9mOjyeKu1g==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.6.6.tgz",
"integrity": "sha512-tPTzAmPGqMX5Bd5H8lzRpmsaMvB9DvI5Dy2za/VQuFtxgXmDiFVgHRkRXIuluSkPTuANu84XBOQ0cBijqY8x4w==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4",
"@tiptap/pm": "^2.6.4"
}
},
"node_modules/@tiptap/extension-horizontal-rule": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.6.4.tgz",
"integrity": "sha512-lL29Hxsj1qFwRqtg41JlBOK/hmN+qnwIWvNCyZpKEVHs7d0iELj2REB/7R1KKAAdsvYo7pJrgqwBd1Ph6xRLpw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4",
"@tiptap/pm": "^2.6.4"
"@tiptap/core": "^2.6.6",
"@tiptap/pm": "^2.6.6"
}
},
"node_modules/@tiptap/extension-image": {
@@ -1335,18 +1282,6 @@
"@tiptap/core": "^2.6.4"
}
},
"node_modules/@tiptap/extension-italic": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.6.4.tgz",
"integrity": "sha512-XG/zaKVuorKr1vGEWEgLQTnQwOpNn/JyGxO7oC7wfYx5eYpbbCtMTEMvuqNvkm7kpvVAUx3ugi/D8DWyWZEtYg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
}
},
"node_modules/@tiptap/extension-link": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.6.4.tgz",
@@ -1363,70 +1298,34 @@
"@tiptap/pm": "^2.6.4"
}
},
"node_modules/@tiptap/extension-list-item": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.6.4.tgz",
"integrity": "sha512-NLP0nshX8eCZMLospdCsUApUQHPL1+T/MIi/Hhr0aNeaAg7KwBNH8/rFPuxPNs4BQkHOCuYq4Fm+klkebkFYJA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
}
},
"node_modules/@tiptap/extension-ordered-list": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.6.4.tgz",
"integrity": "sha512-ecAEFpRKZc+b3f54EGvaRp7hsVza2i1nRhxHoPElqVR5DiCCSuSgAPCsKhUUT1rKweK9h56HiC4xswAyFrU5Ag==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
}
},
"node_modules/@tiptap/extension-paragraph": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.6.4.tgz",
"integrity": "sha512-JVlvhZPzjz0Q+29KmnrmLr3A3SvAMfKOZxbZZVnzee6vtI6rqjdYGBOtyyyWwrAliNQB6GkHiKmT3GxH76dz7A==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.6.6.tgz",
"integrity": "sha512-fD/onCr16UQWx+/xEmuFC2MccZZ7J5u4YaENh8LMnAnBXf78iwU7CAcmuc9rfAEO3qiLoYGXgLKiHlh2ZfD4wA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
}
},
"node_modules/@tiptap/extension-strike": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.6.4.tgz",
"integrity": "sha512-EV4hEA5qnRtKViaLKcucFvXP9xEUJOFgpFeOrp2xIgSXJLSmutkaDfz7nxJ2RLzwwYvPfWUL7ay97JSCzSuaIA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
"@tiptap/core": "^2.6.6"
}
},
"node_modules/@tiptap/extension-text": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.6.4.tgz",
"integrity": "sha512-QfspuCTTpmFrSLbDs2z/0W7GLaoNanwj4OCKPSPz5XcraZJgFLsWAqZxZE4aLgZbJH2hcGWMe5ZHmvLf5dJogw==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.6.6.tgz",
"integrity": "sha512-e84uILnRzNzcwK1DVQNpXVmBG1Cq3BJipTOIDl1LHifOok7MBjhI/X+/NR0bd3N2t6gmDTWi63+4GuJ5EeDmsg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.6.4"
"@tiptap/core": "^2.6.6"
}
},
"node_modules/@tiptap/pm": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.6.4.tgz",
"integrity": "sha512-k/AyigUioZVxFTcF7kWcUh5xeOV0bdGzHz+wmtP33md2jo8SJP29yEZ4Kshvk0IcFnVFEDrsfKiGhLRWpKx+YQ==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.6.6.tgz",
"integrity": "sha512-56FGLPn3fwwUlIbLs+BO21bYfyqP9fKyZQbQyY0zWwA/AG2kOwoXaRn7FOVbjP6CylyWpFJnpRRmgn694QKHEg==",
"dependencies": {
"prosemirror-changeset": "^2.2.1",
"prosemirror-collab": "^1.3.1",
@@ -1452,37 +1351,6 @@
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tiptap/starter-kit": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.6.4.tgz",
"integrity": "sha512-uvGXOI6h+AjyyOgJOmBSFrDR7xJ841+gtwzGbAolVM2a7LCEkocyHjLBWFYVfQu2vvMIqA63+0+yAsw6ghwUgw==",
"dependencies": {
"@tiptap/core": "^2.6.4",
"@tiptap/extension-blockquote": "^2.6.4",
"@tiptap/extension-bold": "^2.6.4",
"@tiptap/extension-bullet-list": "^2.6.4",
"@tiptap/extension-code": "^2.6.4",
"@tiptap/extension-code-block": "^2.6.4",
"@tiptap/extension-document": "^2.6.4",
"@tiptap/extension-dropcursor": "^2.6.4",
"@tiptap/extension-gapcursor": "^2.6.4",
"@tiptap/extension-hard-break": "^2.6.4",
"@tiptap/extension-heading": "^2.6.4",
"@tiptap/extension-history": "^2.6.4",
"@tiptap/extension-horizontal-rule": "^2.6.4",
"@tiptap/extension-italic": "^2.6.4",
"@tiptap/extension-list-item": "^2.6.4",
"@tiptap/extension-ordered-list": "^2.6.4",
"@tiptap/extension-paragraph": "^2.6.4",
"@tiptap/extension-strike": "^2.6.4",
"@tiptap/extension-text": "^2.6.4",
"@tiptap/pm": "^2.6.4"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tiptap/suggestion": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.6.4.tgz",

View File

@@ -37,7 +37,15 @@
"@noble/hashes": "^1.4.0",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sveltejs/adapter-static": "^3.0.4",
"@tiptap/starter-kit": "^2.6.4",
"@tiptap/extension-code": "^2.6.6",
"@tiptap/extension-code-block": "^2.6.6",
"@tiptap/extension-document": "^2.6.6",
"@tiptap/extension-dropcursor": "^2.6.6",
"@tiptap/extension-gapcursor": "^2.6.6",
"@tiptap/extension-hard-break": "^2.6.6",
"@tiptap/extension-history": "^2.6.6",
"@tiptap/extension-paragraph": "^2.6.6",
"@tiptap/extension-text": "^2.6.6",
"@tiptap/suggestion": "^2.6.4",
"@types/throttle-debounce": "^5.0.2",
"@welshman/lib": "^0.0.15",

View File

@@ -1,6 +1,6 @@
import {derived} from "svelte/store"
import {memoize, assoc} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import type {TrustedEvent, HashedEvent} from "@welshman/util"
import {Repository, createEvent, Relay} from "@welshman/util"
import {withGetter} from "@welshman/store"
import {NetworkContext, Tracker} from "@welshman/net"
@@ -8,13 +8,17 @@ import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from "@welshman/sign
import {synced} from "@lib/util"
import type {Session} from "@app/types"
export const DEFAULT_RELAYS = ["wss://groups.fiatjaf.com/"]
export const DEFAULT_RELAYS = [
"wss://groups.fiatjaf.com/",
"wss://relay29.galaxoidlabs.com/",
"wss://devrelay.highlighter.com/",
]
export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://nos.lol/"]
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const repository = new Repository()
export const repository = new Repository<HashedEvent>()
export const relay = new Relay(repository)

View File

@@ -1,6 +1,7 @@
import {uniqBy, uniq, now} from "@welshman/lib"
import {uniqBy, uniq, now, choice} from "@welshman/lib"
import {
GROUPS,
GROUP_JOIN,
asDecryptedEvent,
getGroupTags,
getRelayTagValues,
@@ -9,6 +10,7 @@ import {
makeList,
createList,
createEvent,
displayProfile,
} from "@welshman/util"
import {pk, signer, repository, INDEXER_RELAYS} from "@app/base"
import {
@@ -23,8 +25,34 @@ import {
makeThunk,
publishThunk,
ensurePlaintext,
getProfilesByPubkey,
} from "@app/state"
// Utils
export const getPubkeyHints = (pubkey: string) => {
const selections = getRelaySelectionsByPubkey().get(pubkey)
const relays = selections ? getWriteRelayUrls(selections) : []
const hints = relays.length ? relays : INDEXER_RELAYS
return hints
}
export const getPubkeyPetname = (pubkey: string) => {
const profile = getProfilesByPubkey().get(pubkey)
const display = displayProfile(profile)
return display
}
export const makeMention = (pubkey: string, hints?: string[]) =>
["p", pubkey, choice(hints || getPubkeyHints(pubkey)), getPubkeyPetname(pubkey)]
export const makeIMeta = (url: string, data: Record<string, string>) =>
["imeta", `url ${url}`, ...Object.entries(data).map(([k, v]) => [k, v].join(' '))]
// Loaders
export const loadUserData = async (pubkey: string, hints: string[] = []) => {
const relaySelections = await loadRelaySelections(pubkey, INDEXER_RELAYS)
const relays = uniq([
@@ -46,6 +74,8 @@ export const loadUserData = async (pubkey: string, hints: string[] = []) => {
await Promise.all(promises)
}
// Updates
export type ModifyTags = (tags: string[][]) => string[][]
export const updateList = async (kind: number, modifyTags: ModifyTags) => {
@@ -67,3 +97,10 @@ export const addGroupMemberships = (newTags: string[][]) =>
export const removeGroupMemberships = (noms: string[]) =>
updateList(GROUPS, (tags: string[][]) => tags.filter(t => !noms.includes(t[1])))
export const sendJoinRequest = async (nom: string, url: string) => {
const event = createEvent(GROUP_JOIN, {tags: [["h", nom]]})
const result = await publishThunk(makeThunk({event, relays: [url]}))
return result[url]
}

View File

@@ -1,9 +1,17 @@
<script lang="ts">
import {onMount} from 'svelte'
import type {Readable} from 'svelte/store'
import {nprofileEncode} from 'nostr-tools/nip19'
import {createEditor, type Editor, EditorContent, SvelteNodeViewRenderer} from 'svelte-tiptap'
import {Extension} from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Code from '@tiptap/extension-code'
import CodeBlock from '@tiptap/extension-code-block'
import Document from '@tiptap/extension-document'
import Dropcursor from '@tiptap/extension-dropcursor'
import Gapcursor from '@tiptap/extension-gapcursor'
import History from '@tiptap/extension-history'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import HardBreakExtension from '@tiptap/extension-hard-break'
import {Bolt11Extension, NProfileExtension, NEventExtension, NAddrExtension, ImageExtension, VideoExtension, FileUploadExtension} from 'nostr-editor'
import type {StampedEvent} from '@welshman/util'
@@ -21,8 +29,11 @@
import GroupComposeSuggestions from '@app/components/GroupComposeSuggestions.svelte'
import GroupComposeTopicSuggestion from '@app/components/GroupComposeTopicSuggestion.svelte'
import GroupComposeProfileSuggestion from '@app/components/GroupComposeProfileSuggestion.svelte'
import {signer} from '@app/base'
import {searchProfiles, searchTopics, displayProfileByPubkey} from '@app/state'
import {signer, INDEXER_RELAYS} from '@app/base'
import {searchProfiles, publishThunk, makeThunk, searchTopics, userRelayUrlsByNom, getWriteRelayUrls, displayProfileByPubkey, getRelaySelectionsByPubkey} from '@app/state'
import {getPubkeyHints, makeMention, makeIMeta} from '@app/commands'
export let nom
let editor: Readable<Editor>
let uploading = false
@@ -34,31 +45,36 @@
const uploadFiles = () => $editor.chain().uploadFiles().run()
const sendMessage = () => {
console.log($editor.getJSON())
$editor.chain().clearContent().run()
createEvent(CHAT_MESSAGE, {
content: '',
tags: [],
const sendMessage = async () => {
const json = $editor.getJSON()
const relays = $userRelayUrlsByNom.get(nom)
const event = createEvent(CHAT_MESSAGE, {
content: $editor.getText(),
tags: [
["h", nom],
...findNodes(TopicExtension.name, json).map(t => ["t", t.attrs!.name.toLowerCase()]),
...findNodes(NProfileExtension.name, json).map(m => makeMention(m.attrs!.pubkey, m.attrs!.relays)),
...findNodes(ImageExtension.name, json).map(({attrs: {src, sha256: x}}: any) => makeIMeta(src, {x, ox: x})),
],
})
publishThunk(makeThunk({event, relays}))
$editor.chain().clearContent().run()
}
onMount(() => {
editor = createEditor({
autofocus: true,
extensions: [
StarterKit.configure({
blockquote: false,
bold: false,
bulletList: false,
heading: false,
horizontalRule: false,
italic: false,
listItem: false,
orderedList: false,
strike: false,
hardBreak: false,
}),
Code,
CodeBlock,
Document,
Dropcursor,
Gapcursor,
History,
Paragraph,
Text,
HardBreakExtension.extend({
addKeyboardShortcuts() {
return {
@@ -79,7 +95,7 @@
LinkExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(GroupComposeLink),
}),
Bolt11Extension.extend({addNodeView: () => SvelteNodeViewRenderer(GroupComposeBolt11)}),
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeBolt11)})),
NProfileExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(GroupComposeMention),
addProseMirrorPlugins() {
@@ -89,7 +105,12 @@
name: 'nprofile',
editor: this.editor,
search: searchProfiles,
select: (pubkey: string, props: any) => props.command({pubkey}),
select: (pubkey: string, props: any) => {
const relays = getPubkeyHints(pubkey)
const nprofile = nprofileEncode({pubkey, relays})
return props.command({pubkey, nprofile, relays})
},
suggestionComponent: GroupComposeProfileSuggestion,
suggestionsComponent: GroupComposeSuggestions,
}),
@@ -135,11 +156,7 @@
}),
],
content: '',
onUpdate: () => {
// console.log('update', $editor.getJSON(), $editor.getText())
},
})
console.log($editor)
})
</script>

View File

@@ -1,10 +1,20 @@
<script lang="ts">
import cx from 'classnames'
import type {NodeViewProps} from '@tiptap/core'
import {NodeViewWrapper} from 'svelte-tiptap'
import Icon from '@lib/components/Icon.svelte'
import Button from '@lib/components/Button.svelte'
import {clip} from '@app/toast'
export let node: NodeViewProps['node']
export let selected: NodeViewProps['selected']
const copy = () => clip(node.attrs.lnbc)
</script>
<NodeViewWrapper class="inline link-content">
<NodeViewWrapper class="inline">
<Button on:click={copy} class={cx("link-content", {'link-content-selected': selected})}>
<Icon icon="bolt" size={3} class="inline-block translate-y-px" />
{node.attrs.lnbc.slice(0, 16)}...
</Button>
</NodeViewWrapper>

View File

@@ -7,10 +7,13 @@
import {deriveProfile} from '@app/state'
export let node: NodeViewProps['node']
export let selected: NodeViewProps['selected']
$: profile = deriveProfile(node.attrs.pubkey, node.attrs.relays)
</script>
<NodeViewWrapper class="inline">
<span class="text-primary">@</span><Link external href="https://njump.me/{node.attrs.nprofile}">{displayProfile($profile)}</Link>
<Link external href="https://njump.me/{node.attrs.nprofile}" class={cx("link-content", {'link-content-selected': selected})}>
@{displayProfile($profile)}
</Link>
</NodeViewWrapper>

View File

@@ -1,10 +1,15 @@
<script lang="ts">
import cx from 'classnames'
import type {NodeViewProps} from '@tiptap/core'
import {NodeViewWrapper} from 'svelte-tiptap'
import Link from '@lib/components/Link.svelte'
export let node: NodeViewProps['node']
export let selected: NodeViewProps['selected']
</script>
<NodeViewWrapper class="inline text-primary">
#<span class="underline">{node.attrs.name}</span>
<NodeViewWrapper class="inline">
<Link external href="https://coracle.social/topics/{node.attrs.name.toLowerCase()}" class={cx("link-content", {'link-content-selected': selected})}>
#{node.attrs.name}
</Link>
</NodeViewWrapper>

View File

@@ -1,13 +1,15 @@
<script lang="ts">
import twColors from "tailwindcss/colors"
import {readable} from "svelte/store"
import {readable, derived} from "svelte/store"
import {hash} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {GROUP_REPLY, getAncestorTags, displayPubkey} from "@welshman/util"
import {fly} from "@lib/transition"
import {PublishStatus} from "@welshman/net"
import {GROUP_REPLY, displayRelayUrl, getAncestorTags, displayPubkey} from "@welshman/util"
import {fly, fade} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import {deriveProfile, deriveProfileDisplay, deriveEvent} from "@app/state"
import type {PublishStatusData} from "@app/state"
import {deriveProfile, deriveProfileDisplay, deriveEvent, publishStatusData} from "@app/state"
export let event: TrustedEvent
export let showPubkey: boolean
@@ -41,10 +43,17 @@
const parentHints = [replies[0]?.[2]].filter(Boolean)
const parentEvent = parentId ? deriveEvent(parentId, parentHints) : readable(null)
const [colorName, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const ps = derived(publishStatusData, $m => Object.values($m[event.id] || {}))
const findStatus = ($ps: PublishStatusData[], statuses: PublishStatus[]) =>
$ps.find(({status}) => statuses.includes(status))
$: parentPubkey = $parentEvent?.pubkey || replies[0]?.[4]
$: parentProfile = deriveProfile(parentPubkey)
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey)
$: isPublished = findStatus($ps, [PublishStatus.Success])
$: isPending = findStatus($ps, [PublishStatus.Pending])
$: failure = !isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout])
</script>
<div in:fly class="group relative flex flex-col gap-1 p-2 transition-colors hover:bg-base-300">
@@ -65,13 +74,28 @@
{#if showPubkey}
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={10} />
{:else}
<div class="w-10" />
<div class="min-w-10 max-w-10 w-10" />
{/if}
<div class="-mt-1">
{#if showPubkey}
<strong class="text-sm" style="color: {colorValue}" data-color={colorName}>{$profileDisplay}</strong>
{/if}
<p class="text-sm">{event.content}</p>
<p class="text-sm">
{event.content}
{#if isPending}
<span class="ml-1 flex-inline gap-1">
<span class="loading loading-spinner h-3 w-3 mx-1 translate-y-px" />
<span class="opacity-50">Sending...</span>
</span>
{:else if failure}
<span
class="ml-1 flex-inline gap-1 tooltip cursor-pointer"
data-tip="{failure.message} ({displayRelayUrl(failure.url)})">
<Icon icon="danger" class="translate-y-px" size={3} />
<span class="opacity-50">Failed to send!</span>
</span>
{/if}
</p>
</div>
</div>
<div

View File

@@ -1,7 +1,9 @@
<script lang="ts">
import {displayRelayUrl} from '@welshman/util'
import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte"
import {DEFAULT_RELAYS} from "@app/base"
import {clip} from "@app/toast"
</script>
@@ -21,14 +23,16 @@
>. If you do decide to join someone else's, make sure to follow their directions for registering
as a user.
</p>
{#each DEFAULT_RELAYS as url}
<div class="alert !flex items-center justify-between">
<div class="flex items-center gap-2">
<Icon icon="remote-controller-minimalistic" />
groups.fiatjaf.com
{displayRelayUrl(url)}
</div>
<Button on:click={() => clip("groups.fiatjaf.com")}>
<Button on:click={() => clip(url)}>
<Icon icon="copy" />
</Button>
</div>
{/each}
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
</div>

View File

@@ -52,9 +52,12 @@
{#each $userGroupsByNom.entries() as [nom, qualifiedGroups] (nom)}
{@const qualifiedGroup = qualifiedGroups[0]}
<PrimaryNavItem title={displayGroup(qualifiedGroup?.group)} href="/spaces/{nom}">
<div class="w-10 rounded-full border border-solid border-base-300">
<img alt={displayGroup(qualifiedGroup?.group)} src={qualifiedGroup?.group.picture} />
</div>
<Avatar
icon="ghost"
class="!h-10 !w-10 border border-solid border-base-300"
alt={displayGroup(qualifiedGroup?.group)}
src={qualifiedGroup?.group.picture}
size={7} />
</PrimaryNavItem>
{/each}
<PrimaryNavItem title="Add Space" on:click={addSpace}>

View File

@@ -1,14 +1,17 @@
<script lang="ts">
import {append, remove} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util"
import {PublishStatus} from "@welshman/net"
import {goto} from "$app/navigation"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import InfoNip29 from "@app/components/InfoNip29.svelte"
import {pushModal} from "@app/modal"
import {pushModal, clearModal} from "@app/modal"
import {pushToast} from "@app/toast"
import type {PublishStatusData} from "@app/state"
import {deriveGroup, displayGroup, relayUrlsByNom} from "@app/state"
import {addGroupMemberships} from "@app/commands"
import {sendJoinRequest, addGroupMemberships} from "@app/commands"
export let nom
@@ -22,20 +25,35 @@
: append(e.target.value, urls)
}
const tryJoin = async () => {
for (const url of urls) {
const {status, message} = await sendJoinRequest(nom, url)
if (status !== PublishStatus.Success) {
return pushToast({
theme: 'error',
message: `Failed to join relay: ${message || status}`,
})
}
}
await addGroupMemberships(urls.map(url => ["group", nom, url]))
clearModal()
}
const join = async () => {
loading = true
try {
await addGroupMemberships(urls.map(url => ["group", nom, url]))
await tryJoin()
} finally {
loading = false
}
goto(`/spaces/${nom}`)
}
let urls: string[] = $relayUrlsByNom.get(nom) || []
let loading = false
let urls: string[] = $relayUrlsByNom.get(nom) || []
$: hasUrls = urls.length > 0
$: urlOptions = $relayUrlsByNom.get(nom)?.toSorted() || []

View File

@@ -4,12 +4,14 @@
</script>
{#if $toast}
{@const theme = $toast.theme || "info"}
{#key $toast.id}
<div transition:fly class="toast z-toast">
<div
role="alert"
class="alert flex justify-center"
class:alert-error={$toast.theme === "error"}>
class:alert-info={theme === "info"}
class:alert-error={theme === "error"}>
{$toast.message}
</div>
</div>

View File

@@ -5,6 +5,7 @@ import {get, writable, readable, derived} from "svelte/store"
import type {Maybe} from "@welshman/lib"
import {
max,
append,
between,
uniqBy,
groupBy,
@@ -38,6 +39,8 @@ import {
displayPubkey,
GROUP_JOIN,
GROUP_ADD_USER,
isStampedEvent,
isEventTemplate,
} from "@welshman/util"
import type {SignedEvent, HashedEvent, EventTemplate, TrustedEvent, PublishedProfile, PublishedList} from "@welshman/util"
import type {SubscribeRequest, PublishRequest} from "@welshman/net"
@@ -129,9 +132,13 @@ export type Thunk = {
relays: string[]
}
export const thunkWorker = new Worker<Thunk>()
export type ThunkWithResolve = Thunk & {
resolve: (data: PublishStatusDataByUrl) => void
}
thunkWorker.addGlobalHandler(async ({event, relays}: Thunk) => {
export const thunkWorker = new Worker<ThunkWithResolve>()
thunkWorker.addGlobalHandler(async ({event, relays, resolve}: ThunkWithResolve) => {
const session = getSession(event.pubkey)
if (!session) {
@@ -139,26 +146,20 @@ thunkWorker.addGlobalHandler(async ({event, relays}: Thunk) => {
}
const signedEvent = await getSigner(session)!.sign(event)
const savedEvent = repository.getEvent(signedEvent.id)
const pub = basePublish({event: signedEvent, relays})
// Copy the signature over since we had deferred it
if (savedEvent) {
savedEvent.sig = signedEvent.sig
}
;(repository.getEvent(signedEvent.id) as SignedEvent).sig = signedEvent.sig
const failures = new Set<string>()
// Track publish success
const {id} = event
const statusByUrl: PublishStatusDataByUrl = {}
// Watch for failures
pub.emitter.on('*', (status: PublishStatus, url: string) => {
console.log('pub status', status, url)
pub.emitter.on('*', (status: PublishStatus, url: string, message: string) => {
publishStatusData.update(assoc(id, Object.assign(statusByUrl, {[url]: {id, url, status, message}})))
if ([PublishStatus.Failure, PublishStatus.Timeout].includes(status)) {
failures.add(url)
}
if (failures.size === relays.length) {
console.warn("Failed to publish", pub)
if (Object.values(statusByUrl).filter(s => s.status !== PublishStatus.Pending).length === relays.length) {
resolve(statusByUrl)
}
})
})
@@ -178,17 +179,20 @@ export const makeThunk = ({event, relays}: ThunkParams) => {
return {event: hash(own(stamp(event), $pk)), relays}
}
export const publishThunk = (thunk: Thunk) => {
thunkWorker.push(thunk)
export const publishThunk = (thunk: Thunk) =>
new Promise<PublishStatusDataByUrl>(resolve => {
thunkWorker.push({...thunk, resolve})
repository.publish(thunk.event)
}
})
// Subscribe
export const subscribe = (request: SubscribeRequest) => {
const sub = baseSubscribe({delay: 50, authTimeout: 3000, ...request})
sub.emitter.on("event", (url: string, e: SignedEvent) => repository.publish(e))
sub.emitter.on("event", (url: string, e: SignedEvent) => {
repository.publish(e)
})
return sub
}
@@ -198,14 +202,27 @@ export const load = (request: SubscribeRequest) =>
const sub = subscribe({closeOnEose: true, timeout: 3000, ...request})
const events: TrustedEvent[] = []
sub.emitter.on("event", (url: string, e: SignedEvent) => {
repository.publish(e)
events.push(e)
})
sub.emitter.on("event", (url: string, e: SignedEvent) => events.push(e))
sub.emitter.on("complete", () => resolve(events))
})
// Publish status
export type PublishStatusData = {
id: string
url: string
message: string
status: PublishStatus
}
export type PublishStatusDataByUrl = Record<string, PublishStatusData>
export type PublishStatusDataByUrlById = Record<string, PublishStatusDataByUrl>
export const publishStatusData = writable<PublishStatusDataByUrlById>({})
// Freshness
export const freshness = withGetter(writable<Record<string, number>>({}))
@@ -290,7 +307,7 @@ export const searchTopics = derived(topics, $topics =>
export const relays = writable<Relay[]>([])
export const relaysByPubkey = derived(relays, $relays =>
groupBy(($relay: Relay) => $relay.pubkey, $relays),
groupBy(($relay: Relay) => $relay.pubkey, $relays.filter(r => r.pubkey)),
)
export const {
@@ -508,7 +525,7 @@ export const getGroupName = (e?: TrustedEvent) => e?.tags.find(nthEq(0, "name"))
export const getGroupPicture = (e?: TrustedEvent) => e?.tags.find(nthEq(0, "picture"))?.[1]
export const displayGroup = (group?: Group) => group?.name || "[no name]"
export const displayGroup = (group?: Group) => group?.name || group?.nom || "[no name]"
export type Group = {
nom: string
@@ -524,8 +541,8 @@ export type PublishedGroup = Omit<Group, "event"> & {
export const readGroup = (event: TrustedEvent) => {
const nom = getIdentifier(event)!
const name = event?.tags.find(nthEq(0, "name"))?.[1] || "[no name]"
const about = event?.tags.find(nthEq(0, "about"))?.[1] || ""
const name = event?.tags.find(nthEq(0, "name"))?.[1]
const about = event?.tags.find(nthEq(0, "about"))?.[1]
const picture = event?.tags.find(nthEq(0, "picture"))?.[1]
return {nom, name, about, picture, event}
@@ -759,3 +776,18 @@ export const userGroupsByNom = withGetter(
return $userGroupsByNom
}),
)
export const userRelayUrlsByNom = derived(
userGroupsByNom,
$userGroupsByNom => {
const $userRelayUrlsByNom = new Map()
for (const [nom, groups] of $userGroupsByNom.entries()) {
for (const group of groups) {
pushToMapKey($userRelayUrlsByNom, nom, group.relay.url)
}
}
return $userRelayUrlsByNom
}
)

View File

@@ -21,8 +21,6 @@ export const subs: Unsubscriber[] = []
export const DB_NAME = "flotilla"
export const DB_VERSION = 1
export const getAll = async (name: string) => {
const tx = db.transaction(name, "readwrite")
const store = tx.objectStore(name)
@@ -81,12 +79,12 @@ export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapt
)
}
export const initStorage = async (adapters: Record<string, IndexedDbAdapter>) => {
export const initStorage = async (version: number, adapters: Record<string, IndexedDbAdapter>) => {
if (!window.indexedDB) return
window.addEventListener("beforeunload", () => closeStorage())
db = await openDB(DB_NAME, DB_VERSION, {
db = await openDB(DB_NAME, version, {
upgrade(db: IDBPDatabase) {
const names = Object.keys(adapters)

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.66953 9.91436L8.73167 5.77133C10.711 3.09327 11.7007 1.75425 12.6241 2.03721C13.5474 2.32018 13.5474 3.96249 13.5474 7.24712V7.55682C13.5474 8.74151 13.5474 9.33386 13.926 9.70541L13.946 9.72466C14.3327 10.0884 14.9492 10.0884 16.1822 10.0884C18.4011 10.0884 19.5106 10.0884 19.8855 10.7613C19.8917 10.7724 19.8977 10.7837 19.9036 10.795C20.2576 11.4784 19.6152 12.3475 18.3304 14.0857L15.2683 18.2287C13.2889 20.9067 12.2992 22.2458 11.3758 21.9628C10.4525 21.6798 10.4525 20.0375 10.4525 16.7528L10.4526 16.4433C10.4526 15.2585 10.4526 14.6662 10.074 14.2946L10.054 14.2754C9.6673 13.9117 9.05079 13.9117 7.81775 13.9117C5.59888 13.9117 4.48945 13.9117 4.1145 13.2387C4.10829 13.2276 4.10225 13.2164 4.09639 13.205C3.74244 12.5217 4.3848 11.6526 5.66953 9.91436Z" stroke="#1C274C" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 7V13" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="12" cy="16" r="1" fill="#1C274C"/>
<path d="M7.84308 3.80211C9.8718 2.6007 10.8862 2 12 2C13.1138 2 14.1282 2.6007 16.1569 3.80211L16.8431 4.20846C18.8718 5.40987 19.8862 6.01057 20.4431 7C21 7.98943 21 9.19084 21 11.5937V12.4063C21 14.8092 21 16.0106 20.4431 17C19.8862 17.9894 18.8718 18.5901 16.8431 19.7915L16.1569 20.1979C14.1282 21.3993 13.1138 22 12 22C10.8862 22 9.8718 21.3993 7.84308 20.1979L7.15692 19.7915C5.1282 18.5901 4.11384 17.9894 3.55692 17C3 16.0106 3 14.8092 3 12.4063V11.5937C3 9.19084 3 7.98943 3.55692 7C4.11384 6.01057 5.1282 5.40987 7.15692 4.20846L7.84308 3.80211Z" stroke="#1C274C" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 826 B

View File

@@ -3,15 +3,17 @@
import Icon from "@lib/components/Icon.svelte"
export let src
export let alt = ""
export let size = 7
export let icon = "user-rounded"
</script>
<div
class={cx($$props.class, "!flex items-center justify-center overflow-hidden rounded-full")}
style={`width: ${size * 4}px; height: ${size * 4}px;`}>
{#if src}
<img alt="" {src} />
<img {alt} {src} />
{:else}
<Icon icon="user-rounded" size={Math.round(size * 0.7)} />
<Icon {icon} size={Math.round(size * 0.7)} />
{/if}
</div>

View File

@@ -6,6 +6,7 @@
</style>
<script lang="ts">
import cx from 'classnames'
import {switcher} from "@welshman/lib"
import AddSquare from "@assets/icons/Add Square.svg?dataurl"
import AddCircle from "@assets/icons/Add Circle.svg?dataurl"
@@ -14,6 +15,7 @@
import AltArrowLeft from "@assets/icons/Alt Arrow Left.svg?dataurl"
import ArrowRight from "@assets/icons/Arrow Right.svg?dataurl"
import Bag from "@assets/icons/Bag.svg?dataurl"
import Bolt from "@assets/icons/Bolt.svg?dataurl"
import CalendarMinimalistic from "@assets/icons/Calendar Minimalistic.svg?dataurl"
import ChatRound from "@assets/icons/Chat Round.svg?dataurl"
import CheckCircle from "@assets/icons/Check Circle.svg?dataurl"
@@ -22,6 +24,7 @@
import Copy from "@assets/icons/Copy.svg?dataurl"
import Compass from "@assets/icons/Compass.svg?dataurl"
import CompassBig from "@assets/icons/Compass Big.svg?dataurl"
import Danger from "@assets/icons/Danger.svg?dataurl"
import Exit from "@assets/icons/Exit.svg?dataurl"
import FireMinimalistic from "@assets/icons/Fire Minimalistic.svg?dataurl"
import GallerySend from "@assets/icons/Gallery Send.svg?dataurl"
@@ -65,6 +68,7 @@
"alt-arrow-left": AltArrowLeft,
"arrow-right": ArrowRight,
bag: Bag,
bolt: Bolt,
"calendar-minimalistic": CalendarMinimalistic,
"chat-round": ChatRound,
"check-circle": CheckCircle,
@@ -73,6 +77,7 @@
copy: Copy,
compass: Compass,
"compass-big": CompassBig,
danger: Danger,
exit: Exit,
"fire-minimalistic": FireMinimalistic,
"gallery-send": GallerySend,
@@ -110,5 +115,5 @@
</script>
<div
class={$$props.class}
class={cx("inline-block", $$props.class)}
style="mask-image: url({data}); width: {px}px; height: {px}px; min-width: {px}px; min-height: {px}px; background-color: currentcolor;" />

View File

@@ -5,7 +5,7 @@ export const createPasteRuleMatch = <T extends Record<string, unknown>>(
data: T,
): PasteRuleMatch => ({ index: match.index!, replaceWith: match[2], text: match[0], match, data })
export const findNodes = (json: JSONContent, type: string) => {
export const findNodes = (type: string, json: JSONContent) => {
const results: JSONContent[] = []
for (const node of json.content || []) {
@@ -13,7 +13,7 @@ export const findNodes = (json: JSONContent, type: string) => {
results.push(node)
}
for (const result of findNodes(node, type)) {
for (const result of findNodes(type, node)) {
results.push(result)
}
}

View File

@@ -1,5 +1,7 @@
import type {FlyParams} from "svelte/transition"
import {fly as baseFly} from "svelte/transition"
export {fade} from 'svelte/transition'
export const fly = (node: Element, params?: FlyParams | undefined) =>
baseFly(node, {y: 20, ...params})

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import "@src/app.css"
import {onMount} from "svelte"
import {get, derived} from 'svelte/store'
import {page} from "$app/stores"
import {goto} from "$app/navigation"
import {browser} from "$app/environment"
import {createEventStore} from "@welshman/store"
import {createEventStore, adapter} from "@welshman/store"
import ModalBox from "@lib/components/ModalBox.svelte"
import Toast from "@app/components/Toast.svelte"
import Landing from "@app/components/Landing.svelte"
@@ -12,9 +13,12 @@
import {modals, clearModal} from "@app/modal"
import {theme} from "@app/theme"
import {pk, session, repository, DEFAULT_RELAYS} from "@app/base"
import {relays, handles, loadRelay} from "@app/state"
import type {PublishStatusData, PublishStatusDataByUrlById} from "@app/state"
import {relays, freshness, plaintext, handles, loadRelay, publishStatusData} from "@app/state"
import {initStorage} from "@app/storage"
import {loadUserData} from "@app/commands"
import * as base from "@app/base"
import * as state from "@app/state"
let ready: Promise<void>
let dialog: HTMLDialogElement
@@ -43,7 +47,9 @@
}
onMount(() => {
ready = initStorage({
Object.assign(window, {get, base, state})
ready = initStorage(3, {
events: {
keyPath: "id",
store: createEventStore(repository),
@@ -56,6 +62,65 @@
keyPath: "nip05",
store: handles,
},
publishStatus: {
keyPath: "id",
store: adapter({
store: publishStatusData,
forward: ($psd: PublishStatusDataByUrlById) => {
const data = []
for (const [id, itemsByUrl] of Object.entries($psd)) {
for (const [url, item] of Object.entries(itemsByUrl)) {
data.push(item)
}
}
return data
},
backward: (data: PublishStatusData[]) => {
const result: PublishStatusDataByUrlById = {}
for (const item of data) {
result[item.id] = result[item.id] || {}
result[item.id][item.url] = item
}
return result
},
}),
},
freshness: {
keyPath: "key",
store: adapter({
store: freshness,
forward: ($freshness: Record<string, number>) => Object.entries($freshness).map(([key, ts]) => ({key, ts})),
backward: (data: any[]) => {
const result: Record<string, number> = {}
for (const {key, ts} of data) {
result[key] = ts
}
return result
},
}),
},
plaintext: {
keyPath: "id",
store: adapter({
store: plaintext,
forward: ($plaintext: Record<string, string>) => Object.entries($plaintext).map(([id, plaintext]) => ({id, plaintext})),
backward: (data: any[]) => {
const result: Record<string, string> = {}
for (const {id, plaintext} of data) {
result[id] = plaintext
}
return result
},
}),
},
})
dialog.addEventListener("close", () => {

View File

@@ -14,10 +14,10 @@
userMembership,
} from "@app/state"
const getRelayUrls = (nom: string): string[] => $relayUrlsByNom.get(nom) || []
let term = ""
$: groups = $searchGroups.searchOptions(term).filter(g => $relayUrlsByNom.get(g.nom)?.length > 0)
onMount(() => {
load({
relays: [...DEFAULT_RELAYS, ...$relays.map(r => r.url)],
@@ -35,7 +35,7 @@
</label>
<Masonry
animate={false}
items={$searchGroups.searchOptions(term)}
items={groups}
minColWidth={250}
maxColWidth={800}
gap={16}
@@ -57,7 +57,7 @@
{#if $userMembership?.noms.has(group.nom)}
<div class="center absolute flex w-full">
<div
class="tooltip relative left-8 top-[38px] rounded-full bg-primary"
class="tooltip relative left-8 w-5 h-5 top-[38px] rounded-full bg-primary"
data-tip="You are already a member of this space.">
<Icon icon="check-circle" class="scale-110" />
</div>
@@ -66,7 +66,7 @@
<div class="card-body">
<h2 class="card-title justify-center">{displayGroup(group)}</h2>
<div class="text-center text-sm">
{#each getRelayUrls(group.nom) as url}
{#each $relayUrlsByNom.get(group.nom) || [] as url}
<div class="badge badge-neutral">{displayRelayUrl(url)}</div>
{/each}
</div>

View File

@@ -90,5 +90,5 @@
</Spinner>
</p>
</div>
<GroupCompose />
<GroupCompose {nom} />
</div>