forked from coracle/flotilla
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57447e5bf4 | |||
| 8e411daaef | |||
| 183aebf841 | |||
| e3e500ccc2 | |||
| e7a2535ece | |||
| 761e369313 | |||
| 5248275d73 | |||
| cb033279dd | |||
| 41d50d8c28 | |||
| a52c2b4c3c | |||
| b5917cb184 | |||
| 57348472f8 | |||
| 4b6223dc00 | |||
| 5525e45a15 | |||
| 80a2ae60b0 | |||
| d7e95f5d2f | |||
| ca4e5ae5ee | |||
| b673658c0c | |||
| 5c5c130700 | |||
| 2d89ca6c0e | |||
| 806a7c2609 | |||
| 501ce8067d | |||
| 6429f82829 | |||
| fe626218ea | |||
| b62b1bc063 | |||
| d980f36246 | |||
| b469addd29 | |||
| 6923c2a8b7 | |||
| 1d3f32fb99 | |||
| 42a550788a | |||
| b1c68972c9 | |||
| 3978e32d5f | |||
| ba2b5d182e | |||
| bef04fa899 | |||
| 4f8609421c | |||
| 07660c9d44 | |||
| a324dad2ba | |||
| dbaa0f5d49 | |||
| 478721d349 | |||
| a669a23dbc | |||
| cfeb6478cc | |||
| 64539c49c1 | |||
| 0399ae37ec | |||
| 173a411a36 | |||
| 62013a2ea2 | |||
| c82cf4a4c2 |
+1
-1
@@ -1,7 +1,7 @@
|
||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||
VITE_BURROW_URL=
|
||||
VITE_PLATFORM_URL=https://flotilla.social
|
||||
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||
VITE_PLATFORM_NAME=Flotilla
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
# 1.5.3
|
||||
|
||||
* Add space edit form
|
||||
* Improve room syncing
|
||||
* Return better blossom errors
|
||||
* Fix access restricted bugs
|
||||
* Add room detail dialog
|
||||
* Fix broken link to self hosting
|
||||
* Tweak shadows
|
||||
* Always join spaces when visiting them
|
||||
|
||||
# 1.5.2
|
||||
|
||||
* Fix negentropy room syncing
|
||||
|
||||
# 1.5.1
|
||||
|
||||
* Fix chat path link
|
||||
|
||||
# 1.5.0
|
||||
|
||||
* Restyle mobile dialogs
|
||||
* Add room membership lists
|
||||
* Add space membership lists
|
||||
* Add edit room form
|
||||
* Support closed/private/restricted/hidden rooms
|
||||
* Add hosting services page
|
||||
* Improve performance and UI
|
||||
* Fix push notifications
|
||||
* Improve error detection and handling
|
||||
* Support invite links on discover page
|
||||
* Add link to landlubber if user is admin
|
||||
* Clear reply/share/edit on escape
|
||||
|
||||
# 1.4.1
|
||||
|
||||
* Improve data synchronization
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ Here are a few important domain objects:
|
||||
|
||||
- Spaces are relays used as community groups. Their `url`s are core to a lot of data and components, and are frequently passed around from place to place.
|
||||
- Chats are direct message conversations. There is currently some ambiguity in routing, since relays that don't support NIP 29 also have a "chat" tab, which uses vanilla NIP-C7.
|
||||
- NIP 29 groups are variously called "rooms" and "channels". Conventionally, a "room" is a group id, while a "channel" as an object representing the group's metadata.
|
||||
- NIP 29 groups are called "rooms". Conventionally, "h" is a group id, while a "room" as an object representing the group's metadata.
|
||||
- "Alerts" are records of requests the user has made to be notified, following [this NIP](https://github.com/nostr-protocol/nips/pull/1796)
|
||||
|
||||
`app/core/requests` contains utilities related to loading data from the nostr network. This might include feed manager utilities, loaders, or listeners.
|
||||
|
||||
@@ -8,8 +8,8 @@ If you would like to be interoperable with Flotilla, please check out this guide
|
||||
|
||||
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
|
||||
|
||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust.
|
||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted. This is only used for build-time population of meta tags.
|
||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
|
||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted
|
||||
- `VITE_PLATFORM_NAME` - The name of the app
|
||||
- `VITE_PLATFORM_LOGO` - A logo url for the app
|
||||
- `VITE_PLATFORM_RELAYS` - A list of comma-separated relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "social.flotilla"
|
||||
minSdk rootProject.ext.minSdkVersion
|
||||
targetSdk rootProject.ext.targetSdkVersion
|
||||
versionCode 30
|
||||
versionName "1.4.1"
|
||||
versionCode 34
|
||||
versionName "1.5.3"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -6,10 +6,6 @@ if [ -f .env.template ]; then
|
||||
source .env.template
|
||||
fi
|
||||
|
||||
if [ -f .env ]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Avoid overwriting env vars provided directly
|
||||
# https://stackoverflow.com/a/69127685/1467342
|
||||
eval "$temp_env"
|
||||
|
||||
@@ -358,14 +358,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -384,14 +384,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -60,16 +60,16 @@
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.8",
|
||||
"@welshman/app": "^0.6.3",
|
||||
"@welshman/content": "^0.6.3",
|
||||
"@welshman/editor": "^0.6.3",
|
||||
"@welshman/feeds": "^0.6.3",
|
||||
"@welshman/lib": "^0.6.3",
|
||||
"@welshman/net": "^0.6.3",
|
||||
"@welshman/router": "^0.6.3",
|
||||
"@welshman/signer": "^0.6.3",
|
||||
"@welshman/store": "^0.6.3",
|
||||
"@welshman/util": "^0.6.3",
|
||||
"@welshman/app": "^0.6.8",
|
||||
"@welshman/content": "^0.6.8",
|
||||
"@welshman/editor": "^0.6.8",
|
||||
"@welshman/feeds": "^0.6.8",
|
||||
"@welshman/lib": "^0.6.8",
|
||||
"@welshman/net": "^0.6.8",
|
||||
"@welshman/router": "^0.6.8",
|
||||
"@welshman/signer": "^0.6.8",
|
||||
"@welshman/store": "^0.6.8",
|
||||
"@welshman/util": "^0.6.8",
|
||||
"compressorjs": "^1.2.1",
|
||||
"daisyui": "^4.12.24",
|
||||
"date-picker-svelte": "^2.16.0",
|
||||
|
||||
Generated
+132
-132
@@ -72,35 +72,35 @@ importers:
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
|
||||
'@welshman/app':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/content':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(typescript@5.9.3)
|
||||
'@welshman/editor':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)
|
||||
'@welshman/feeds':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/lib':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8
|
||||
'@welshman/net':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/store':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(typescript@5.9.3)
|
||||
compressorjs:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
@@ -1451,77 +1451,77 @@ packages:
|
||||
peerDependencies:
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-code-block@2.26.3':
|
||||
resolution: {integrity: sha512-3DbzKRfMqw9EGS7mGkpyopbRWTO+qpV52Mby4Ll2+OfhvGnHzSN4Q7xOsp+VeZr14GMEmua5Oq2e/gRypqXatQ==}
|
||||
'@tiptap/extension-code-block@2.27.1':
|
||||
resolution: {integrity: sha512-wCI5VIOfSAdkenCWFvh4m8FFCJ51EOK+CUmOC/PWUjyo2Dgn8QC8HMi015q8XF7886T0KvYVVoqxmxJSUDAYNg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-code@2.26.3':
|
||||
resolution: {integrity: sha512-bAkUNzV+tA1J1RYbtbAGTFqkRw9+yRpAd+d3S9jy/dAD+uOe1ZD1EIngyEf2GTonnoy4bpDYtytbCjUt9PozoA==}
|
||||
'@tiptap/extension-code@2.27.1':
|
||||
resolution: {integrity: sha512-i65wUGJevzBTIIUBHBc1ggVa27bgemvGl/tY1/89fEuS/0Xmre+OQjw8rCtSLevoHSiYYLgLRlvjtUSUhE4kgg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-document@2.26.3':
|
||||
resolution: {integrity: sha512-gcJg4Otchilr4eSUwhPNwbhPUkEYvXhkUZ/1MAhVGD40Ovq2P8ZWkJipA3tKOCJinL5MJK59ccZBstnKSTw+JA==}
|
||||
'@tiptap/extension-document@2.27.1':
|
||||
resolution: {integrity: sha512-NtJzJY7Q/6XWjpOm5OXKrnEaofrcc1XOTYlo/SaTwl8k2bZo918Vl0IDBWhPVDsUN7kx767uHwbtuQZ+9I82hA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-dropcursor@2.26.3':
|
||||
resolution: {integrity: sha512-54rgDTmRStVmXZR7KdCvSOCAbumh5luXgticUkRM8OM8PBe1c0T9X8jfV7+XEFGugRVl8mtCZZpgUt5vhuxHog==}
|
||||
'@tiptap/extension-dropcursor@2.27.1':
|
||||
resolution: {integrity: sha512-3MBQRGHHZ0by3OT0CWbLKS7J3PH9PpobrXjmIR7kr0nde7+bHqxXiVNuuIf501oKU9rnEUSedipSHkLYGkmfsA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-gapcursor@2.26.3':
|
||||
resolution: {integrity: sha512-ZDNSkpz7ik2PJOjrys27rwko5Ufe6GtLjaAxjvkWmyzcgAOTadDeth9NaRdBVMDGgSLBKbXihYZZXLkiAP9RLA==}
|
||||
'@tiptap/extension-gapcursor@2.27.1':
|
||||
resolution: {integrity: sha512-A9e1jr+jGhDWzNSXtIO6PYVYhf5j/udjbZwMja+wCE/3KvZU9V3IrnGKz1xNW+2Q2BDOe1QO7j5uVL9ElR6nTA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-hard-break@2.26.3':
|
||||
resolution: {integrity: sha512-KJWUi+2KOZejVRb2KI0NM3LgCpNimxcunbOCKsZKygV/UByzhUl7UaCAIa+ySMM+kbu/Ec3hkTzafGfaU9ZkLg==}
|
||||
'@tiptap/extension-hard-break@2.27.1':
|
||||
resolution: {integrity: sha512-W4hHa4Io6QCTwpyTlN6UAvqMIQ7t56kIUByZhyY9EWrg/+JpbfpxE1kXFLPB4ZGgwBknFOw+e4bJ1j3oAbTJFw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-history@2.26.3':
|
||||
resolution: {integrity: sha512-Qg4+WWf/hDgiBspxLbrhrIFUy7lzi2eBKPSoF/haEYFw/t/FeN60NXYYYtpLimUNpUzyJSOSIwsngFcVJO5X+g==}
|
||||
'@tiptap/extension-history@2.27.1':
|
||||
resolution: {integrity: sha512-K8PHC9gegSAt0wzSlsd4aUpoEyIJYOmVVeyniHr1P1mIblW1KYEDbRGbDlrLALTyUEfMcBhdIm8zrB9X2Nihvg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-image@2.26.3':
|
||||
resolution: {integrity: sha512-juAAY1QuzCgfl66Q8AHITLVKbwXpv+BmLNCi8Cl4j6a+IkySzcS8gENJee0hMMyRvc9K1U75o4vokvy580u4kA==}
|
||||
'@tiptap/extension-image@2.27.1':
|
||||
resolution: {integrity: sha512-wu3vMKDYWJwKS6Hrw5PPCKBO2RxyHNeFLiA/uDErEV7axzNpievK/U9DyaDXmtK3K/h1XzJAJz19X+2d/pY68w==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-link@2.26.3':
|
||||
resolution: {integrity: sha512-cNYqAeiaG/65ctVEUOHt1MQnTF1JcdZqBkN9pLf3grzcmkmdr3w1/JbKOphZc84vOB2rxuhGZx9NFV2lrC5Qwg==}
|
||||
'@tiptap/extension-link@2.27.1':
|
||||
resolution: {integrity: sha512-cCwWPZsnVh9MXnGOqSIRXPPuUixRDK8eMN2TvqwbxUBb1TU7b/HtNvfMU4tAOqAuMRJ0aJkFuf3eB0Gi8LVb1g==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-paragraph@2.26.3':
|
||||
resolution: {integrity: sha512-eBC5UsaTJRUMhePtK1dcCAfes0CpqqFiewpIM0lWk4XMtpG2aoczVVVkImybbFKfqsvEEo3vgHJ2YiE5YZFCSg==}
|
||||
'@tiptap/extension-paragraph@2.27.1':
|
||||
resolution: {integrity: sha512-R3QdrHcUdFAsdsn2UAIvhY0yWyHjqGyP/Rv8RRdN0OyFiTKtwTPqreKMHKJOflgX4sMJl/OpHTpNG1Kaf7Lo2A==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-placeholder@2.26.3':
|
||||
resolution: {integrity: sha512-HDF4FZj8CmQQvbSyXb/G+Ujqoue7TMQPMAe1h1OMJAXq856Y0AsVLXYKiBojUTfI11I7zVwYe08D8atIXHLZZw==}
|
||||
'@tiptap/extension-placeholder@2.27.1':
|
||||
resolution: {integrity: sha512-UbXaibHHFE+lOTlw/vs3jPzBoj1sAfbXuTAhXChjgYIcTTY5Cr6yxwcymLcimbQ79gf04Xkua2FCN3YsJxIFmw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
|
||||
'@tiptap/extension-text@2.26.3':
|
||||
resolution: {integrity: sha512-sGRbX96ss4jQeKw9d0iphuAWja8Dv4w4ryTDKfxD7Lizx3UaIxQB/y+Wna89tM3kfbi/qJcrD3AF7NJgfc/tEA==}
|
||||
'@tiptap/extension-text@2.27.1':
|
||||
resolution: {integrity: sha512-a4GCT+GZ9tUwl82F4CEum9/+WsuW0/De9Be/NqrMmi7eNfAwbUTbLCTFU0gEvv25WMHCoUzaeNk/qGmzeVPJ1Q==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/pm@2.26.3':
|
||||
resolution: {integrity: sha512-8gUmdxWlUevmgq2mNvGxvf2CpDW097tVKECMWKEn8sf846kXv3CoqaGRhI3db4kfR+09uWZeRM7rtrjRBmUThg==}
|
||||
|
||||
'@tiptap/suggestion@2.26.3':
|
||||
resolution: {integrity: sha512-kcyiyKEEDnqFImGQQEEuRa6N/N+/vU/OrI99wRfJnDnN8c3dP6UHJ4wr2qX6bUpx3Z0QTu6GGCpMpaqwtHTtJg==}
|
||||
'@tiptap/suggestion@2.27.1':
|
||||
resolution: {integrity: sha512-yTy75ZMYgVWM18cl7YxLqMJ7TorQTGysSd1aKmBA9qd8uzYlvLMmHKE9qBDxM9HXODBz1DA/BLLm9esv2enmFw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
@@ -1692,38 +1692,38 @@ packages:
|
||||
'@vite-pwa/assets-generator':
|
||||
optional: true
|
||||
|
||||
'@welshman/app@0.6.3':
|
||||
resolution: {integrity: sha512-iCZj0b3D5Q7rSEUiON+nJEKIdYG3j6MdK3oSyFiiQQxa+TPArjEbWF5kkogp1J1GMGo7gzQ6owByPgDIdlNiZg==}
|
||||
'@welshman/app@0.6.8':
|
||||
resolution: {integrity: sha512-bhl18VWA9tzHLY7D+b2xlkc/RbJr03XiA7+otcjzf8X48S4pih/F4TDw1yJbAWOMOx9G3NI6sWLffpZQeSUPiQ==}
|
||||
|
||||
'@welshman/content@0.6.3':
|
||||
resolution: {integrity: sha512-VvlW2kJ/lB6sy/Upa0nbKbO6rMwTO9Bi2iGXNiR9XFA8rlMUJc7Wl1Dcd3QZD3QHBTeTICOuFB4xopwQK3W0JQ==}
|
||||
'@welshman/content@0.6.8':
|
||||
resolution: {integrity: sha512-VLek8oOoMMTrEtpIfqFqM9BsbifWYwPC7UiuVuWYqaTSmiAbU3DM2J+tYFcrgnQF8xMnUi/JoVXJ+b2AtpjFrw==}
|
||||
|
||||
'@welshman/editor@0.6.3':
|
||||
resolution: {integrity: sha512-oLOhUUYO+4vhOehEpVhmsGMhmiBsJa/jgSsVRuCpZRKi+jwrJsaC9J7AyUzbQmcA8e07oNsqfakDYFkbFAfUVQ==}
|
||||
'@welshman/editor@0.6.8':
|
||||
resolution: {integrity: sha512-QzNX7/Nobkh+bpjFnuW2REVpX7Sa+lj70LDdGmEJpjtXlTKlLuNZzpFLee5F9fSObcKCl1G2xBN3tYbZD3vHUA==}
|
||||
|
||||
'@welshman/feeds@0.6.3':
|
||||
resolution: {integrity: sha512-Q5Dupo+C/CIm7BMZ++7BhQdjgyu3tU4aK2w172cx0gTFfSTqKxFrbWX4ewUWqq8ex1/1Xd+heVheE1q54FCVNQ==}
|
||||
'@welshman/feeds@0.6.8':
|
||||
resolution: {integrity: sha512-95VRR2QmGrBUyzYgdsMxhntVoOnaEMsMHRsci1/GX1oOFZPJFTiV7e/m/dD/aWVLUQV1hlRxxXosFFtEDkpjIw==}
|
||||
|
||||
'@welshman/lib@0.6.3':
|
||||
resolution: {integrity: sha512-O632yJQ/1IUmb9FuC+O0TsOUl+UubYriU95LpKA9ZFAaogXLrzHZH0HZ/4xmdPj82kqBeS98EOb3GkSAOYZaLA==}
|
||||
'@welshman/lib@0.6.8':
|
||||
resolution: {integrity: sha512-1Wybkk8+vBdqv9nRhnNwIW9YVbhu3di07A2fUYWAQvldto49X26U8u7EV2CkUsz4iNC/799EBYuelcc6W9oZYw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
'@welshman/net@0.6.3':
|
||||
resolution: {integrity: sha512-RlCYFFau6p2RZq3K4XMYhD7xvNFYjA6zpzHuM8iV4Xh8nL5b8tn6e6f6a4I2E5uZMfYdWstvcC6OhU/Z4NQZoA==}
|
||||
'@welshman/net@0.6.8':
|
||||
resolution: {integrity: sha512-Lc1nIckdxW2ILiknowcbaKo+192QWQOBn6FLhFCEUZNyRNEOJYkAgDu4jKn7GXu91xpfJUFnq5KDvvq7hUeHqg==}
|
||||
|
||||
'@welshman/router@0.6.3':
|
||||
resolution: {integrity: sha512-lZaw166aJKhftakMGofzmFmtAqfQBGSur58/ANgLCwVy4b5riqempcPoH6EOgwqANye5YUlsUisxbx0r/2wXPA==}
|
||||
'@welshman/router@0.6.8':
|
||||
resolution: {integrity: sha512-+OJoD2Jm+yFiLc5FYb4/za66639CeIMYk7j4UAzR7n1z/gFQYMDviXqFYbcWHln3fgy4G7UF1HWBoU0sQD8EEw==}
|
||||
|
||||
'@welshman/signer@0.6.3':
|
||||
resolution: {integrity: sha512-O0cd5nV77Lt4vWRvb50l6U1wkhQF6LW3x+N4tspqWqo9BeIHTDlUtZMdsnhwfTjeVlLocSVkUValxhoxxt20SA==}
|
||||
'@welshman/signer@0.6.8':
|
||||
resolution: {integrity: sha512-lt9Qq89TWyx/zSWgHkeVUX7MBCx86iBCkvzTdUIS7Ad6KfjjcYtsL9wAtfCc+TlvE87okOg97hAOvw18yIwfbw==}
|
||||
peerDependencies:
|
||||
nostr-signer-capacitor-plugin: ~0.0.4
|
||||
|
||||
'@welshman/store@0.6.3':
|
||||
resolution: {integrity: sha512-3TMon00CF1l/LcWq+B3pq/FNs2Ie4Y9EdjiAcg4dvDeZPI9h1HoQPB/s2RcaG4FYPhkynvM5zL/eH+5GIe9kJw==}
|
||||
'@welshman/store@0.6.8':
|
||||
resolution: {integrity: sha512-s5s5+tdPyXB1m2vLn2wfo7nx+uNKWBdwCyomk+soWKWEY3LWvg4DAKgQ1gF5hyOcja+UIHOJY9hS3BBEo0DtDA==}
|
||||
|
||||
'@welshman/util@0.6.3':
|
||||
resolution: {integrity: sha512-ki2j3ADmxqI0rbdnkq0wS7PFXTY0dZ24yOgceyTizwF/dtcRKmH7YyvM+2YwgbgaUO6BtqKjktc3jrh6XaxOQg==}
|
||||
'@welshman/util@0.6.8':
|
||||
resolution: {integrity: sha512-Q4x3Jm3yIk4zORYOscMuxyC7fJGyZFetE5U4PVYNrvgtSLCtULYKs1y6WkAra4FD7zfAa7lqzTlQq4uIZWzdkA==}
|
||||
|
||||
'@xml-tools/parser@1.0.11':
|
||||
resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==}
|
||||
@@ -3489,8 +3489,8 @@ packages:
|
||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
nostr-editor@1.0.1:
|
||||
resolution: {integrity: sha512-HXqXjxtIN0CcC7sLV5xYjEsQF0bFYLmNKxS75ya2yZGQ/z16U+uK6bb2Hd72QyqXlHXyWN0m24E5Gcws8/NhRQ==}
|
||||
nostr-editor@1.0.2:
|
||||
resolution: {integrity: sha512-z1XfVH0cDsDBvIfsNfIjjD1MI+ugChMbJToNIlKXi6aMkm8KgZOkHl9nkKdkAfZXU5yk+DPTEvv433NPZp2yKA==}
|
||||
engines: {node: '>=18.16.1'}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.6.6
|
||||
@@ -6346,58 +6346,58 @@ snapshots:
|
||||
dependencies:
|
||||
'@tiptap/pm': 2.26.3
|
||||
|
||||
'@tiptap/extension-code-block@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
'@tiptap/extension-code-block@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/pm': 2.26.3
|
||||
|
||||
'@tiptap/extension-code@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
'@tiptap/extension-code@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
|
||||
'@tiptap/extension-document@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
'@tiptap/extension-document@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
|
||||
'@tiptap/extension-dropcursor@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
'@tiptap/extension-dropcursor@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/pm': 2.26.3
|
||||
|
||||
'@tiptap/extension-gapcursor@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
'@tiptap/extension-gapcursor@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/pm': 2.26.3
|
||||
|
||||
'@tiptap/extension-hard-break@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
'@tiptap/extension-hard-break@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
|
||||
'@tiptap/extension-history@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
'@tiptap/extension-history@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/pm': 2.26.3
|
||||
|
||||
'@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
'@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
|
||||
'@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
'@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/pm': 2.26.3
|
||||
linkifyjs: 4.3.2
|
||||
|
||||
'@tiptap/extension-paragraph@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
'@tiptap/extension-paragraph@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
|
||||
'@tiptap/extension-placeholder@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
'@tiptap/extension-placeholder@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/pm': 2.26.3
|
||||
|
||||
'@tiptap/extension-text@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
'@tiptap/extension-text@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
|
||||
@@ -6422,7 +6422,7 @@ snapshots:
|
||||
prosemirror-transform: 1.10.4
|
||||
prosemirror-view: 1.41.3
|
||||
|
||||
'@tiptap/suggestion@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
'@tiptap/suggestion@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/pm': 2.26.3
|
||||
@@ -6651,16 +6651,16 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@vite-pwa/assets-generator': 0.2.6
|
||||
|
||||
'@welshman/app@0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/app@0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@types/throttle-debounce': 5.0.2
|
||||
'@welshman/feeds': 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer': 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/store': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/feeds': 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/lib': 0.6.8
|
||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer': 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/store': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
||||
fuse.js: 7.1.0
|
||||
svelte: 4.2.20
|
||||
throttle-debounce: 5.0.2
|
||||
@@ -6669,31 +6669,31 @@ snapshots:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/content@0.6.3(typescript@5.9.3)':
|
||||
'@welshman/content@0.6.8(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@braintree/sanitize-url': 7.1.1
|
||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@welshman/editor@0.6.3(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)':
|
||||
'@welshman/editor@0.6.8(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-code': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-code-block': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-document': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-dropcursor': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-gapcursor': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-hard-break': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-history': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-paragraph': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-placeholder': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-text': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-code': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-code-block': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-document': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-dropcursor': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-gapcursor': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-hard-break': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-history': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-paragraph': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-placeholder': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-text': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/pm': 2.26.3
|
||||
'@tiptap/suggestion': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
nostr-editor: 1.0.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))
|
||||
'@tiptap/suggestion': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@welshman/lib': 0.6.8
|
||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
||||
nostr-editor: 1.0.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))
|
||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||
tippy.js: 6.3.7
|
||||
transitivePeerDependencies:
|
||||
@@ -6707,71 +6707,71 @@ snapshots:
|
||||
- tiptap-markdown
|
||||
- typescript
|
||||
|
||||
'@welshman/feeds@0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/feeds@0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer': 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.8
|
||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer': 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
||||
trava: 1.2.1
|
||||
transitivePeerDependencies:
|
||||
- nostr-signer-capacitor-plugin
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/lib@0.6.3':
|
||||
'@welshman/lib@0.6.8':
|
||||
dependencies:
|
||||
'@scure/base': 1.2.6
|
||||
'@types/events': 3.0.3
|
||||
events: 3.3.0
|
||||
|
||||
'@welshman/net@0.6.3(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/net@0.6.8(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.8
|
||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
||||
events: 3.3.0
|
||||
isomorphic-ws: 5.0.0(ws@8.18.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/router@0.6.3(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/router@0.6.8(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.8
|
||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/signer@0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/signer@0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@noble/curves': 1.9.7
|
||||
'@noble/hashes': 1.8.0
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.8
|
||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
||||
nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.4.3)
|
||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/store@0.6.3(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/store@0.6.8(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.8
|
||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
||||
svelte: 4.2.20
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/util@0.6.3(typescript@5.9.3)':
|
||||
'@welshman/util@0.6.8(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@types/ws': 8.18.1
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/lib': 0.6.8
|
||||
js-base64: 3.7.8
|
||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||
nostr-wasm: 0.1.0
|
||||
@@ -8625,11 +8625,11 @@ snapshots:
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
||||
nostr-editor@1.0.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))):
|
||||
nostr-editor@1.0.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))):
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-image': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-link': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-image': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/extension-link': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@tiptap/pm': 2.26.3
|
||||
js-base64: 3.7.8
|
||||
light-bolt11-decoder: 3.2.0
|
||||
|
||||
+1
-1
@@ -395,7 +395,7 @@ progress[value]::-webkit-progress-value {
|
||||
/* chat view */
|
||||
|
||||
.chat__compose {
|
||||
@apply cb cw fixed;
|
||||
@apply cb cw fixed z-compose;
|
||||
}
|
||||
|
||||
.chat__scroll-down {
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</script>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon={Inbox} />
|
||||
@@ -108,7 +108,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl">
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon={Bell} />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
const path = makeCalendarPath(url, event.id)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if room && showRoom}
|
||||
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<ChannelName {room} {url} />
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, room}: Props = $props()
|
||||
const {url, h}: Props = $props()
|
||||
</script>
|
||||
|
||||
<CalendarEventForm {url} {room}>
|
||||
<CalendarEventForm {url} {h}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
header: Snippet
|
||||
initialValues?: {
|
||||
d: string
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const {url, room, header, initialValues}: Props = $props()
|
||||
const {url, h, header, initialValues}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -85,8 +85,8 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (room) {
|
||||
tags.push(["h", room])
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import {makeCalendarPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -15,16 +15,18 @@
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}>
|
||||
<Link
|
||||
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
|
||||
href={makeCalendarPath(url, event.id)}>
|
||||
<CalendarEventHeader {event} />
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if room}
|
||||
in <ChannelLink {url} {room} />
|
||||
{#if h}
|
||||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<CalendarEventActions showActivity {url} {event} />
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {channelsById, makeChannelId} from "@app/core/state"
|
||||
|
||||
const {url, room} = $props()
|
||||
</script>
|
||||
|
||||
{$channelsById.get(makeChannelId(url, room))?.name || room}
|
||||
@@ -2,21 +2,14 @@
|
||||
import {type Instance} from "tippy.js"
|
||||
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {
|
||||
thunks,
|
||||
mergeThunks,
|
||||
pubkey,
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
sendWrapped,
|
||||
} from "@welshman/app"
|
||||
import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
||||
import {isMobile} from "@lib/html"
|
||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
@@ -37,7 +30,6 @@
|
||||
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
||||
|
||||
const isOwn = event.pubkey === $pubkey
|
||||
const profile = deriveProfile(event.pubkey)
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||
@@ -107,8 +99,8 @@
|
||||
<div class="flex items-center gap-2">
|
||||
{#if !isOwn}
|
||||
<Button onclick={openProfile} class="flex items-center gap-1">
|
||||
<Avatar
|
||||
src={$profile?.picture}
|
||||
<ProfileCircle
|
||||
pubkey={event.pubkey}
|
||||
class="border border-solid border-base-content"
|
||||
size={4} />
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
type Props = {
|
||||
url: string
|
||||
onClick: () => void
|
||||
room?: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, room, onClick}: Props = $props()
|
||||
const {url, h, onClick}: Props = $props()
|
||||
|
||||
const createGoal = () => pushModal(GoalCreate, {url, room})
|
||||
const createGoal = () => pushModal(GoalCreate, {url, h})
|
||||
|
||||
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, room})
|
||||
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, h})
|
||||
|
||||
const createThread = () => pushModal(ThreadCreate, {url, room})
|
||||
const createThread = () => pushModal(ThreadCreate, {url, h})
|
||||
|
||||
let ul: Element
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}>
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md" bind:this={ul}>
|
||||
<li>
|
||||
<Button onclick={createGoal}>
|
||||
<Icon size={4} icon={StarFallMinimalistic} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import type {ProfilePointer} from "@welshman/content"
|
||||
import {deriveProfileDisplay} from "@welshman/app"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
const {value, url}: Props = $props()
|
||||
|
||||
const display = deriveProfileDisplay(value.pubkey, removeNil([url]))
|
||||
const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
||||
</script>
|
||||
|
||||
@@ -8,29 +8,29 @@
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import {goToEvent} from "@app/util/routes"
|
||||
import {displayChannel} from "@app/core/state"
|
||||
import {displayRoom} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
events: TrustedEvent[]
|
||||
latest: TrustedEvent
|
||||
earliest: TrustedEvent
|
||||
participants: string[]
|
||||
}
|
||||
|
||||
const {url, room, events, latest, earliest, participants}: Props = $props()
|
||||
const {url, h, events, latest, earliest, participants}: Props = $props()
|
||||
</script>
|
||||
|
||||
<Button class="card2 bg-alt" onclick={() => goToEvent(earliest)}>
|
||||
<Button class="card2 bg-alt shadow-lg" onclick={() => goToEvent(earliest)}>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<ProfileCircle pubkey={earliest.pubkey} size={10} />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||
{#if room}
|
||||
{#if h}
|
||||
<span class="truncate font-medium text-blue-400">
|
||||
#{displayChannel(url, room)}
|
||||
#{displayRoom(url, h)}
|
||||
</span>
|
||||
<span class="opacity-50">•</span>
|
||||
{/if}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}>
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md" bind:this={ul}>
|
||||
{#if isRoot}
|
||||
<li>
|
||||
<Button onclick={share}>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import {channelsByUrl} from "@app/core/state"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {roomsByUrl} from "@app/core/state"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
|
||||
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
|
||||
@@ -22,8 +22,8 @@
|
||||
goto(makeRoomPath(url, selection), {replaceState: true})
|
||||
}
|
||||
|
||||
const toggleRoom = (room: string) => {
|
||||
selection = room === selection ? "" : room
|
||||
const toggleRoom = (h: string) => {
|
||||
selection = h === selection ? "" : h
|
||||
}
|
||||
|
||||
let selection = $state("")
|
||||
@@ -39,14 +39,14 @@
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
{#each $channelsByUrl.get(url) || [] as channel (channel.room)}
|
||||
{#each $roomsByUrl.get(url) || [] as room (room.h)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
class:btn-neutral={selection !== channel.room}
|
||||
class:btn-primary={selection === channel.room}
|
||||
onclick={() => toggleRoom(channel.room)}>
|
||||
#<ChannelName {...channel} />
|
||||
class:btn-neutral={selection !== room.h}
|
||||
class:btn-primary={selection === room.h}
|
||||
onclick={() => toggleRoom(room.h)}>
|
||||
#<RoomName {...room} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {makeGoalPath, makeSpacePath} from "@app/util/routes"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const path = makeGoalPath(url, event.id)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const deleteReaction = async (event: TrustedEvent) =>
|
||||
@@ -31,9 +31,9 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if room && showRoom}
|
||||
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<ChannelName {room} {url} />
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, room}: Props = $props()
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (room) {
|
||||
tags.push(["h", room])
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import GoalActions from "@app/components/GoalActions.svelte"
|
||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import {makeGoalPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -17,10 +17,10 @@
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const summary = getTagValue("summary", event.tags)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeGoalPath(url, event.id)}>
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-md" href={makeGoalPath(url, event.id)}>
|
||||
<p class="text-2xl">{event.content}</p>
|
||||
<Content
|
||||
event={{content: summary, tags: event.tags}}
|
||||
@@ -32,8 +32,8 @@
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if room}
|
||||
in <ChannelLink {url} {room} />
|
||||
{#if h}
|
||||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<GoalActions showActivity {url} {event} />
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-96 rounded-box bg-base-100 p-4 shadow-lg">
|
||||
<div class="w-96 rounded-box bg-base-100 p-4 shadow-2xl">
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Magnifier} />
|
||||
<input bind:value={searchTerm} class="grow" type="text" placeholder="Search icons..." />
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import SquareTopDown from "@assets/icons/square-top-down.svg?dataurl"
|
||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Login from "@assets/icons/login-3.svg?dataurl"
|
||||
import History from "@assets/icons/history.svg?dataurl"
|
||||
import Tuning2 from "@assets/icons/tuning-2.svg?dataurl"
|
||||
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
||||
@@ -47,6 +49,7 @@
|
||||
hasNip29,
|
||||
alerts,
|
||||
deriveUserCanCreateRoom,
|
||||
deriveUserIsSpaceAdmin,
|
||||
} from "@app/core/state"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
@@ -63,6 +66,7 @@
|
||||
const otherRooms = deriveOtherRooms(url)
|
||||
const members = deriveSpaceMembers(url)
|
||||
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
|
||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||
|
||||
const spaceKinds = derived(
|
||||
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
||||
@@ -130,7 +134,7 @@
|
||||
<Popover hideOnClick onClose={toggleMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl">
|
||||
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
<li>
|
||||
<Button onclick={createInvite}>
|
||||
<Icon icon={LinkRound} />
|
||||
@@ -149,7 +153,15 @@
|
||||
View Members ({$members.length})
|
||||
</Button>
|
||||
</li>
|
||||
{#if $relay?.pubkey}
|
||||
{#if $userIsAdmin}
|
||||
<li>
|
||||
<Link external href="https://landlubber.coracle.social">
|
||||
<Icon icon={Tuning2} />
|
||||
Manage Space
|
||||
<Icon icon={SquareTopDown} size={4} class="opacity-50" />
|
||||
</Link>
|
||||
</li>
|
||||
{:else if $relay?.pubkey}
|
||||
<li>
|
||||
<Link href={makeChatPath([$relay.pubkey])}>
|
||||
<Icon icon={Letter} />
|
||||
@@ -174,7 +186,7 @@
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto">
|
||||
<div class="flex max-h-[calc(100vh-250px)] min-h-0 flex-col gap-1 overflow-auto">
|
||||
{#if hasNip29($relay)}
|
||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
||||
<Icon icon={History} /> Recent Activity
|
||||
@@ -216,8 +228,8 @@
|
||||
<div class="h-2"></div>
|
||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||
{/if}
|
||||
{#each $userRooms as room, i (room)}
|
||||
<MenuSpaceRoomItem {replaceState} notify {url} {room} />
|
||||
{#each $userRooms as h, i (h)}
|
||||
<MenuSpaceRoomItem {replaceState} notify {url} {h} />
|
||||
{/each}
|
||||
{#if $otherRooms.length > 0}
|
||||
<div class="h-2"></div>
|
||||
@@ -229,8 +241,8 @@
|
||||
{/if}
|
||||
</SecondaryNavHeader>
|
||||
{/if}
|
||||
{#each $otherRooms as room, i (room)}
|
||||
<MenuSpaceRoomItem {replaceState} {url} {room} />
|
||||
{#each $otherRooms as h, i (h)}
|
||||
<MenuSpaceRoomItem {replaceState} {url} {h} />
|
||||
{/each}
|
||||
{#if $canCreateRoom}
|
||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
||||
|
||||
@@ -1,43 +1,24 @@
|
||||
<script lang="ts">
|
||||
import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
import {deriveChannel} from "@app/core/state"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
room: any
|
||||
h: any
|
||||
notify?: boolean
|
||||
replaceState?: boolean
|
||||
}
|
||||
|
||||
const {url, room, notify = false, replaceState = false}: Props = $props()
|
||||
const {url, h, notify = false, replaceState = false}: Props = $props()
|
||||
|
||||
const path = makeRoomPath(url, room)
|
||||
const channel = deriveChannel(url, room)
|
||||
const path = makeRoomPath(url, h)
|
||||
</script>
|
||||
|
||||
<SecondaryNavItem
|
||||
href={path}
|
||||
{replaceState}
|
||||
notification={notify ? $notifications.has(path) : false}>
|
||||
{#if $channel?.picture}
|
||||
{@const src = $channel.picture}
|
||||
{#if src.match("\.(png|svg)$") || src.match("image/(png|svg)")}
|
||||
<Icon icon={src} />
|
||||
{:else}
|
||||
<img alt="Room icon" {src} class="h-6 w-6 rounded-lg" />
|
||||
{/if}
|
||||
{:else if $channel?.closed || $channel?.private}
|
||||
<Icon icon={Lock} />
|
||||
{:else}
|
||||
<Icon icon={Hashtag} />
|
||||
{/if}
|
||||
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||
<ChannelName {url} {room} />
|
||||
</div>
|
||||
<RoomNameWithImage {url} {h} />
|
||||
</SecondaryNavItem>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
|
||||
import {userSpaceUrls, PLATFORM_RELAYS} from "@app/core/state"
|
||||
</script>
|
||||
|
||||
<div class="column menu gap-2">
|
||||
{#each PLATFORM_RELAYS as url (url)}
|
||||
<MenuSpacesItem {url} />
|
||||
{:else}
|
||||
{#if $userSpaceUrls.length > 0}
|
||||
{#each $userSpaceUrls as url (url)}
|
||||
<MenuSpacesItem {url} />
|
||||
{/each}
|
||||
<Divider />
|
||||
{/if}
|
||||
<Link href="/discover">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Compass} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Explore Spaces</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Join create, or browse spaces</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
@@ -13,9 +13,9 @@
|
||||
</script>
|
||||
|
||||
<Link replaceState href={path}>
|
||||
<CardButton class="btn-neutral">
|
||||
<CardButton class="btn-neutral shadow-md">
|
||||
{#snippet icon()}
|
||||
<div><SpaceAvatar {url} /></div>
|
||||
<RelayIcon {url} size={12} />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div class="flex gap-1">
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
import Dialog from "@lib/components/Dialog.svelte"
|
||||
import {modal, clearModals} from "@app/util/modal"
|
||||
|
||||
const closeModals = () => {
|
||||
if ($modal && !$modal.options.noEscape) {
|
||||
clearModals()
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyDown = (e: any) => {
|
||||
if (e.code === "Escape" && e.target === document.body) {
|
||||
clearModals()
|
||||
closeModals()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +33,7 @@
|
||||
instance = mount(wrapper as any, {
|
||||
target: element,
|
||||
props: {
|
||||
onClose: clearModals,
|
||||
onClose: closeModals,
|
||||
children: createRawSnippet(() => ({
|
||||
render: () => "<div></div>",
|
||||
setup: (target: Element) => {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl">
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||
<div class="flex justify-between">
|
||||
<Profile {pubkey} {url} />
|
||||
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex">
|
||||
|
||||
@@ -4,12 +4,18 @@
|
||||
import {goto} from "$app/navigation"
|
||||
import {splitAt} from "@welshman/lib"
|
||||
import {userProfile, shouldUnwrap} from "@welshman/app"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Widget from "@assets/icons/widget.svg?dataurl"
|
||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||
import MenuSpaces from "@app/components/MenuSpaces.svelte"
|
||||
import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte"
|
||||
import MenuSettings from "@app/components/MenuSettings.svelte"
|
||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||
@@ -17,13 +23,6 @@
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import Widget from "@assets/icons/widget.svg?dataurl"
|
||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||
|
||||
type Props = {
|
||||
children?: Snippet
|
||||
@@ -31,9 +30,6 @@
|
||||
|
||||
const {children}: Props = $props()
|
||||
|
||||
const showSpacesMenu = () =>
|
||||
$userSpaceUrls.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd)
|
||||
|
||||
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
|
||||
|
||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||
@@ -66,7 +62,7 @@
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{:else}
|
||||
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
|
||||
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" />
|
||||
</PrimaryNavItem>
|
||||
<Divider />
|
||||
{#each primarySpaceUrls as url (url)}
|
||||
@@ -78,11 +74,11 @@
|
||||
class="tooltip-right"
|
||||
onclick={showOtherSpacesMenu}
|
||||
notification={otherSpaceNotifications}>
|
||||
<Avatar icon={Widget} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Other Spaces" src={Widget} />
|
||||
</PrimaryNavItem>
|
||||
{/if}
|
||||
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
|
||||
<Avatar icon={Compass} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Add a Space" src={Compass} size={7} />
|
||||
</PrimaryNavItem>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -95,17 +91,17 @@
|
||||
href="/settings/profile"
|
||||
prefix="/settings"
|
||||
class="tooltip-right">
|
||||
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Settings" src={$userProfile?.picture || UserRounded} class="rounded-full" />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem
|
||||
title="Messages"
|
||||
onclick={openChat}
|
||||
class="tooltip-right"
|
||||
notification={$notifications.has("/chat")}>
|
||||
<Avatar icon={Letter} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Messages" src={Letter} size={7} />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
||||
<Avatar icon={Magnifier} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Search" src={Magnifier} size={7} />
|
||||
</PrimaryNavItem>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,27 +114,28 @@
|
||||
<div
|
||||
class="border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
||||
<div class="content-padding-x content-sizing flex justify-between px-2">
|
||||
<div class="flex gap-2 sm:gap-8">
|
||||
<div class="flex gap-2 sm:gap-6">
|
||||
<PrimaryNavItem title="Home" href="/home">
|
||||
<Avatar icon={HomeSmile} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Home" src={HomeSmile} size={7} />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem
|
||||
title="Messages"
|
||||
onclick={openChat}
|
||||
notification={$notifications.has("/chat")}>
|
||||
<Avatar icon={Letter} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Messages" src={Letter} size={7} />
|
||||
</PrimaryNavItem>
|
||||
{#if PLATFORM_RELAYS.length !== 1}
|
||||
<PrimaryNavItem
|
||||
title="Spaces"
|
||||
onclick={showSpacesMenu}
|
||||
notification={anySpaceNotifications}>
|
||||
<Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" />
|
||||
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
|
||||
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={7} />
|
||||
</PrimaryNavItem>
|
||||
{/if}
|
||||
</div>
|
||||
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
|
||||
<Avatar icon={Settings} src={$userProfile?.picture} class="!h-10 !w-10" />
|
||||
<ImageIcon
|
||||
alt="Settings"
|
||||
src={$userProfile?.picture || Settings}
|
||||
size={7}
|
||||
class="rounded-full" />
|
||||
</PrimaryNavItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||
import {encodeRelay} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {lastPageBySpaceUrl} from "@app/util/history"
|
||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||
import {makeSpacePath, goToSpace} from "@app/util/routes"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const path = makeSpacePath(url)
|
||||
|
||||
const onClick = () => goto(lastPageBySpaceUrl.get(encodeRelay(url)) || path)
|
||||
const onClick = () => goToSpace(url)
|
||||
</script>
|
||||
|
||||
<PrimaryNavItem
|
||||
onclick={onClick}
|
||||
title={displayRelayUrl(url)}
|
||||
class="tooltip-right"
|
||||
notification={$notifications.has(path)}>
|
||||
<SpaceAvatar {url} />
|
||||
notification={$notifications.has(makeSpacePath(url))}>
|
||||
<RelayIcon {url} size={10} class="rounded-full" />
|
||||
</PrimaryNavItem>
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
<script lang="ts">
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import {displayPubkey} from "@welshman/util"
|
||||
import {
|
||||
deriveHandleForPubkey,
|
||||
displayHandle,
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
} from "@welshman/app"
|
||||
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import WotScore from "@app/components/WotScore.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
@@ -26,8 +21,7 @@
|
||||
|
||||
const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props()
|
||||
|
||||
const relays = removeNil([url])
|
||||
const profile = deriveProfile(pubkey, relays)
|
||||
const relays = removeUndefined([url])
|
||||
const profileDisplay = deriveProfileDisplay(pubkey, relays)
|
||||
const handle = deriveHandleForPubkey(pubkey)
|
||||
|
||||
@@ -38,7 +32,7 @@
|
||||
|
||||
<div class="flex max-w-full items-start gap-3">
|
||||
<Button onclick={openProfile} class="py-1">
|
||||
<Avatar src={$profile?.picture} size={avatarSize} />
|
||||
<ProfileCircle {pubkey} size={avatarSize} />
|
||||
</Button>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
<script lang="ts">
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import cx from "classnames"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
|
||||
type Props = {
|
||||
pubkey: string
|
||||
class?: string
|
||||
size?: number
|
||||
url?: string
|
||||
} & Record<string, any>
|
||||
}
|
||||
|
||||
const {pubkey, url, ...props}: Props = $props()
|
||||
const {pubkey, url, size = 7, ...props}: Props = $props()
|
||||
|
||||
const profile = deriveProfile(pubkey, removeNil([url]))
|
||||
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||
</script>
|
||||
|
||||
<Avatar src={$profile?.picture} icon={UserCircle} {...props} />
|
||||
<ImageIcon
|
||||
{size}
|
||||
class={cx(props.class, "rounded-full")}
|
||||
src={$profile?.picture || UserRounded}
|
||||
alt="Profile picture" />
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script lang="ts">
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
|
||||
const {...props} = $props()
|
||||
type Props = {
|
||||
pubkeys: string[]
|
||||
size?: number
|
||||
}
|
||||
|
||||
const {pubkeys, size = 7}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex pr-3">
|
||||
{#each props.pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
|
||||
{#each pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
|
||||
<div class="z-feature -mr-3 inline-block">
|
||||
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {...props} />
|
||||
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
@@ -41,7 +41,7 @@
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral">
|
||||
<Avatar src="/coracle.png" />
|
||||
<ImageIcon alt="Open in Coracle" src="/coracle.png" />
|
||||
Open in Coracle
|
||||
</Link>
|
||||
<Button onclick={openChat} class="btn btn-primary">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
const {pubkey, url}: Props = $props()
|
||||
|
||||
const profile = deriveProfile(pubkey, removeNil([url]))
|
||||
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||
</script>
|
||||
|
||||
{#if $profile}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import {deriveProfileDisplay} from "@welshman/app"
|
||||
|
||||
type Props = {
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
const {pubkey, url}: Props = $props()
|
||||
|
||||
const profileDisplay = deriveProfileDisplay(pubkey, removeNil([url]))
|
||||
const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
|
||||
</script>
|
||||
|
||||
{$profileDisplay}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {deriveGroupSelections, getSpaceUrlsFromGroupSelections} from "@app/core/state"
|
||||
@@ -26,7 +26,7 @@
|
||||
{#each spaceUrls as url (url)}
|
||||
<div class="card2 bg-alt flex flex-row items-center gap-2">
|
||||
<div class="flex-shrink-0">
|
||||
<SpaceAvatar {url} />
|
||||
<RelayIcon {url} size={12} />
|
||||
</div>
|
||||
<div class="flex flex-grow flex-col">
|
||||
<RelayName {url} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {onMount} from "svelte"
|
||||
import type {Snippet} from "svelte"
|
||||
import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
|
||||
@@ -134,10 +135,15 @@
|
||||
<button
|
||||
type="button"
|
||||
data-tip={tooltip}
|
||||
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full text-xs font-normal {reactionClass}"
|
||||
class:tooltip={!noTooltip && !isMobile}
|
||||
class:btn-neutral={!isOwn}
|
||||
class:btn-primary={isOwn}>
|
||||
class={cx(
|
||||
reactionClass,
|
||||
"flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full text-xs font-normal",
|
||||
{
|
||||
tooltip: !noTooltip && !isMobile,
|
||||
"border-neutral-content/20": !isOwn,
|
||||
"btn-primary": isOwn,
|
||||
},
|
||||
)}>
|
||||
<Reaction event={zaps[0].request} />
|
||||
<span>{amount}</span>
|
||||
</button>
|
||||
@@ -151,10 +157,15 @@
|
||||
<button
|
||||
type="button"
|
||||
data-tip={tooltip}
|
||||
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full font-normal {reactionClass}"
|
||||
class:tooltip={!noTooltip && !isMobile}
|
||||
class:btn-neutral={!isOwn}
|
||||
class:btn-primary={isOwn}
|
||||
class={cx(
|
||||
reactionClass,
|
||||
"flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal",
|
||||
{
|
||||
tooltip: !noTooltip && !isMobile,
|
||||
"border-neutral-content/20": !isOwn,
|
||||
"btn-primary": isOwn,
|
||||
},
|
||||
)}
|
||||
onclick={stopPropagation(preventDefault(onClick))}>
|
||||
<Reaction event={events[0]} />
|
||||
{#if events.length > 1}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
size?: number
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {url, size = 7, ...props}: Props = $props()
|
||||
|
||||
const relay = deriveRelay(url)
|
||||
</script>
|
||||
|
||||
<ImageIcon
|
||||
{size}
|
||||
src={$relay?.icon || RemoteControllerMinimalistic}
|
||||
alt="Relay image"
|
||||
class={props.class} />
|
||||
@@ -1,7 +1,11 @@
|
||||
<script lang="ts">
|
||||
import {deriveRelayDisplay} from "@welshman/app"
|
||||
|
||||
const {url} = $props()
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
|
||||
const display = $derived(deriveRelayDisplay(url))
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
|
||||
import Lock from "@assets/icons/lock.svg?dataurl"
|
||||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {deriveRoom} from "@app/core/state"
|
||||
|
||||
interface Props {
|
||||
h: any
|
||||
url: any
|
||||
}
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
</script>
|
||||
|
||||
{#if $room.isHidden}
|
||||
<Button
|
||||
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||
data-tip="This room is not visible to non-members.">
|
||||
<Icon size={4} icon={EyeClosed} />
|
||||
</Button>
|
||||
{:else if $room.isPrivate}
|
||||
<Button
|
||||
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||
data-tip="Only members can view messages.">
|
||||
<Icon size={4} icon={Lock} />
|
||||
</Button>
|
||||
{:else if $room.isRestricted}
|
||||
<Button
|
||||
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||
data-tip="Only members can send messages.">
|
||||
<Icon size={4} icon={Microphone} />
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -16,13 +16,14 @@
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
room?: string
|
||||
h?: string
|
||||
content?: string
|
||||
onEscape?: () => void
|
||||
onEditPrevious?: () => void
|
||||
onSubmit: (event: EventContent) => void
|
||||
}
|
||||
|
||||
const {url, room, content, onEditPrevious, onSubmit}: Props = $props()
|
||||
const {url, h, content, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
||||
|
||||
const autofocus = !isMobile
|
||||
|
||||
@@ -34,6 +35,10 @@
|
||||
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
|
||||
|
||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
onEscape?.()
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
|
||||
onEditPrevious?.()
|
||||
}
|
||||
@@ -74,7 +79,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||
<form class="relative flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||
<div class="join">
|
||||
<Button
|
||||
data-tip="Add an image"
|
||||
@@ -90,7 +95,7 @@
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={ComposeMenu}
|
||||
props={{url, room, onClick: hidePopover}}
|
||||
props={{url, h, onClick: hidePopover}}
|
||||
params={{trigger: "manual", interactive: true}}>
|
||||
<Button
|
||||
data-tip="More options"
|
||||
+2
-2
@@ -12,10 +12,10 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
||||
class="relative flex h-8 items-center justify-between border-l-2 border-solid border-primary bg-base-300 px-2 pr-7 text-xs"
|
||||
transition:slide>
|
||||
<p class="text-primary">Editing message</p>
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||
<Button onclick={clear} class="flex items-center">
|
||||
<Icon icon={CloseCircle} />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1,181 +1,47 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {uniqBy, nth} from "@welshman/lib"
|
||||
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
|
||||
import {deriveRelay, waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||
import {preventDefault, compressFile} from "@lib/html"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
|
||||
import {hasNip29, loadChannel} from "@app/core/state"
|
||||
import RoomForm from "@app/components/RoomForm.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {uploadFile} from "@app/core/commands"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const room = makeRoomMeta()
|
||||
const relay = deriveRelay(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const tryCreate = async () => {
|
||||
room.tags = uniqBy(nth(0), [...room.tags, ["name", name]])
|
||||
|
||||
if (imageFile) {
|
||||
const {error, result} = await uploadFile(imageFile)
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
room.tags.push(["picture", result.url, ...result.tags])
|
||||
} else if (selectedIcon) {
|
||||
room.tags.push(["picture", selectedIcon])
|
||||
}
|
||||
|
||||
const createMessage = await waitForThunkError(createRoom(url, room))
|
||||
|
||||
if (createMessage && !createMessage.match(/^duplicate:|already a member/)) {
|
||||
return pushToast({theme: "error", message: createMessage})
|
||||
}
|
||||
|
||||
const editMessage = await waitForThunkError(editRoom(url, room))
|
||||
|
||||
if (editMessage) {
|
||||
return pushToast({theme: "error", message: editMessage})
|
||||
}
|
||||
|
||||
const joinMessage = await waitForThunkError(joinRoom(url, room))
|
||||
|
||||
if (joinMessage && !joinMessage.includes("already")) {
|
||||
return pushToast({theme: "error", message: joinMessage})
|
||||
}
|
||||
|
||||
await loadChannel(url, room.id)
|
||||
|
||||
goto(makeSpacePath(url, room.id))
|
||||
}
|
||||
|
||||
const create = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await tryCreate()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let name = $state("")
|
||||
let loading = $state(false)
|
||||
let imageFile = $state<File | undefined>()
|
||||
let imagePreview = $state<string | undefined>()
|
||||
let selectedIcon = $state<string | undefined>()
|
||||
|
||||
const handleImageUpload = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
|
||||
if (file && file.type.startsWith("image/")) {
|
||||
selectedIcon = undefined
|
||||
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = e => {
|
||||
imagePreview = e.target?.result as string
|
||||
}
|
||||
|
||||
reader.readAsDataURL(imageFile)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIconSelect = (iconUrl: string) => {
|
||||
imageFile = undefined
|
||||
imagePreview = undefined
|
||||
selectedIcon = iconUrl
|
||||
}
|
||||
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, room.h))
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(create)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Create a Room</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>
|
||||
On <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{#if hasNip29($relay)}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Room Name</p>
|
||||
<RoomForm {url} {onsubmit}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Create a Room</div>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Hashtag} />
|
||||
<input bind:value={name} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="font-bold">Room Icon</p>
|
||||
<div class="flex items-center gap-4">
|
||||
{#if imagePreview}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-75">Selected:</span>
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Room icon preview"
|
||||
class="h-8 w-8 rounded-lg object-cover" />
|
||||
</div>
|
||||
{:else if selectedIcon}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-75">Selected:</span>
|
||||
<Icon icon={selectedIcon} class="h-8 w-8" />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-sm opacity-75">No icon selected</span>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
|
||||
<Icon icon={StickerSmileSquare} size={4} />
|
||||
Select
|
||||
</IconPickerButton>
|
||||
<label class="btn btn-neutral btn-sm cursor-pointer">
|
||||
<Icon icon={UploadMinimalistic} size={4} />
|
||||
Upload
|
||||
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
|
||||
</label>
|
||||
{#snippet info()}
|
||||
<div>
|
||||
On <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="bg-alt card2 row-2">
|
||||
<Icon icon={Danger} />
|
||||
This relay does not support creating rooms.
|
||||
</p>
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={!name || loading || !hasNip29($relay)}>
|
||||
<Spinner {loading}>Create Room</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{/snippet}
|
||||
{#snippet footer({loading})}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Create Room</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
{/snippet}
|
||||
</RoomForm>
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import Login3 from "@assets/icons/login-3.svg?dataurl"
|
||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
|
||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||
import Lock from "@assets/icons/lock.svg?dataurl"
|
||||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import ProfileList from "@app/components/ProfileList.svelte"
|
||||
import RoomEdit from "@app/components/RoomEdit.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import RoomImage from "@app/components/RoomImage.svelte"
|
||||
import {
|
||||
deriveRoom,
|
||||
deriveRoomMembers,
|
||||
deriveUserIsRoomAdmin,
|
||||
deriveUserRoomMembershipStatus,
|
||||
MembershipStatus,
|
||||
} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
h: string
|
||||
}
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
const members = deriveRoomMembers(url, h)
|
||||
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
||||
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const startEdit = () => pushModal(RoomEdit, {url, h})
|
||||
|
||||
const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const message = await waitForThunkError(f(url, makeRoomMeta({h})))
|
||||
|
||||
if (message && !message.startsWith("duplicate:")) {
|
||||
pushToast({theme: "error", message})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const join = () => handleLoading(joinRoom)
|
||||
|
||||
const leave = () => handleLoading(leaveRoom)
|
||||
|
||||
const showMembers = () =>
|
||||
pushModal(ProfileList, {
|
||||
title: "Members",
|
||||
subtitle: `of ${$room?.name || h}`,
|
||||
pubkeys: $members,
|
||||
})
|
||||
|
||||
const startDelete = () =>
|
||||
pushModal(Confirm, {
|
||||
title: "Are you sure you want to delete this room?",
|
||||
message:
|
||||
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
|
||||
confirm: async () => {
|
||||
const thunk = deleteRoom(url, $room)
|
||||
const message = await waitForThunkError(thunk)
|
||||
|
||||
if (message) {
|
||||
repository.removeEvent(thunk.event.id)
|
||||
pushToast({theme: "error", message})
|
||||
} else {
|
||||
goto(makeSpacePath(url))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
let loading = $state(false)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-3">
|
||||
<RoomImage {url} {h} size={8} />
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<RoomName {url} {h} class="text-2xl" />
|
||||
<span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#if $room?.isRestricted}
|
||||
<Button
|
||||
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||
data-tip="Only members can send messages.">
|
||||
<Icon size={4} icon={Microphone} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if $room?.isPrivate}
|
||||
<Button
|
||||
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||
data-tip="Only members can view messages.">
|
||||
<Icon size={4} icon={Lock} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if $room?.isHidden}
|
||||
<Button
|
||||
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||
data-tip="This room is not visible to non-members.">
|
||||
<Icon size={4} icon={EyeClosed} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if $room?.isClosed}
|
||||
<Button
|
||||
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||
data-tip="Requests to join this room will be ignored.">
|
||||
<Icon size={4} icon={MinusCircle} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $room?.about}
|
||||
<p>{$room.about}</p>
|
||||
{/if}
|
||||
{#if $members.length > 0}
|
||||
<div class="card2 card2-sm bg-alt flex gap-4">
|
||||
<span>Members:</span>
|
||||
<Button onclick={showMembers}>
|
||||
<ProfileCircles pubkeys={$members} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
{#if $userIsAdmin}
|
||||
<Button class="btn btn-outline btn-error" onclick={startDelete}>
|
||||
<Icon icon={TrashBin2} />
|
||||
<span class="hidden md:inline">Delete Room</span>
|
||||
</Button>
|
||||
<Button class="btn btn-primary" onclick={startEdit}>
|
||||
<Icon icon={Pen} />
|
||||
Edit Room
|
||||
</Button>
|
||||
{:else if $membershipStatus === MembershipStatus.Initial}
|
||||
<Button class="btn btn-neutral" disabled={loading} onclick={join}>
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={Login3} />
|
||||
{/if}
|
||||
Join member list
|
||||
</Button>
|
||||
{:else if $membershipStatus === MembershipStatus.Pending}
|
||||
<Button class="btn btn-neutral">
|
||||
<Icon icon={ClockCircle} />
|
||||
Membership pending
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral" disabled={loading} onclick={leave}>
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={Login3} />
|
||||
{/if}
|
||||
Leave member list
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import RoomForm from "@app/components/RoomForm.svelte"
|
||||
import {deriveRoom} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
h: string
|
||||
}
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, h))
|
||||
</script>
|
||||
|
||||
<RoomForm {url} {onsubmit} initialValues={$room}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Edit a Room</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>
|
||||
On <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{/snippet}
|
||||
{#snippet footer({loading})}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Save Changes</Spinner>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
{/snippet}
|
||||
</RoomForm>
|
||||
@@ -0,0 +1,206 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
import {makeRoomMeta} from "@welshman/util"
|
||||
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||
import {preventDefault, compressFile} from "@lib/html"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {uploadFile} from "@app/core/commands"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
header: Snippet
|
||||
footer: Snippet<[{loading: boolean}]>
|
||||
onsubmit: (room: RoomMeta) => void
|
||||
initialValues?: RoomMeta
|
||||
}
|
||||
|
||||
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
|
||||
|
||||
const values = $state(initialValues)
|
||||
|
||||
const submit = async () => {
|
||||
const room = $state.snapshot(values)
|
||||
|
||||
if (imageFile) {
|
||||
const {error, result} = await uploadFile(imageFile)
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
room.picture = result.url
|
||||
room.pictureMeta = result.tags
|
||||
} else if (selectedIcon) {
|
||||
room.picture = selectedIcon
|
||||
}
|
||||
|
||||
const createMessage = await waitForThunkError(createRoom(url, room))
|
||||
|
||||
if (createMessage && !createMessage.includes("already")) {
|
||||
return pushToast({theme: "error", message: createMessage})
|
||||
}
|
||||
|
||||
const editMessage = await waitForThunkError(editRoom(url, room))
|
||||
|
||||
if (editMessage) {
|
||||
return pushToast({theme: "error", message: editMessage})
|
||||
}
|
||||
|
||||
const joinMessage = await waitForThunkError(joinRoom(url, room))
|
||||
|
||||
if (joinMessage && !joinMessage.includes("already")) {
|
||||
return pushToast({theme: "error", message: joinMessage})
|
||||
}
|
||||
|
||||
onsubmit(room)
|
||||
}
|
||||
|
||||
const trySubmit = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await submit()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
let imageFile = $state<File | undefined>()
|
||||
let imagePreview = $state(initialValues.picture)
|
||||
let selectedIcon = $state<string | undefined>()
|
||||
|
||||
const handleImageUpload = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
|
||||
if (file && file.type.startsWith("image/")) {
|
||||
selectedIcon = undefined
|
||||
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = e => {
|
||||
imagePreview = e.target?.result as string
|
||||
}
|
||||
|
||||
reader.readAsDataURL(imageFile)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIconSelect = (iconUrl: string) => {
|
||||
imageFile = undefined
|
||||
imagePreview = undefined
|
||||
selectedIcon = iconUrl
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(trySubmit)}>
|
||||
{@render header()}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Icon</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
{#if imagePreview}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-75">Selected:</span>
|
||||
<ImageIcon src={imagePreview} alt="Room icon preview" />
|
||||
</div>
|
||||
{:else if selectedIcon}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-75">Selected:</span>
|
||||
<Icon icon={selectedIcon} class="h-8 w-8" />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-sm opacity-75">No icon selected</span>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
|
||||
<Icon icon={StickerSmileSquare} size={4} />
|
||||
Select
|
||||
</IconPickerButton>
|
||||
<label class="btn btn-neutral btn-sm cursor-pointer">
|
||||
<Icon icon={UploadMinimalistic} size={4} />
|
||||
Upload
|
||||
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Name</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
{#if imagePreview}
|
||||
<ImageIcon src={imagePreview} alt="Room icon preview" />
|
||||
{:else if selectedIcon}
|
||||
<Icon icon={selectedIcon} class="h-8 w-8" />
|
||||
{:else}
|
||||
<Icon icon={Hashtag} />
|
||||
{/if}
|
||||
<input bind:value={values.name} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Description</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<input bind:value={values.about} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<strong>Restricted</strong>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
|
||||
<span class="text-sm opacity-75">Only allow members to send messages</span>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<strong>Private</strong>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
|
||||
<span class="text-sm opacity-75">Only allow members to read messages</span>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<strong>Hidden</strong>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="checkbox" bind:checked={values.isHidden} />
|
||||
<span class="text-sm opacity-75">Hide this group from non-members</span>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<strong>Closed</strong>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
|
||||
<span class="text-sm opacity-75">Ignore requests to join</span>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{@render footer({loading})}
|
||||
</form>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import {deriveRoom} from "@app/core/state"
|
||||
|
||||
interface Props {
|
||||
h: string
|
||||
url: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
const {url, h, size = 5}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
</script>
|
||||
|
||||
{#if $room.picture}
|
||||
<ImageIcon src={$room.picture} {size} alt="Room icon" />
|
||||
{:else}
|
||||
<Icon icon={Hashtag} {size} />
|
||||
{/if}
|
||||
@@ -7,7 +7,6 @@
|
||||
thunks,
|
||||
pubkey,
|
||||
mergeThunks,
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
displayProfileByPubkey,
|
||||
} from "@welshman/app"
|
||||
@@ -16,21 +15,21 @@
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import ReplyAlt from "@assets/icons/reply.svg?dataurl"
|
||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ChannelItemZapButton from "@app/components/ChannelItemZapButton.svelte"
|
||||
import ChannelItemEmojiButton from "@app/components/ChannelItemEmojiButton.svelte"
|
||||
import ChannelItemMenuButton from "@app/components/ChannelItemMenuButton.svelte"
|
||||
import ChannelItemMenuMobile from "@app/components/ChannelItemMenuMobile.svelte"
|
||||
import ChannelItemContent from "@app/components/ChannelItemContent.svelte"
|
||||
import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte"
|
||||
import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte"
|
||||
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
|
||||
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
|
||||
import RoomItemContent from "@app/components/RoomItemContent.svelte"
|
||||
import {colors, ENABLE_ZAPS, deriveEventsForUrl} from "@app/core/state"
|
||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {getChannelItemPath} from "@app/util/routes"
|
||||
import {getRoomItemPath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
interface Props {
|
||||
@@ -53,10 +52,9 @@
|
||||
onEdit,
|
||||
}: Props = $props()
|
||||
|
||||
const path = getChannelItemPath(url, event)
|
||||
const path = getRoomItemPath(url, event)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const today = formatTimestampAsDate(now())
|
||||
const profile = deriveProfile(event.pubkey, [url])
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||
@@ -65,7 +63,7 @@
|
||||
const reply = () => replyTo!(event)
|
||||
const edit = canEdit(event) ? () => onEdit(event) : undefined
|
||||
|
||||
const onTap = () => pushModal(ChannelItemMenuMobile, {url, event, reply, edit})
|
||||
const onTap = () => pushModal(RoomItemMenuMobile, {url, event, reply, edit})
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||
|
||||
@@ -83,7 +81,10 @@
|
||||
<div class="flex w-full gap-3 overflow-auto">
|
||||
{#if showPubkey}
|
||||
<Button onclick={openProfile} class="flex items-start">
|
||||
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} />
|
||||
<ProfileCircle
|
||||
pubkey={event.pubkey}
|
||||
class="border border-solid border-base-content"
|
||||
size={8} />
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="w-8 min-w-8 max-w-8"></div>
|
||||
@@ -105,7 +106,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
||||
<ChannelItemContent {url} {event} />
|
||||
<RoomItemContent {url} {event} />
|
||||
{#if thunk}
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||
{/if}
|
||||
@@ -142,9 +143,9 @@
|
||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}>
|
||||
{#if ENABLE_ZAPS}
|
||||
<ChannelItemZapButton {url} {event} />
|
||||
<RoomItemZapButton {url} {event} />
|
||||
{/if}
|
||||
<ChannelItemEmojiButton {url} {event} />
|
||||
<RoomItemEmojiButton {url} {event} />
|
||||
{#if replyTo}
|
||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||
<Icon icon={Reply} size={4} />
|
||||
@@ -155,7 +156,7 @@
|
||||
<Icon icon={Pen} size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
<ChannelItemMenuButton {url} {event} />
|
||||
<RoomItemMenuButton {url} {event} />
|
||||
</button>
|
||||
{/if}
|
||||
</TapTarget>
|
||||
+2
-2
@@ -5,11 +5,11 @@
|
||||
import {isMobile} from "@lib/html"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import {getChannelItemPath} from "@app/util/routes"
|
||||
import {getRoomItemPath} from "@app/util/routes"
|
||||
|
||||
const props: ComponentProps<typeof NoteContent> = $props()
|
||||
|
||||
const path = getChannelItemPath(props.url!, props.event)
|
||||
const path = getRoomItemPath(props.url!, props.event)
|
||||
</script>
|
||||
|
||||
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md">
|
||||
<li>
|
||||
<Button onclick={showInfo}>
|
||||
<Icon size={4} icon={Code2} />
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import ChannelItemMenu from "@app/components/ChannelItemMenu.svelte"
|
||||
import RoomItemMenu from "@app/components/RoomItemMenu.svelte"
|
||||
|
||||
const {url, event} = $props()
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</Button>
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={ChannelItemMenu}
|
||||
component={RoomItemMenu}
|
||||
props={{url, event, onClick}}
|
||||
params={{trigger: "manual", interactive: true}} />
|
||||
</div>
|
||||
+2
-2
@@ -17,7 +17,7 @@
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import {ENABLE_ZAPS} from "@app/core/state"
|
||||
import {publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {getChannelItemPath} from "@app/util/routes"
|
||||
import {getRoomItemPath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
const {url, event, reply}: Props = $props()
|
||||
|
||||
const path = getChannelItemPath(url, event)
|
||||
const path = getRoomItemPath(url, event)
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
room: string
|
||||
h: string
|
||||
url: string
|
||||
class?: string
|
||||
unstyled?: boolean
|
||||
}
|
||||
|
||||
const {room, url, unstyled, ...props}: Props = $props()
|
||||
const {h, url, unstyled, ...props}: Props = $props()
|
||||
|
||||
const path = makeSpacePath(url, room)
|
||||
const path = makeSpacePath(url, h)
|
||||
</script>
|
||||
|
||||
<Link href={path} class={cx(props.class, {"link-content bg-alt": !unstyled})}>
|
||||
#<ChannelName {room} {url} />
|
||||
#<RoomName {h} {url} />
|
||||
</Link>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import {deriveRoom} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
h: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {url, h, ...props}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
</script>
|
||||
|
||||
<span class="ellipsize {props.class}">
|
||||
{$room?.name || h}
|
||||
</span>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import RoomImage from "@app/components/RoomImage.svelte"
|
||||
|
||||
interface Props {
|
||||
h: string
|
||||
url: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {url, h, ...props}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-grow items-center justify-between gap-4 {props.class}">
|
||||
<div class="flex items-center gap-3">
|
||||
<RoomImage {url} {h} />
|
||||
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||
<RoomName {url} {h} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +14,7 @@
|
||||
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {checkRelayAccess} from "@app/core/commands"
|
||||
import {attemptRelayAccess} from "@app/core/commands"
|
||||
import {deriveSocket} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
@@ -31,7 +31,7 @@
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const message = await checkRelayAccess(url, claim)
|
||||
const message = await attemptRelayAccess(url, claim)
|
||||
|
||||
if (message) {
|
||||
return pushToast({theme: "error", message, timeout: 30_000})
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<script lang="ts">
|
||||
import Login from "@assets/icons/login-3.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import SpaceCreateExternal from "@app/components/SpaceCreateExternal.svelte"
|
||||
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const startCreate = () => pushModal(SpaceCreateExternal)
|
||||
type Props = {
|
||||
hideDiscover?: boolean
|
||||
}
|
||||
|
||||
const {hideDiscover}: Props = $props()
|
||||
|
||||
const startJoin = () => pushModal(SpaceInviteAccept)
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<div class="column gap-2">
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Add a Space</div>
|
||||
@@ -23,8 +28,23 @@
|
||||
<div>Spaces are places where communities come together to work, play, and hang out.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{#if !hideDiscover}
|
||||
<Link href="/discover">
|
||||
<CardButton class="btn-primary">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Compass} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Explore Spaces</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Join create, or browse spaces</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
{/if}
|
||||
<Button onclick={startJoin}>
|
||||
<CardButton class="btn-primary">
|
||||
<CardButton class={hideDiscover ? "btn-primary" : "btn-neutral"}>
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Login} size={7} /></div>
|
||||
{/snippet}
|
||||
@@ -36,7 +56,7 @@
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Button>
|
||||
<Button onclick={startCreate}>
|
||||
<Link href="/spaces/create">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={AddCircle} size={7} /></div>
|
||||
@@ -48,5 +68,5 @@
|
||||
<div>Just a few questions and you'll be on your way.</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {parse, renderAsHtml} from "@welshman/content"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -11,12 +12,29 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {removeSpaceMembership, publishLeaveRequest, removeTrustedRelay} from "@app/core/commands"
|
||||
|
||||
const {url, error} = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
const back = () => goto("/home")
|
||||
|
||||
const requestAccess = () => pushModal(SpaceAccessRequest, {url})
|
||||
|
||||
const leaveSpace = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await removeSpaceMembership(url)
|
||||
await publishLeaveRequest({url})
|
||||
await removeTrustedRelay(url)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
goto("/home")
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(requestAccess)}>
|
||||
@@ -37,11 +55,16 @@
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary">
|
||||
Request Access
|
||||
<Icon icon={AltArrowRight} />
|
||||
Go Home
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<Button class="btn btn-outline btn-error" onclick={leaveSpace} disabled={loading}>
|
||||
Leave Space
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
Request Access
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
||||
|
||||
interface Props {
|
||||
url?: string
|
||||
}
|
||||
|
||||
const {url = ""}: Props = $props()
|
||||
|
||||
const relay = deriveRelay(url)
|
||||
</script>
|
||||
|
||||
<Avatar
|
||||
icon={RemoteControllerMinimalistic}
|
||||
class="!h-10 !w-10"
|
||||
alt={displayRelayUrl(url)}
|
||||
src={$relay?.icon} />
|
||||
@@ -8,22 +8,26 @@
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import SpaceVisitConfirm, {confirmSpaceVisit} from "@app/components/SpaceVisitConfirm.svelte"
|
||||
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
|
||||
import {attemptRelayAccess} from "@app/core/commands"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const next = () => {
|
||||
if (!error && Pool.get().get(url).auth.status === AuthStatus.None) {
|
||||
pushModal(SpaceVisitConfirm, {url}, {replaceState: true})
|
||||
const next = async () => {
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error, timeout: 30_000})
|
||||
}
|
||||
|
||||
if (Pool.get().get(url).auth.status === AuthStatus.None) {
|
||||
pushModal(SpaceJoinConfirm, {url}, {replaceState: true})
|
||||
} else {
|
||||
confirmSpaceVisit(url)
|
||||
await confirmSpaceJoin(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +53,10 @@
|
||||
</ModalHeader>
|
||||
<div class="m-auto flex flex-col gap-4">
|
||||
{#if loading}
|
||||
<Spinner loading>Hold tight, we're checking your connection.</Spinner>
|
||||
<p class="flex items-center gap-3">
|
||||
<span class="loading loading-spinner"></span>
|
||||
Hold tight, we're checking your connection.
|
||||
</p>
|
||||
{:else if error}
|
||||
<p>Oops! We ran into some problems:</p>
|
||||
<p class="card2 bg-alt">{error}</p>
|
||||
@@ -70,7 +77,7 @@
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
Go to Space
|
||||
Join Space
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import FireMinimalistic from "@assets/icons/fire-minimalistic.svg?dataurl"
|
||||
import Server from "@assets/icons/server.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import InfoRelay from "@app/components/InfoRelay.svelte"
|
||||
import InputProfilePicture from "@app/components/InputProfilePicture.svelte"
|
||||
import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const next = () => pushModal(SpaceCreateFinish)
|
||||
|
||||
let file: File | undefined = $state()
|
||||
let name = $state("")
|
||||
let relay = $state("")
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Customize your Space</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Give people a few details to go on. You can always change this later.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<div class="flex justify-center py-2">
|
||||
<InputProfilePicture bind:file />
|
||||
</div>
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Space Name</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={FireMinimalistic} />
|
||||
<input bind:value={name} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Relay</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Server} />
|
||||
<input bind:value={relay} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
This can be any nostr relay where you'd like to host your space.
|
||||
<Button class="link" onclick={() => pushModal(InfoRelay)}>What is a relay?</Button>
|
||||
</p>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary">
|
||||
Next
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -1,56 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
|
||||
import {PLATFORM_NAME} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const next = () => {
|
||||
window.open("https://relay.tools/signup")
|
||||
|
||||
setTimeout(() => pushModal(SpaceInviteAccept), 300)
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Create a Space</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Host your own space, for your community.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<p>
|
||||
<Link class="link" external href="https://relay.tools">relay.tools</Link> is a third-party service
|
||||
that allows anyone to run their own relay for use with {PLATFORM_NAME}, or any other
|
||||
nostr-compatible app.
|
||||
</p>
|
||||
<p>
|
||||
Once you've created a relay of your own, come back here to link {PLATFORM_NAME} with your new relay.
|
||||
</p>
|
||||
<p>
|
||||
Alternatively, you can
|
||||
<Link external class="link" href="https://github.com/coracle-social/frith"
|
||||
>run your own community relay</Link
|
||||
>.
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary">
|
||||
Let's go
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -1 +0,0 @@
|
||||
hi
|
||||
@@ -2,16 +2,22 @@
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
||||
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
||||
import Ghost from "@assets/icons/ghost-smile.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import SpaceEdit from "@app/components/SpaceEdit.svelte"
|
||||
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
|
||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||
import ProfileLatest from "@app/components/ProfileLatest.svelte"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
@@ -20,8 +26,11 @@
|
||||
const {url}: Props = $props()
|
||||
const relay = deriveRelay(url)
|
||||
const owner = $derived($relay?.pubkey)
|
||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay})
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
@@ -78,5 +87,18 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
||||
{#if $userIsAdmin}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button class="btn btn-primary" onclick={startEdit}>
|
||||
<Icon icon={Pen} />
|
||||
Edit Space
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
{:else}
|
||||
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
<script lang="ts">
|
||||
import {uniqBy, prop, append, ifLet} from "@welshman/lib"
|
||||
import type {RelayProfile} from "@welshman/util"
|
||||
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||
import {manageRelay, relays, fetchRelayProfileDirectly} from "@welshman/app"
|
||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
import {uploadFile} from "@app/core/commands"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
initialValues: RelayProfile
|
||||
}
|
||||
|
||||
const {url, initialValues}: Props = $props()
|
||||
|
||||
const values = $state(initialValues)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const submit = async () => {
|
||||
if (values.name != initialValues.name) {
|
||||
const res = await manageRelay(url, {
|
||||
method: ManagementMethod.ChangeRelayName,
|
||||
params: [values.name || ""],
|
||||
})
|
||||
|
||||
if (res.error) {
|
||||
return pushToast({theme: "error", message: res.error})
|
||||
}
|
||||
}
|
||||
|
||||
if (values.description != initialValues.description) {
|
||||
const res = await manageRelay(url, {
|
||||
method: ManagementMethod.ChangeRelayDescription,
|
||||
params: [values.description || ""],
|
||||
})
|
||||
|
||||
if (res.error) {
|
||||
return pushToast({theme: "error", message: res.error})
|
||||
}
|
||||
}
|
||||
|
||||
if (imageFile) {
|
||||
const {error, result} = await uploadFile(imageFile, {maxWidth: 128, maxHeight: 128})
|
||||
|
||||
console.log(imageFile, result)
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
const res = await manageRelay(url, {
|
||||
method: ManagementMethod.ChangeRelayIcon,
|
||||
params: [result.url],
|
||||
})
|
||||
|
||||
if (res.error) {
|
||||
return pushToast({theme: "error", message: res.error})
|
||||
}
|
||||
}
|
||||
|
||||
// Force-reload the relay
|
||||
ifLet(await fetchRelayProfileDirectly(url), relay => {
|
||||
relays.update($relays => uniqBy(prop("url"), append(relay, $relays)))
|
||||
})
|
||||
|
||||
pushToast({message: "Your changes have been saved!"})
|
||||
clearModals()
|
||||
}
|
||||
|
||||
const trySubmit = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await submit()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
let imageFile = $state<File | undefined>()
|
||||
let imagePreview = $state(initialValues.icon)
|
||||
|
||||
const handleImageUpload = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
|
||||
if (file && file.type.startsWith("image/")) {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = e => {
|
||||
imageFile = file
|
||||
imagePreview = e.target?.result as string
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIconSelect = (iconUrl: string) => {
|
||||
imagePreview = iconUrl
|
||||
|
||||
const parts = iconUrl.split(",")
|
||||
const imageData = atob(parts[1])
|
||||
const result = new Uint8Array(imageData.length)
|
||||
|
||||
for (let n = 0; n < imageData.length; n++) {
|
||||
result[n] = imageData.charCodeAt(n)
|
||||
}
|
||||
|
||||
imageFile = new File([result], `icon.svg`, {type: "image/svg+xml"})
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(trySubmit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Edit a Space</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Icon</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
{#if imagePreview}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-75">Selected:</span>
|
||||
<ImageIcon src={imagePreview} alt="Room icon preview" />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-sm opacity-75">No icon selected</span>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
|
||||
<Icon icon={StickerSmileSquare} size={4} />
|
||||
Select
|
||||
</IconPickerButton>
|
||||
<label class="btn btn-neutral btn-sm cursor-pointer">
|
||||
<Icon icon={UploadMinimalistic} size={4} />
|
||||
Upload
|
||||
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Name</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
{#if imagePreview}
|
||||
<ImageIcon src={imagePreview} alt="Room icon preview" />
|
||||
{:else}
|
||||
<Icon icon={SettingsMinimalistic} />
|
||||
{/if}
|
||||
<input bind:value={values.name} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Description</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<input bind:value={values.description} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Save Changes</Spinner>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {sleep, nthEq} from "@welshman/lib"
|
||||
import {sleep} from "@welshman/lib"
|
||||
import {request} from "@welshman/net"
|
||||
import {displayRelayUrl, RELAY_INVITE} from "@welshman/util"
|
||||
import {displayRelayUrl, getTagValue, RELAY_INVITE} from "@welshman/util"
|
||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -13,10 +13,12 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import QRCode from "@app/components/QRCode.svelte"
|
||||
import {clip} from "@app/util/toast"
|
||||
import {PLATFORM_URL} from "@app/core/state"
|
||||
import {PLATFORM_URL, deriveRelayAuthError} from "@app/core/state"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const authError = deriveRelayAuthError(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const copyInvite = () => clip(invite)
|
||||
@@ -38,12 +40,13 @@
|
||||
request({
|
||||
relays: [url],
|
||||
autoClose: true,
|
||||
signal: AbortSignal.timeout(3000),
|
||||
filters: [{kinds: [RELAY_INVITE]}],
|
||||
}),
|
||||
sleep(2000),
|
||||
])
|
||||
|
||||
claim = event?.tags.find(nthEq(0, "claim"))?.[1] || ""
|
||||
claim = getTagValue("claim", event?.tags || []) || ""
|
||||
loading = false
|
||||
})
|
||||
</script>
|
||||
@@ -65,6 +68,8 @@
|
||||
<p class="center">
|
||||
<Spinner {loading}>Requesting an invite link...</Spinner>
|
||||
</p>
|
||||
{:else if $authError}
|
||||
<p class="center">Oops! It looks like you're not a member of this relay.</p>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<QRCode code={invite} />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {tryCatch, fromPairs} from "@welshman/lib"
|
||||
import {isRelayUrl, normalizeRelayUrl} from "@welshman/util"
|
||||
import {Pool, AuthStatus} from "@welshman/net"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {slideAndFade} from "@lib/transition"
|
||||
@@ -19,6 +17,7 @@
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {attemptRelayAccess} from "@app/core/commands"
|
||||
import {parseInviteLink} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
invite: string
|
||||
@@ -57,24 +56,7 @@
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
const inviteData = $derived.by(
|
||||
() =>
|
||||
tryCatch(() => {
|
||||
const {r: relay = "", c: claim = ""} = fromPairs(Array.from(new URL(invite).searchParams))
|
||||
const url = normalizeRelayUrl(relay)
|
||||
|
||||
if (isRelayUrl(url)) {
|
||||
return {url, claim}
|
||||
}
|
||||
}) ||
|
||||
tryCatch(() => {
|
||||
const url = normalizeRelayUrl(invite)
|
||||
|
||||
if (isRelayUrl(url)) {
|
||||
return {url, claim: ""}
|
||||
}
|
||||
}),
|
||||
)
|
||||
const inviteData = $derived(parseInviteLink(invite))
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(join)}>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
await addSpaceMembership(url)
|
||||
|
||||
broadcastUserData([url])
|
||||
goto(makeSpacePath(url), {replaceState: true})
|
||||
relaysMostlyRestricted.update(dissoc(url))
|
||||
goto(makeSpacePath(url), {replaceState: true})
|
||||
pushToast({message: "Welcome to the space!"})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
try {
|
||||
await removeSpaceMembership(url)
|
||||
await removeTrustedRelay(url)
|
||||
goto("/")
|
||||
goto("/home")
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<script module lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
|
||||
export const confirmSpaceVisit = (url: string) => {
|
||||
goto(makeSpacePath(url), {replaceState: true})
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
|
||||
const confirm = () => confirmSpaceVisit(url)
|
||||
</script>
|
||||
|
||||
<Confirm
|
||||
{confirm}
|
||||
message="This space does not appear to limit who can post to it. This can result in a large amount of spam or other objectionable content. Continue?" />
|
||||
@@ -2,7 +2,7 @@
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const h = getTagValue("h", event.tags)
|
||||
const path = makeThreadPath(url, event.id)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const deleteReaction = async (event: TrustedEvent) =>
|
||||
@@ -31,9 +31,9 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if room && showRoom}
|
||||
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<ChannelName {room} {url} />
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, room}: Props = $props()
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (room) {
|
||||
tags.push(["h", room])
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import ThreadActions from "@app/components/ThreadActions.svelte"
|
||||
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import {makeThreadPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -17,10 +17,12 @@
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const title = getTagValue("title", event.tags)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
|
||||
<Link
|
||||
class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
|
||||
href={makeThreadPath(url, event.id)}>
|
||||
{#if title}
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<p class="text-xl">{title}</p>
|
||||
@@ -38,8 +40,8 @@
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by
|
||||
<ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if room}
|
||||
in <ChannelLink {url} {room} />
|
||||
{#if h}
|
||||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<ThreadActions showActivity {url} {event} />
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt col-2 shadow-2xl">
|
||||
<div class="card2 bg-alt col-2 shadow-lg">
|
||||
<p>
|
||||
Failed to publish to {displayRelayUrl(url)}: {message}.
|
||||
</p>
|
||||
|
||||
+32
-77
@@ -74,6 +74,7 @@ import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
||||
import {Router} from "@welshman/router"
|
||||
import {
|
||||
pubkey,
|
||||
sign,
|
||||
signer,
|
||||
session,
|
||||
repository,
|
||||
@@ -84,7 +85,6 @@ import {
|
||||
userRelaySelections,
|
||||
userInboxRelaySelections,
|
||||
nip44EncryptToSelf,
|
||||
loadRelay,
|
||||
dropSession,
|
||||
tagEventForComment,
|
||||
tagEventForQuote,
|
||||
@@ -104,8 +104,10 @@ import {
|
||||
DEFAULT_BLOSSOM_SERVERS,
|
||||
userSpaceUrls,
|
||||
userSettingsValues,
|
||||
getSetting,
|
||||
userInboxRelays,
|
||||
userGroupSelections,
|
||||
shouldIgnoreError,
|
||||
} from "@app/core/state"
|
||||
import {loadAlertStatuses} from "@app/core/requests"
|
||||
import {platform, platformName, getPushInfo} from "@app/util/push"
|
||||
@@ -191,11 +193,11 @@ export const removeSpaceMembership = async (url: string) => {
|
||||
return publishThunk({event, relays})
|
||||
}
|
||||
|
||||
export const addRoomMembership = async (url: string, room: string) => {
|
||||
export const addRoomMembership = async (url: string, h: string) => {
|
||||
const list = get(userGroupSelections) || makeList({kind: ROOMS})
|
||||
const newTags = [
|
||||
["r", url],
|
||||
["group", room, url],
|
||||
["group", h, url],
|
||||
]
|
||||
const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
@@ -203,9 +205,9 @@ export const addRoomMembership = async (url: string, room: string) => {
|
||||
return publishThunk({event, relays})
|
||||
}
|
||||
|
||||
export const removeRoomMembership = async (url: string, room: string) => {
|
||||
export const removeRoomMembership = async (url: string, h: string) => {
|
||||
const list = get(userGroupSelections) || makeList({kind: ROOMS})
|
||||
const pred = (t: string[]) => equals(["group", room, url], t.slice(0, 3))
|
||||
const pred = (t: string[]) => equals(["group", h, url], t.slice(0, 3))
|
||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
|
||||
@@ -250,57 +252,15 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
||||
|
||||
// Relay access
|
||||
|
||||
export const attemptAuth = async (url: string) => {
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||
}
|
||||
|
||||
export const canEnforceNip70 = async (url: string) => {
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||
await socket.auth.attemptAuth(sign)
|
||||
|
||||
return socket.auth.status !== AuthStatus.None
|
||||
}
|
||||
|
||||
export const checkRelayAccess = async (url: string, claim = "") => {
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
await attemptAuth(url)
|
||||
|
||||
const thunk = publishJoinRequest({url, claim})
|
||||
const error = await waitForThunkError(thunk)
|
||||
|
||||
if (error) {
|
||||
const message =
|
||||
socket.auth.details?.replace(/^\w+: /, "") ||
|
||||
error.replace(/^\w+: /, "") ||
|
||||
"join request rejected"
|
||||
|
||||
// If it's a strict NIP 29 relay don't worry about requesting access
|
||||
// TODO: remove this if relay29 ever gets less strict
|
||||
if (message === "missing group (`h`) tag") return
|
||||
|
||||
// Ignore messages about the relay ignoring ours
|
||||
if (error?.startsWith("mute: ")) return
|
||||
|
||||
// Ignore rejected empty claims
|
||||
if (!claim && error?.includes("invite code")) return
|
||||
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
export const checkRelayProfile = async (url: string) => {
|
||||
const relay = await loadRelay(url)
|
||||
|
||||
if (!relay) {
|
||||
return "Sorry, we weren't able to find that relay."
|
||||
}
|
||||
}
|
||||
|
||||
export const checkRelayConnection = async (url: string) => {
|
||||
export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
socket.attemptToOpen()
|
||||
@@ -313,38 +273,29 @@ export const checkRelayConnection = async (url: string) => {
|
||||
if (socket.status !== SocketStatus.Open) {
|
||||
return `Failed to connect`
|
||||
}
|
||||
}
|
||||
|
||||
export const checkRelayAuth = async (url: string) => {
|
||||
const socket = Pool.get().get(url)
|
||||
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
|
||||
|
||||
await attemptAuth(url)
|
||||
await socket.auth.attemptAuth(sign)
|
||||
|
||||
// Only raise an error if it's not a timeout.
|
||||
// If it is, odds are the problem is with our signer, not the relay
|
||||
if (!okStatuses.includes(socket.auth.status)) {
|
||||
if (![AuthStatus.None, AuthStatus.Ok].includes(socket.auth.status)) {
|
||||
if (socket.auth.details) {
|
||||
return `Failed to authenticate (${socket.auth.details})`
|
||||
} else {
|
||||
return `Failed to authenticate (${last(socket.auth.status.split(":"))})`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||
const checks = [
|
||||
() => checkRelayConnection(url),
|
||||
() => checkRelayAccess(url, claim),
|
||||
() => checkRelayAuth(url),
|
||||
]
|
||||
const thunk = publishJoinRequest({url, claim})
|
||||
const error = await waitForThunkError(thunk)
|
||||
|
||||
for (const check of checks) {
|
||||
const error = await check()
|
||||
if (shouldIgnoreError(error)) return
|
||||
|
||||
if (error) {
|
||||
return error
|
||||
}
|
||||
if (claim) {
|
||||
const ignoreClaimError =
|
||||
error.includes("invalid invite code size") || error.includes("failed to validate invite code")
|
||||
|
||||
if (!ignoreClaimError) return error?.replace(/^\w+: /, "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,7 +506,7 @@ export const createAlert = async (params: CreateAlertParams): Promise<CreateAler
|
||||
}
|
||||
|
||||
// If we don't do this we'll get an event rejection
|
||||
await attemptAuth(NOTIFIER_RELAY)
|
||||
await Pool.get().get(NOTIFIER_RELAY).auth.attemptAuth(sign)
|
||||
|
||||
const thunk = await publishAlert(params as AlertParams)
|
||||
const error = await waitForThunkError(thunk)
|
||||
@@ -599,7 +550,7 @@ export const createDmAlert = async () => {
|
||||
// Settings
|
||||
|
||||
export const makeSettings = async (params: Partial<SettingsValues>) => {
|
||||
const json = JSON.stringify({...userSettingsValues.get(), ...params})
|
||||
const json = JSON.stringify({...get(userSettingsValues), ...params})
|
||||
const content = await signer.get().nip44.encrypt(pubkey.get()!, json)
|
||||
const tags = [["d", SETTINGS]]
|
||||
|
||||
@@ -610,10 +561,10 @@ export const publishSettings = async (params: Partial<SettingsValues>) =>
|
||||
publishThunk({event: await makeSettings(params), relays: Router.get().FromUser().getUrls()})
|
||||
|
||||
export const addTrustedRelay = async (url: string) =>
|
||||
publishSettings({trusted_relays: append(url, userSettingsValues.get().trusted_relays)})
|
||||
publishSettings({trusted_relays: append(url, getSetting<string[]>("trusted_relays"))})
|
||||
|
||||
export const removeTrustedRelay = async (url: string) =>
|
||||
publishSettings({trusted_relays: remove(url, userSettingsValues.get().trusted_relays)})
|
||||
publishSettings({trusted_relays: remove(url, getSetting<string[]>("trusted_relays"))})
|
||||
|
||||
// Join request
|
||||
|
||||
@@ -661,7 +612,7 @@ export const payInvoice = async (invoice: string) => {
|
||||
|
||||
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
|
||||
|
||||
export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
|
||||
export const fetchHasBlossomSupport = async (url: string) => {
|
||||
const server = normalizeBlossomUrl(url)
|
||||
const $signer = signer.get() || Nip01Signer.ephemeral()
|
||||
const headers: Record<string, string> = {
|
||||
@@ -682,7 +633,9 @@ export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
export const hasBlossomSupport = simpleCache(([url]: [string]) => fetchHasBlossomSupport(url))
|
||||
|
||||
export type GetBlossomServerOptions = {
|
||||
url?: string
|
||||
@@ -707,6 +660,8 @@ export const getBlossomServer = async (options: GetBlossomServerOptions = {}) =>
|
||||
export type UploadFileOptions = {
|
||||
url?: string
|
||||
encrypt?: boolean
|
||||
maxWidth?: number
|
||||
maxHeight?: number
|
||||
}
|
||||
|
||||
export type UploadFileResult = {
|
||||
@@ -718,8 +673,8 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
||||
try {
|
||||
const {name, type} = file
|
||||
|
||||
if (!type.match("image/(webp|gif)")) {
|
||||
file = await compressFile(file)
|
||||
if (!type.match("image/(webp|gif|svg)")) {
|
||||
file = await compressFile(file, options)
|
||||
}
|
||||
|
||||
const tags: string[][] = []
|
||||
@@ -750,7 +705,7 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
||||
let {uploaded, url, ...task} = parseJson(text) || {}
|
||||
|
||||
if (!uploaded) {
|
||||
return {error: text}
|
||||
return {error: text || `Failed to upload file (HTTP ${res.status})`}
|
||||
}
|
||||
|
||||
// Always append correct file extension if we encrypted the file, or if it's missing
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
sortBy,
|
||||
now,
|
||||
on,
|
||||
isNotNil,
|
||||
isDefined,
|
||||
filterVals,
|
||||
fromPairs,
|
||||
} from "@welshman/lib"
|
||||
@@ -248,13 +248,15 @@ export const makeCalendarFeed = ({
|
||||
// Domain specific
|
||||
|
||||
export const loadAlerts = (pubkey: string) =>
|
||||
load({
|
||||
request({
|
||||
autoClose: true,
|
||||
relays: [NOTIFIER_RELAY],
|
||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID], authors: [pubkey]}],
|
||||
})
|
||||
|
||||
export const loadAlertStatuses = (pubkey: string) =>
|
||||
load({
|
||||
request({
|
||||
autoClose: true,
|
||||
relays: [NOTIFIER_RELAY],
|
||||
filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}],
|
||||
})
|
||||
@@ -277,6 +279,6 @@ export const requestRelayClaim = async (url: string) => {
|
||||
|
||||
export const requestRelayClaims = async (urls: string[]) =>
|
||||
filterVals(
|
||||
isNotNil,
|
||||
isDefined,
|
||||
fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))),
|
||||
)
|
||||
|
||||
+182
-157
@@ -4,6 +4,8 @@ import {get, derived, writable} from "svelte/store"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {
|
||||
on,
|
||||
gt,
|
||||
max,
|
||||
spec,
|
||||
call,
|
||||
first,
|
||||
@@ -17,13 +19,13 @@ import {
|
||||
pushToMapKey,
|
||||
shuffle,
|
||||
parseJson,
|
||||
fromPairs,
|
||||
memoize,
|
||||
addToMapKey,
|
||||
identity,
|
||||
groupBy,
|
||||
always,
|
||||
tryCatch,
|
||||
fromPairs,
|
||||
} from "@welshman/lib"
|
||||
import type {Socket} from "@welshman/net"
|
||||
import {
|
||||
@@ -35,14 +37,7 @@ import {
|
||||
SocketEvent,
|
||||
netContext,
|
||||
} from "@welshman/net"
|
||||
import {
|
||||
collection,
|
||||
custom,
|
||||
throttled,
|
||||
deriveEvents,
|
||||
deriveEventsMapped,
|
||||
withGetter,
|
||||
} from "@welshman/store"
|
||||
import {collection, custom, throttled, deriveEvents, deriveEventsMapped} from "@welshman/store"
|
||||
import {isKindFeed, findFeed} from "@welshman/feeds"
|
||||
import {
|
||||
ALERT_ANDROID,
|
||||
@@ -88,7 +83,6 @@ import {
|
||||
getPubkeyTagValues,
|
||||
getRelaysFromList,
|
||||
getRelayTagValues,
|
||||
getTag,
|
||||
getTagValue,
|
||||
getTagValues,
|
||||
isRelayUrl,
|
||||
@@ -97,8 +91,18 @@ import {
|
||||
readList,
|
||||
RelayMode,
|
||||
verifyEvent,
|
||||
readRoomMeta,
|
||||
makeRoomMeta,
|
||||
ManagementMethod,
|
||||
} from "@welshman/util"
|
||||
import type {
|
||||
TrustedEvent,
|
||||
RelayProfile,
|
||||
PublishedList,
|
||||
PublishedRoomMeta,
|
||||
List,
|
||||
Filter,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, RelayProfile, PublishedList, List, Filter} from "@welshman/util"
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import {routerContext, Router} from "@welshman/router"
|
||||
import {
|
||||
@@ -112,6 +116,7 @@ import {
|
||||
userFollows,
|
||||
ensurePlaintext,
|
||||
thunks,
|
||||
sign,
|
||||
signer,
|
||||
makeOutboxLoader,
|
||||
appContext,
|
||||
@@ -122,6 +127,7 @@ import {
|
||||
deriveRelay,
|
||||
makeUserData,
|
||||
makeUserLoader,
|
||||
manageRelay,
|
||||
} from "@welshman/app"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
|
||||
@@ -232,31 +238,29 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const getUrlsForEvent = withGetter(
|
||||
derived([trackerStore, thunks], ([$tracker, $thunks]) => {
|
||||
const getThunksByEventId = memoize(() => {
|
||||
const thunksByEventId = new Map<string, Thunk[]>()
|
||||
export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thunks]) => {
|
||||
const getThunksByEventId = memoize(() => {
|
||||
const thunksByEventId = new Map<string, Thunk[]>()
|
||||
|
||||
for (const thunk of $thunks) {
|
||||
pushToMapKey(thunksByEventId, thunk.event.id, thunk)
|
||||
}
|
||||
|
||||
return thunksByEventId
|
||||
})
|
||||
|
||||
return (id: string) => {
|
||||
const urls = Array.from($tracker.getRelays(id))
|
||||
|
||||
for (const thunk of getThunksByEventId().get(id) || []) {
|
||||
for (const url of thunk.options.relays) {
|
||||
urls.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
return uniq(urls)
|
||||
for (const thunk of $thunks) {
|
||||
pushToMapKey(thunksByEventId, thunk.event.id, thunk)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return thunksByEventId
|
||||
})
|
||||
|
||||
return (id: string) => {
|
||||
const urls = Array.from($tracker.getRelays(id))
|
||||
|
||||
for (const thunk of getThunksByEventId().get(id) || []) {
|
||||
for (const url of thunk.options.relays) {
|
||||
urls.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
return uniq(urls)
|
||||
}
|
||||
})
|
||||
|
||||
export const getEventsForUrl = (url: string, filters: Filter[]) => {
|
||||
const ids = uniq([
|
||||
@@ -374,15 +378,13 @@ export const userSettings = makeUserData({
|
||||
|
||||
export const loadUserSettings = makeUserLoader(loadSettings)
|
||||
|
||||
export const userSettingsValues = withGetter(
|
||||
derived(userSettings, $s => $s?.values || defaultSettings),
|
||||
)
|
||||
export const userSettingsValues = derived(userSettings, $s => $s?.values || defaultSettings)
|
||||
|
||||
export const getSetting = <T>(key: keyof Settings["values"]) => userSettingsValues.get()[key] as T
|
||||
export const getSetting = <T>(key: keyof Settings["values"]) => get(userSettingsValues)[key] as T
|
||||
|
||||
// Relays sending events with empty signatures that the user has to choose to trust
|
||||
|
||||
export const relaysPendingTrust = withGetter(writable<string[]>([]))
|
||||
export const relaysPendingTrust = writable<string[]>([])
|
||||
|
||||
// Relays that mostly send restricted responses to requests and events
|
||||
|
||||
@@ -407,21 +409,19 @@ export type Alert = {
|
||||
tags: string[][]
|
||||
}
|
||||
|
||||
export const alerts = withGetter(
|
||||
deriveEventsMapped<Alert>(repository, {
|
||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
export const alerts = deriveEventsMapped<Alert>(repository, {
|
||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const getAlertFeed = (alert: Alert) =>
|
||||
tryCatch(() => JSON.parse(getTagValue("feed", alert.tags)!))
|
||||
@@ -441,21 +441,19 @@ export type AlertStatus = {
|
||||
tags: string[][]
|
||||
}
|
||||
|
||||
export const alertStatuses = withGetter(
|
||||
deriveEventsMapped<AlertStatus>(repository, {
|
||||
filters: [{kinds: [ALERT_STATUS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
|
||||
filters: [{kinds: [ALERT_STATUS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const deriveAlertStatus = (address: string) =>
|
||||
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
|
||||
@@ -530,62 +528,53 @@ export const chatSearch = derived(chats, $chats =>
|
||||
}),
|
||||
)
|
||||
|
||||
// Channels
|
||||
// Rooms
|
||||
|
||||
export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]})
|
||||
|
||||
export type Channel = {
|
||||
export type Room = PublishedRoomMeta & {
|
||||
id: string
|
||||
url: string
|
||||
room: string
|
||||
name: string
|
||||
event: TrustedEvent
|
||||
closed: boolean
|
||||
private: boolean
|
||||
picture?: string
|
||||
about?: string
|
||||
}
|
||||
|
||||
export const makeChannelId = (url: string, room: string) => `${url}'${room}`
|
||||
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
|
||||
|
||||
export const splitChannelId = (id: string) => id.split("'")
|
||||
export const splitRoomId = (id: string) => id.split("'")
|
||||
|
||||
export const hasNip29 = (relay?: RelayProfile) =>
|
||||
relay?.supported_nips?.map?.(String)?.includes?.("29")
|
||||
|
||||
export const channels = derived(
|
||||
[deriveEvents(repository, {filters: [{kinds: [ROOM_META, ROOM_DELETE]}]}), getUrlsForEvent],
|
||||
([$events, $getUrlsForEvent]) => {
|
||||
const result = new Map<string, Channel>()
|
||||
export const roomMetas = deriveEventsMapped<PublishedRoomMeta>(repository, {
|
||||
filters: [{kinds: [ROOM_META]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: readRoomMeta,
|
||||
})
|
||||
|
||||
for (const event of sortBy(e => e.created_at, $events)) {
|
||||
for (const url of $getUrlsForEvent(event.id)) {
|
||||
if (event.kind === ROOM_META) {
|
||||
const meta = fromPairs(event.tags)
|
||||
const room = meta.d
|
||||
export const roomDeletes = deriveEvents(repository, {
|
||||
filters: [{kinds: [ROOM_DELETE]}],
|
||||
})
|
||||
|
||||
if (room) {
|
||||
const id = makeChannelId(url, room)
|
||||
export const rooms = derived(
|
||||
[roomMetas, roomDeletes, getUrlsForEvent],
|
||||
([$roomMetas, $roomDeletes, $getUrlsForEvent]) => {
|
||||
const result = new Map<string, Room>()
|
||||
const deletedByH = new Map<string, number>()
|
||||
|
||||
result.set(id, {
|
||||
id,
|
||||
url,
|
||||
room,
|
||||
event,
|
||||
name: meta.name || room,
|
||||
closed: Boolean(getTag("closed", event.tags)),
|
||||
private: Boolean(getTag("private", event.tags)),
|
||||
picture: meta.picture,
|
||||
about: meta.about,
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const event of $roomDeletes) {
|
||||
for (const h of getTagValues("h", event.tags)) {
|
||||
deletedByH.set(h, max([deletedByH.get(h), event.created_at]))
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === ROOM_DELETE) {
|
||||
for (const room of getTagValues("h", event.tags)) {
|
||||
result.delete(makeChannelId(url, room))
|
||||
}
|
||||
}
|
||||
for (const meta of $roomMetas) {
|
||||
if (gt(deletedByH.get(meta.h), meta.event.created_at)) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const url of $getUrlsForEvent(meta.event.id)) {
|
||||
const id = makeRoomId(url, meta.h)
|
||||
|
||||
result.set(id, {...meta, url, id})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,35 +582,33 @@ export const channels = derived(
|
||||
},
|
||||
)
|
||||
|
||||
export const channelsByUrl = derived(channels, $channels => groupBy(c => c.url, $channels))
|
||||
export const roomsByUrl = derived(rooms, $rooms => groupBy(c => c.url, $rooms))
|
||||
|
||||
export const {
|
||||
indexStore: channelsById,
|
||||
deriveItem: _deriveChannel,
|
||||
loadItem: _loadChannel,
|
||||
indexStore: roomsById,
|
||||
deriveItem: _deriveRoom,
|
||||
loadItem: _loadRoom,
|
||||
} = collection({
|
||||
name: "channels",
|
||||
store: channels,
|
||||
getKey: channel => channel.id,
|
||||
name: "rooms",
|
||||
store: rooms,
|
||||
getKey: room => room.id,
|
||||
load: async (id: string) => {
|
||||
const [url, room] = splitChannelId(id)
|
||||
const [url, h] = splitRoomId(id)
|
||||
|
||||
await load({
|
||||
relays: [url],
|
||||
filters: [{kinds: [ROOM_META], "#d": [room]}],
|
||||
filters: [{kinds: [ROOM_META], "#d": [h]}],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const deriveChannel = (url: string, room: string) => _deriveChannel(makeChannelId(url, room))
|
||||
export const deriveRoom = (url: string, h: string) =>
|
||||
derived(_deriveRoom(makeRoomId(url, h)), $meta => $meta || makeRoomMeta({h}))
|
||||
|
||||
export const loadChannel = (url: string, room: string) => _loadChannel(makeChannelId(url, room))
|
||||
export const displayRoom = (url: string, h: string) =>
|
||||
roomsById.get().get(makeRoomId(url, h))?.name || h
|
||||
|
||||
export const displayChannel = (url: string, room: string) =>
|
||||
channelsById.get().get(makeChannelId(url, room))?.name || room
|
||||
|
||||
export const roomComparator = (url: string) => (room: string) =>
|
||||
displayChannel(url, room).toLowerCase()
|
||||
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
|
||||
|
||||
// User space/room selections
|
||||
|
||||
@@ -685,9 +672,9 @@ export const getSpaceRoomsFromGroupSelections = (
|
||||
) => {
|
||||
const rooms: string[] = []
|
||||
|
||||
for (const [_, room, relay] of getGroupTags(getListTags($groupSelections))) {
|
||||
for (const [_, h, relay] of getGroupTags(getListTags($groupSelections))) {
|
||||
if (url === relay) {
|
||||
rooms.push(room)
|
||||
rooms.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,12 +691,12 @@ export const loadUserGroupSelections = makeUserLoader(loadGroupSelections)
|
||||
export const userSpaceUrls = derived(userGroupSelections, getSpaceUrlsFromGroupSelections)
|
||||
|
||||
export const deriveUserRooms = (url: string) =>
|
||||
derived([userGroupSelections, channelsById], ([$userGroupSelections, $channelsById]) => {
|
||||
derived([userGroupSelections, roomsById], ([$userGroupSelections, $roomsById]) => {
|
||||
const rooms: string[] = []
|
||||
|
||||
for (const room of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
||||
if ($channelsById.has(makeChannelId(url, room))) {
|
||||
rooms.push(room)
|
||||
for (const h of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
||||
if ($roomsById.has(makeRoomId(url, h))) {
|
||||
rooms.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,12 +704,12 @@ export const deriveUserRooms = (url: string) =>
|
||||
})
|
||||
|
||||
export const deriveOtherRooms = (url: string) =>
|
||||
derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) => {
|
||||
derived([deriveUserRooms(url), roomsByUrl], ([$userRooms, $roomsByUrl]) => {
|
||||
const rooms: string[] = []
|
||||
|
||||
for (const {room} of $channelsByUrl.get(url) || []) {
|
||||
if (!$userRooms.includes(room)) {
|
||||
rooms.push(room)
|
||||
for (const {h} of $roomsByUrl.get(url) || []) {
|
||||
if (!$userRooms.includes(h)) {
|
||||
rooms.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,7 +730,7 @@ export const deriveSpaceMembers = (url: string) =>
|
||||
return getTagValues("member", membersEvent.tags)
|
||||
}
|
||||
|
||||
const members = new Set()
|
||||
const members = new Set<string>()
|
||||
|
||||
for (const event of sortBy(e => e.created_at, $events)) {
|
||||
const pubkeys = getPubkeyTagValues(event.tags)
|
||||
@@ -765,11 +752,11 @@ export const deriveSpaceMembers = (url: string) =>
|
||||
},
|
||||
)
|
||||
|
||||
export const deriveRoomMembers = (url: string, room: string) =>
|
||||
export const deriveRoomMembers = (url: string, h: string) =>
|
||||
derived(
|
||||
deriveEventsForUrl(url, [
|
||||
{kinds: [ROOM_MEMBERS], "#d": [room]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]},
|
||||
{kinds: [ROOM_MEMBERS], "#d": [h]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
|
||||
]),
|
||||
$events => {
|
||||
const membersEvent = $events.find(spec({kind: ROOM_MEMBERS}))
|
||||
@@ -778,7 +765,7 @@ export const deriveRoomMembers = (url: string, room: string) =>
|
||||
return getPubkeyTagValues(membersEvent.tags)
|
||||
}
|
||||
|
||||
const members = new Set()
|
||||
const members = new Set<string>()
|
||||
|
||||
for (const event of sortBy(e => -e.created_at, $events)) {
|
||||
const pubkeys = getPubkeyTagValues(event.tags)
|
||||
@@ -800,8 +787,8 @@ export const deriveRoomMembers = (url: string, room: string) =>
|
||||
},
|
||||
)
|
||||
|
||||
export const deriveRoomAdmins = (url: string, room: string) =>
|
||||
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [room]}]), $events => {
|
||||
export const deriveRoomAdmins = (url: string, h: string) =>
|
||||
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [h]}]), $events => {
|
||||
const adminsEvent = first($events)
|
||||
|
||||
if (adminsEvent) {
|
||||
@@ -819,15 +806,26 @@ export enum MembershipStatus {
|
||||
Granted,
|
||||
}
|
||||
|
||||
export const deriveUserIsSpaceAdmin = (url: string) => {
|
||||
const store = writable(false)
|
||||
|
||||
manageRelay(url, {method: ManagementMethod.SupportedMethods, params: []}).then(res =>
|
||||
store.set(Boolean(res.result?.length)),
|
||||
)
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export const deriveUserSpaceMembershipStatus = (url: string) =>
|
||||
derived(
|
||||
[
|
||||
pubkey,
|
||||
deriveSpaceMembers(url),
|
||||
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
|
||||
deriveUserIsSpaceAdmin(url),
|
||||
],
|
||||
([$pubkey, $members, $events]) => {
|
||||
const isMember = $members.includes($pubkey)
|
||||
([$pubkey, $members, $events, $isAdmin]) => {
|
||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||
|
||||
for (const event of $events) {
|
||||
if (event.pubkey !== $pubkey) {
|
||||
@@ -847,15 +845,22 @@ export const deriveUserSpaceMembershipStatus = (url: string) =>
|
||||
},
|
||||
)
|
||||
|
||||
export const deriveUserRoomMembershipStatus = (url: string, room: string) =>
|
||||
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
|
||||
derived(
|
||||
[pubkey, deriveRoomAdmins(url, h), deriveUserIsSpaceAdmin(url)],
|
||||
([$pubkey, $admins, $isSpaceAdmin]) => $isSpaceAdmin || $admins.includes($pubkey!),
|
||||
)
|
||||
|
||||
export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
|
||||
derived(
|
||||
[
|
||||
pubkey,
|
||||
deriveRoomMembers(url, room),
|
||||
deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [room]}]),
|
||||
deriveRoomMembers(url, h),
|
||||
deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]),
|
||||
deriveUserIsRoomAdmin(url, h),
|
||||
],
|
||||
([$pubkey, $members, $events]) => {
|
||||
const isMember = $members.includes($pubkey)
|
||||
([$pubkey, $members, $events, $isAdmin]) => {
|
||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||
|
||||
for (const event of $events) {
|
||||
if (event.pubkey !== $pubkey) {
|
||||
@@ -885,9 +890,6 @@ export const deriveUserCanCreateRoom = (url: string) =>
|
||||
},
|
||||
)
|
||||
|
||||
export const deriveUserIsRoomAdmin = (url: string, room: string) =>
|
||||
derived([pubkey, deriveRoomAdmins(url, room)], ([$pubkey, $admins]) => $admins.includes($pubkey!))
|
||||
|
||||
// Other utils
|
||||
|
||||
export const encodeRelay = (url: string) =>
|
||||
@@ -977,13 +979,19 @@ export const deriveTimeout = (timeout: number) => {
|
||||
return derived(store, identity)
|
||||
}
|
||||
|
||||
export const shouldIgnoreError = (error: string) => {
|
||||
const isIgnored = error.startsWith("mute: ")
|
||||
const isAborted = error.includes("Signing was aborted")
|
||||
const isStrictNip29Relay = error.includes("missing group (`h`) tag")
|
||||
|
||||
return isIgnored || isAborted || isStrictNip29Relay
|
||||
}
|
||||
|
||||
export const deriveRelayAuthError = (url: string, claim = "") => {
|
||||
const $signer = signer.get()
|
||||
const socket = Pool.get().get(url)
|
||||
const stripPrefix = (m: string) => m.replace(/^\w+: /, "")
|
||||
|
||||
// Kick off the auth process
|
||||
socket.auth.attemptAuth($signer.sign)
|
||||
Pool.get().get(url).auth.attemptAuth(sign)
|
||||
|
||||
// Attempt to join the relay
|
||||
const thunk = publishThunk({
|
||||
@@ -992,8 +1000,8 @@ export const deriveRelayAuthError = (url: string, claim = "") => {
|
||||
})
|
||||
|
||||
return derived(
|
||||
[relaysMostlyRestricted, deriveSocket(url)],
|
||||
([$relaysMostlyRestricted, $socket]) => {
|
||||
[thunk, relaysMostlyRestricted, deriveSocket(url)],
|
||||
([$thunk, $relaysMostlyRestricted, $socket]) => {
|
||||
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
|
||||
return stripPrefix($socket.auth.details)
|
||||
}
|
||||
@@ -1002,17 +1010,34 @@ export const deriveRelayAuthError = (url: string, claim = "") => {
|
||||
return stripPrefix($relaysMostlyRestricted[url])
|
||||
}
|
||||
|
||||
const error = getThunkError(thunk)
|
||||
const error = getThunkError($thunk)
|
||||
|
||||
if (error) {
|
||||
const isIgnored = error.startsWith("mute: ")
|
||||
const isEmptyInvite = !claim && error.includes("invite code")
|
||||
const isStrictNip29Relay = error.includes("missing group (`h`) tag")
|
||||
|
||||
if (!isStrictNip29Relay && !isIgnored && !isEmptyInvite && !isStrictNip29Relay) {
|
||||
if (!shouldIgnoreError(error) && !isEmptyInvite) {
|
||||
return stripPrefix(error) || "join request rejected"
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export type InviteData = {url: string; claim: string}
|
||||
|
||||
export const parseInviteLink = (invite: string): InviteData | undefined =>
|
||||
tryCatch(() => {
|
||||
const {r: relay = "", c: claim = ""} = fromPairs(Array.from(new URL(invite).searchParams))
|
||||
const url = normalizeRelayUrl(relay)
|
||||
|
||||
if (isRelayUrl(url)) {
|
||||
return {url, claim}
|
||||
}
|
||||
}) ||
|
||||
tryCatch(() => {
|
||||
const url = normalizeRelayUrl(invite)
|
||||
|
||||
if (isRelayUrl(url)) {
|
||||
return {url, claim: ""}
|
||||
}
|
||||
})
|
||||
|
||||
+13
-74
@@ -6,7 +6,6 @@ import {
|
||||
getListTags,
|
||||
getRelayTagValues,
|
||||
WRAP,
|
||||
MESSAGE,
|
||||
ROOM_META,
|
||||
ROOM_DELETE,
|
||||
ROOM_ADMINS,
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
RELAY_ADD_MEMBER,
|
||||
RELAY_REMOVE_MEMBER,
|
||||
isSignedEvent,
|
||||
unionFilters,
|
||||
} from "@welshman/util"
|
||||
import type {Filter, TrustedEvent} from "@welshman/util"
|
||||
import {request, load, pull} from "@welshman/net"
|
||||
@@ -36,9 +36,9 @@ import {
|
||||
repository,
|
||||
shouldUnwrap,
|
||||
hasNegentropy,
|
||||
relaysByUrl,
|
||||
} from "@welshman/app"
|
||||
import {
|
||||
REACTION_KINDS,
|
||||
MESSAGE_KINDS,
|
||||
CONTENT_KINDS,
|
||||
INDEXER_RELAYS,
|
||||
@@ -49,7 +49,6 @@ import {
|
||||
bootstrapPubkeys,
|
||||
decodeRelay,
|
||||
getUrlsForEvent,
|
||||
hasNip29,
|
||||
getSpaceUrlsFromGroupSelections,
|
||||
getSpaceRoomsFromGroupSelections,
|
||||
makeCommentFilter,
|
||||
@@ -96,7 +95,7 @@ const pullAndListen = ({relays, filters, signal}: PullOpts) => {
|
||||
request({
|
||||
relays,
|
||||
signal,
|
||||
filters: filters.map(assoc("limit", 0)),
|
||||
filters: unionFilters(filters).map(assoc("limit", 0)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,7 +149,7 @@ const syncUserSpaceMembership = (url: string) => {
|
||||
return () => controller.abort()
|
||||
}
|
||||
|
||||
const syncUserRoomMembership = (url: string, room: string) => {
|
||||
const syncUserRoomMembership = (url: string, h: string) => {
|
||||
const $pubkey = pubkey.get()
|
||||
const controller = new AbortController()
|
||||
|
||||
@@ -162,7 +161,7 @@ const syncUserRoomMembership = (url: string, room: string) => {
|
||||
{
|
||||
kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
|
||||
"#p": [$pubkey],
|
||||
"#h": [room],
|
||||
"#h": [h],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -187,11 +186,11 @@ const syncUserData = () => {
|
||||
|
||||
keys.add(url)
|
||||
|
||||
for (const room of getSpaceRoomsFromGroupSelections(url, $l)) {
|
||||
const key = `${url}'${room}`
|
||||
for (const h of getSpaceRoomsFromGroupSelections(url, $l)) {
|
||||
const key = `${url}'${h}`
|
||||
|
||||
if (!unsubscribersByKey.has(key)) {
|
||||
unsubscribersByKey.set(key, syncUserRoomMembership(url, room))
|
||||
unsubscribersByKey.set(key, syncUserRoomMembership(url, h))
|
||||
}
|
||||
|
||||
keys.add(key)
|
||||
@@ -264,10 +263,13 @@ const syncSpace = (url: string) => {
|
||||
signal: controller.signal,
|
||||
filters: [
|
||||
{kinds: [RELAY_MEMBERS]},
|
||||
{kinds: [ROOM_META]},
|
||||
{kinds: [ROOM_META, ROOM_DELETE]},
|
||||
{kinds: [ROOM_ADMINS, ROOM_MEMBERS]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]},
|
||||
{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]},
|
||||
...MESSAGE_KINDS.map(kind => ({kinds: [kind]})),
|
||||
makeCommentFilter(CONTENT_KINDS),
|
||||
{kinds: REACTION_KINDS, limit: 0},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -326,69 +328,6 @@ const syncSpaces = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Chat
|
||||
|
||||
const syncRoomChat = (url: string, room: string) => {
|
||||
const controller = new AbortController()
|
||||
|
||||
pullAndListen({
|
||||
relays: [url],
|
||||
signal: controller.signal,
|
||||
filters: [
|
||||
{kinds: [ROOM_ADMINS, ROOM_MEMBERS], "#d": [room]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]},
|
||||
{kinds: [ROOM_DELETE], "#h": [room]},
|
||||
{kinds: [MESSAGE], "#h": [room]},
|
||||
],
|
||||
})
|
||||
|
||||
return () => controller.abort()
|
||||
}
|
||||
|
||||
const syncRooms = () => {
|
||||
const unsubscribersByKey = new Map<string, Unsubscriber>()
|
||||
|
||||
const unsubscribeSpaceUrls = derived([userGroupSelections, relaysByUrl], identity).subscribe(
|
||||
([$l, $relaysByUrl]) => {
|
||||
const keys = new Set<string>()
|
||||
const newUnsubscribersByKey = new Map<string, Unsubscriber>()
|
||||
|
||||
// Add new subscriptions, depending on whether nip 29 is supported
|
||||
for (const url of getRelayTagValues(getListTags($l))) {
|
||||
if (hasNip29($relaysByUrl.get(url))) {
|
||||
for (const room of getSpaceRoomsFromGroupSelections(url, $l)) {
|
||||
const id = `${url}'${room}`
|
||||
|
||||
if (!unsubscribersByKey.has(id)) {
|
||||
newUnsubscribersByKey.set(url, syncRoomChat(url, room))
|
||||
}
|
||||
|
||||
keys.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop syncing removed selections
|
||||
for (const [key, unsubscribe] of unsubscribersByKey.entries()) {
|
||||
if (!keys.has(key)) {
|
||||
unsubscribersByKey.delete(key)
|
||||
unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
// Start syncing newly added spaces
|
||||
for (const [key, unsubscriber] of newUnsubscribersByKey.entries()) {
|
||||
unsubscribersByKey.set(key, unsubscriber)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return () => {
|
||||
Array.from(unsubscribersByKey.values()).forEach(call)
|
||||
unsubscribeSpaceUrls()
|
||||
}
|
||||
}
|
||||
|
||||
// DMs
|
||||
|
||||
const syncDMRelay = (url: string, pubkey: string) => {
|
||||
@@ -478,7 +417,7 @@ const syncDMs = () => {
|
||||
// Merge all synchronization functions
|
||||
|
||||
export const syncApplicationData = () => {
|
||||
const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncRooms(), syncDMs()]
|
||||
const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncDMs()]
|
||||
|
||||
return () => unsubscribers.forEach(call)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import {deriveProfileDisplay} from "@welshman/app"
|
||||
|
||||
export const makeMentionNodeView =
|
||||
(url?: string) =>
|
||||
({node}: NodeViewProps) => {
|
||||
const dom = document.createElement("span")
|
||||
const display = deriveProfileDisplay(node.attrs.pubkey, removeNil([url]))
|
||||
const display = deriveProfileDisplay(node.attrs.pubkey, removeUndefined([url]))
|
||||
|
||||
dom.classList.add("tiptap-object")
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import {displayPubkey} from "@welshman/util"
|
||||
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
|
||||
import WotScore from "@app/components/WotScore.svelte"
|
||||
@@ -13,7 +13,7 @@
|
||||
const {value, url}: Props = $props()
|
||||
|
||||
const pubkey = value
|
||||
const profileDisplay = deriveProfileDisplay(pubkey, removeNil([url]))
|
||||
const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
|
||||
const handle = deriveHandleForPubkey(pubkey)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {page} from "$app/stores"
|
||||
|
||||
export type ModalOptions = {
|
||||
drawer?: boolean
|
||||
noEscape?: boolean
|
||||
fullscreen?: boolean
|
||||
replaceState?: boolean
|
||||
path?: string
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import {writable} from "svelte/store"
|
||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
||||
import {
|
||||
NIP46_PERMS,
|
||||
PLATFORM_URL,
|
||||
PLATFORM_NAME,
|
||||
PLATFORM_LOGO,
|
||||
SIGNER_RELAYS,
|
||||
} from "@app/core/state"
|
||||
import {PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
export class Nip46Controller {
|
||||
@@ -25,7 +19,6 @@ export class Nip46Controller {
|
||||
|
||||
async start() {
|
||||
const url = await this.broker.makeNostrconnectUrl({
|
||||
perms: NIP46_PERMS,
|
||||
url: PLATFORM_URL,
|
||||
name: PLATFORM_NAME,
|
||||
image: PLATFORM_LOGO,
|
||||
|
||||
@@ -171,11 +171,9 @@ export const notifications = derived(
|
||||
}
|
||||
|
||||
if (hasNip29($relaysByUrl.get(url))) {
|
||||
for (const room of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
||||
const roomPath = makeRoomPath(url, room)
|
||||
const latestEvent = allMessages.find(
|
||||
e => $getUrlsForEvent(e.id).includes(url) && e.tags.find(spec(["h", room])),
|
||||
)
|
||||
for (const h of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
||||
const roomPath = makeRoomPath(url, h)
|
||||
const latestEvent = messages.find(e => e.tags.some(spec(["h", h])))
|
||||
|
||||
if (hasNotification(roomPath, latestEvent)) {
|
||||
paths.add(spacePathMobile)
|
||||
|
||||
+39
-24
@@ -1,5 +1,4 @@
|
||||
import {on, call, dissoc, assoc, uniq} from "@welshman/lib"
|
||||
import type {StampedEvent} from "@welshman/util"
|
||||
import {on, always, call, dissoc, assoc, uniq} from "@welshman/lib"
|
||||
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
|
||||
import {
|
||||
makeSocketPolicyAuth,
|
||||
@@ -7,11 +6,14 @@ import {
|
||||
isRelayEvent,
|
||||
isRelayOk,
|
||||
isRelayClosed,
|
||||
isRelayNegErr,
|
||||
isClientReq,
|
||||
isClientEvent,
|
||||
isClientClose,
|
||||
isClientNegOpen,
|
||||
isClientNegClose,
|
||||
} from "@welshman/net"
|
||||
import {signer} from "@welshman/app"
|
||||
import {sign} from "@welshman/app"
|
||||
import {
|
||||
userSettingsValues,
|
||||
getSetting,
|
||||
@@ -19,10 +21,7 @@ import {
|
||||
relaysMostlyRestricted,
|
||||
} from "@app/core/state"
|
||||
|
||||
export const authPolicy = makeSocketPolicyAuth({
|
||||
sign: (event: StampedEvent) => signer.get()?.sign(event),
|
||||
shouldAuth: (socket: Socket) => true,
|
||||
})
|
||||
export const authPolicy = makeSocketPolicyAuth({sign, shouldAuth: always(true)})
|
||||
|
||||
export const trustPolicy = (socket: Socket) => {
|
||||
const buffer: RelayMessage[] = []
|
||||
@@ -59,14 +58,18 @@ export const trustPolicy = (socket: Socket) => {
|
||||
export const mostlyRestrictedPolicy = (socket: Socket) => {
|
||||
let total = 0
|
||||
let restricted = 0
|
||||
let error = ""
|
||||
|
||||
const pending = new Set<string>()
|
||||
|
||||
const updateStatus = () =>
|
||||
relaysMostlyRestricted.update(
|
||||
restricted > total / 2 ? assoc(socket.url, error) : dissoc(socket.url),
|
||||
)
|
||||
const updateStatus = (error?: string) => {
|
||||
if (restricted > total / 2) {
|
||||
if (error) {
|
||||
return relaysMostlyRestricted.update(assoc(socket.url, error))
|
||||
}
|
||||
} else {
|
||||
relaysMostlyRestricted.update(dissoc(socket.url))
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribers = [
|
||||
on(socket, SocketEvent.Receive, (message: RelayMessage) => {
|
||||
@@ -76,33 +79,45 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id)
|
||||
|
||||
if (!ok && details.startsWith("restricted: ")) {
|
||||
restricted++
|
||||
error = details
|
||||
updateStatus()
|
||||
if (!ok) {
|
||||
if (details.startsWith("auth-required: ")) {
|
||||
total--
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
if (details.startsWith("restricted: ")) {
|
||||
restricted++
|
||||
updateStatus(details)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isRelayClosed(message)) {
|
||||
if (isRelayClosed(message) || isRelayNegErr(message)) {
|
||||
const [_, id, details = ""] = message
|
||||
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id)
|
||||
|
||||
if (details.startsWith("auth-required: ")) {
|
||||
total--
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
if (details.startsWith("restricted: ")) {
|
||||
restricted++
|
||||
error = details
|
||||
updateStatus()
|
||||
updateStatus(details)
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
on(socket, SocketEvent.Send, (message: ClientMessage) => {
|
||||
if (isClientReq(message)) {
|
||||
total++
|
||||
pending.add(message[1])
|
||||
updateStatus()
|
||||
if (isClientReq(message) || isClientNegOpen(message)) {
|
||||
if (!pending.has(message[1])) {
|
||||
total++
|
||||
pending.add(message[1])
|
||||
updateStatus()
|
||||
}
|
||||
}
|
||||
|
||||
if (isClientEvent(message)) {
|
||||
@@ -111,7 +126,7 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
if (isClientClose(message)) {
|
||||
if (isClientClose(message) || isClientNegClose(message)) {
|
||||
pending.delete(message[1])
|
||||
}
|
||||
}),
|
||||
|
||||
+19
-14
@@ -4,7 +4,7 @@ import * as nip19 from "nostr-tools/nip19"
|
||||
import {goto} from "$app/navigation"
|
||||
import {nthEq, sleep} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {tracker, relaysByUrl} from "@welshman/app"
|
||||
import {tracker, loadRelay} from "@welshman/app"
|
||||
import {scrollToEvent} from "@lib/html"
|
||||
import {identity} from "@welshman/lib"
|
||||
import {
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
hasNip29,
|
||||
ROOM,
|
||||
} from "@app/core/state"
|
||||
import {lastPageBySpaceUrl} from "@app/util/history"
|
||||
|
||||
export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) => {
|
||||
let path = `/spaces/${encodeRelay(url)}`
|
||||
@@ -37,22 +38,26 @@ export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) =>
|
||||
.filter(identity)
|
||||
.map(s => encodeURIComponent(s as string))
|
||||
.join("/")
|
||||
} else {
|
||||
const relay = relaysByUrl.get().get(url)
|
||||
|
||||
if (hasNip29(relay)) {
|
||||
path += "/recent"
|
||||
} else {
|
||||
path += "/chat"
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
export const goToSpace = async (url: string) => {
|
||||
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
|
||||
|
||||
if (prevPath) {
|
||||
goto(prevPath)
|
||||
} else if (hasNip29(await loadRelay(url))) {
|
||||
goto(makeSpacePath(url, "recent"))
|
||||
} else {
|
||||
goto(makeSpacePath(url, "chat"))
|
||||
}
|
||||
}
|
||||
|
||||
export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}`
|
||||
|
||||
export const makeRoomPath = (url: string, room: string) => `/spaces/${encodeRelay(url)}/${room}`
|
||||
export const makeRoomPath = (url: string, h: string) => `/spaces/${encodeRelay(url)}/${h}`
|
||||
|
||||
export const makeSpaceChatPath = (url: string) => makeRoomPath(url, "chat")
|
||||
|
||||
@@ -103,7 +108,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||
return makeChatPath([event.pubkey, ...getPubkeyTagValues(event.tags)])
|
||||
}
|
||||
|
||||
const room = getTagValue(ROOM, event.tags)
|
||||
const h = getTagValue(ROOM, event.tags)
|
||||
|
||||
if (urls.length > 0) {
|
||||
const url = urls[0]
|
||||
@@ -121,7 +126,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||
}
|
||||
|
||||
if (event.kind === MESSAGE) {
|
||||
return room ? makeRoomPath(url, room) : makeSpacePath(url, "chat")
|
||||
return h ? makeRoomPath(url, h) : makeSpacePath(url, "chat")
|
||||
}
|
||||
|
||||
const kind = event.tags.find(nthEq(0, "K"))?.[1]
|
||||
@@ -141,7 +146,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||
}
|
||||
|
||||
if (parseInt(kind) === MESSAGE) {
|
||||
return room ? makeRoomPath(url, room) : makeSpacePath(url, "chat")
|
||||
return h ? makeRoomPath(url, h) : makeSpacePath(url, "chat")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,7 +154,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
|
||||
}
|
||||
|
||||
export const getChannelItemPath = (url: string, event: TrustedEvent) => {
|
||||
export const getRoomItemPath = (url: string, event: TrustedEvent) => {
|
||||
switch (event.kind) {
|
||||
case THREAD:
|
||||
return makeThreadPath(url, event.id)
|
||||
|
||||
+14
-5
@@ -25,7 +25,9 @@ import {
|
||||
ROOM_ADD_MEMBER,
|
||||
ROOM_CREATE_PERMISSION,
|
||||
ROOM_MEMBERS,
|
||||
ROOM_ADMINS,
|
||||
ROOM_META,
|
||||
ROOM_DELETE,
|
||||
ROOM_REMOVE_MEMBER,
|
||||
ROOMS,
|
||||
THREAD,
|
||||
@@ -48,6 +50,7 @@ import {
|
||||
wrapManager,
|
||||
} from "@welshman/app"
|
||||
import {Collection} from "@lib/storage"
|
||||
import {isMobile} from "@lib/html"
|
||||
|
||||
const syncEvents = async () => {
|
||||
const collection = new Collection<TrustedEvent>({table: "events", getId: prop("id")})
|
||||
@@ -75,6 +78,8 @@ const syncEvents = async () => {
|
||||
const spaceKinds = [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE]
|
||||
const roomKinds = [
|
||||
ROOM_META,
|
||||
ROOM_DELETE,
|
||||
ROOM_ADMINS,
|
||||
ROOM_MEMBERS,
|
||||
ROOM_ADD_MEMBER,
|
||||
ROOM_REMOVE_MEMBER,
|
||||
@@ -87,7 +92,7 @@ const syncEvents = async () => {
|
||||
if (alertKinds.includes(event.kind)) return 8
|
||||
if (spaceKinds.includes(event.kind)) return 7
|
||||
if (roomKinds.includes(event.kind)) return 6
|
||||
if (contentKinds.includes(event.kind)) return 5
|
||||
if (!isMobile && contentKinds.includes(event.kind)) return 5
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -236,17 +241,21 @@ const syncWrapManager = async () => {
|
||||
}
|
||||
|
||||
export const syncDataStores = async () => {
|
||||
const unsubscribers = await Promise.all([
|
||||
const promises = [
|
||||
syncEvents(),
|
||||
syncTracker(),
|
||||
syncRelays(),
|
||||
syncRelayStats(),
|
||||
syncHandles(),
|
||||
syncZappers(),
|
||||
syncFreshness(),
|
||||
syncPlaintext(),
|
||||
syncWrapManager(),
|
||||
])
|
||||
]
|
||||
|
||||
if (!isMobile) {
|
||||
promises.push(syncFreshness(), syncRelayStats())
|
||||
}
|
||||
|
||||
const unsubscribers = await Promise.all(promises)
|
||||
|
||||
return () => unsubscribers.forEach(call)
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
|
||||
const {src = "", size = 7, icon = UserRounded, style = "", ...restProps} = $props()
|
||||
|
||||
let element: HTMLElement
|
||||
|
||||
const rem = $derived(size * 4)
|
||||
|
||||
onMount(() => {
|
||||
if (src) {
|
||||
const image = new Image()
|
||||
|
||||
image.addEventListener("error", () => {
|
||||
element?.querySelector(".hidden")?.classList.remove("hidden")
|
||||
})
|
||||
|
||||
image.src = src
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={element}
|
||||
class="{restProps.class} relative !flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-cover bg-center"
|
||||
style="width: {rem}px; height: {rem}px; min-width: {rem}px; background-image: url({src}); {style}">
|
||||
<Icon {icon} class={src ? "hidden" : ""} size={Math.round(size * 0.8)} />
|
||||
</div>
|
||||
@@ -12,8 +12,8 @@
|
||||
</script>
|
||||
|
||||
<div class="btn flex h-[unset] w-full flex-nowrap py-4 text-left {props.class}">
|
||||
<div class="flex flex-grow flex-row items-start gap-1 sm:pl-2">
|
||||
<div class="flex h-14 w-12 flex-shrink-0 items-center">
|
||||
<div class="flex flex-grow flex-row items-start gap-4">
|
||||
<div class="flex h-14 w-12 flex-shrink-0 items-center justify-center">
|
||||
{@render props.icon?.()}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {noop} from "@welshman/lib"
|
||||
import {fade, fly} from "@lib/transition"
|
||||
|
||||
@@ -12,7 +13,11 @@
|
||||
|
||||
const extraClass = $derived(
|
||||
!fullscreen &&
|
||||
"card2 bg-alt max-h-[90vh] w-[90vw] overflow-auto text-base-content sm:w-[520px] shadow-xl",
|
||||
cx(
|
||||
"bg-alt text-base-content overflow-auto text-base-content shadow-md",
|
||||
"px-4 py-6 bottom-0 left-0 right-0 top-20 rounded-t-box absolute",
|
||||
"sm:p-6 sm:max-h-[90vh] sm:w-[520px] sm:rounded-box sm:relative sm:top-0",
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -23,7 +28,7 @@
|
||||
transition:fade={{duration: 300}}
|
||||
onclick={onClose}>
|
||||
</button>
|
||||
<div class="scroll-container relative {extraClass}" transition:fly={{duration: 300}}>
|
||||
<div class="scroll-container {extraClass}" transition:fly={{duration: 300}}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
{@render props.input?.()}
|
||||
</div>
|
||||
<p class="flex-end text-sm md:col-span-3">
|
||||
<p class="flex-end text-sm opacity-70 md:col-span-3">
|
||||
{#if props.info}
|
||||
{@render props.info?.()}
|
||||
{/if}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user