Compare commits

...

46 Commits

Author SHA1 Message Date
Jon Staab 57447e5bf4 Bump version 2025-11-11 14:09:05 -08:00
Jon Staab 8e411daaef Refactor avatar components, add space edit form 2025-11-11 13:50:45 -08:00
Jon Staab 183aebf841 Improve room syncing 2025-11-10 16:19:50 -08:00
Jon Staab e3e500ccc2 Return better blossom errors 2025-11-10 16:02:02 -08:00
Jon Staab e7a2535ece Fix access restricted after successful invite code 2025-11-10 15:24:11 -08:00
Jon Staab 761e369313 Add room detail, assume admins are members 2025-11-10 14:59:15 -08:00
Jon Staab 5248275d73 Fix nav index 2025-11-10 13:20:42 -08:00
Jon Staab cb033279dd Fix link 2025-11-06 11:25:07 -08:00
Jon Staab 41d50d8c28 Add room policy indicator 2025-11-05 16:59:17 -08:00
Jon Staab a52c2b4c3c Lighten up shadows 2025-11-05 15:32:55 -08:00
Jon Staab b5917cb184 Show loading on spaces menu 2025-11-05 15:24:46 -08:00
Jon Staab 57348472f8 Always join spaces when visiting them 2025-11-05 15:09:23 -08:00
Jon Staab 4b6223dc00 Update changelog 2025-11-05 09:46:05 -08:00
Jon Staab 5525e45a15 Bump version, upgrade welshman 2025-11-05 09:42:27 -08:00
Jon Staab 80a2ae60b0 Bump version 2025-11-04 17:28:27 -08:00
Jon Staab d7e95f5d2f Fix chat url 2025-11-04 17:25:50 -08:00
Jon Staab ca4e5ae5ee Add shadow to thread items etc, bump welshman, update changelog, update version 2025-11-04 17:14:33 -08:00
Jon Staab b673658c0c Handle escape in chat 2025-11-04 16:59:17 -08:00
Jon Staab 5c5c130700 Add landlubber link if user is admin 2025-11-04 16:55:26 -08:00
Jon Staab 2d89ca6c0e Support invite links on discover page 2025-11-04 16:39:34 -08:00
Jon Staab 806a7c2609 Persist alert kinds again 2025-11-04 16:25:21 -08:00
Jon Staab 501ce8067d Detect nip29 properly before choosing smart path, more robust auth error checking 2025-11-04 16:14:32 -08:00
Jon Staab 6429f82829 Improve claim/access detection 2025-11-04 15:36:20 -08:00
Jon Staab fe626218ea Ignore aborted signatures when checking auth 2025-11-04 09:34:07 -08:00
Jon Staab b62b1bc063 Don't source local .env file on build 2025-11-04 09:18:26 -08:00
Jon Staab d980f36246 Use request instead of load to avoid timeouts 2025-11-04 09:05:17 -08:00
Jon Staab b469addd29 Remove withGetter 2025-11-03 14:52:12 -08:00
Jon Staab 6923c2a8b7 Tweak modal, reduce storage on mobile 2025-11-03 14:43:27 -08:00
Jon Staab 1d3f32fb99 Only return error from attemptRelayAccess if there is a claim sent 2025-11-03 12:08:50 -08:00
Jon Staab 42a550788a Fix some alerts stuff 2025-11-03 11:10:16 -08:00
Jon Staab b1c68972c9 Streamline deriveRoom 2025-10-31 16:19:22 -07:00
Jon Staab 3978e32d5f Tweak access terminology, relay access attempts 2025-10-31 16:00:14 -07:00
Jon Staab ba2b5d182e Fix alerts 2025-10-31 14:51:59 -07:00
Jon Staab bef04fa899 Add holis to hosting suggestions 2025-10-31 14:02:52 -07:00
Jon Staab 4f8609421c Fix membership status 2025-10-31 12:10:16 -07:00
Jon Staab 07660c9d44 Re-work rooms derivation 2025-10-30 15:52:24 -07:00
Jon Staab a324dad2ba Rename channel to room 2025-10-30 15:36:14 -07:00
Jon Staab dbaa0f5d49 Rename room variables to h 2025-10-30 15:33:34 -07:00
Jon Staab 478721d349 Add room editing 2025-10-30 15:22:31 -07:00
Jon Staab a669a23dbc Tweak reaction buttons 2025-10-30 12:53:21 -07:00
Jon Staab cfeb6478cc Fix flapping subscription 2025-10-30 12:06:53 -07:00
Jon Staab 64539c49c1 Fix link, spinner animation 2025-10-30 07:20:09 -07:00
Jon Staab 0399ae37ec Move space create to its own page 2025-10-29 12:52:26 -07:00
Jon Staab 173a411a36 Update space create dialog 2025-10-29 11:18:27 -07:00
Jon Staab 62013a2ea2 Tweak mobile space menu 2025-10-28 16:50:15 -07:00
Jon Staab c82cf4a4c2 Update platform url 2025-10-28 16:08:48 -07:00
121 changed files with 2109 additions and 1451 deletions
+1 -1
View File
@@ -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
+34
View File
@@ -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
View File
@@ -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.
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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.
-4
View File
@@ -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"
+4 -4
View File
@@ -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
View File
@@ -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",
+132 -132
View File
@@ -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
View File
@@ -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 {
+2 -2
View File
@@ -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()}
+4 -4
View File
@@ -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})
+7 -5
View File
@@ -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} />
-7
View File
@@ -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}
+4 -12
View File
@@ -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">
+6 -6
View File
@@ -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} />
+2 -2
View File
@@ -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>
+6 -6
View File
@@ -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}
+1 -1
View File
@@ -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 -9
View File
@@ -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>
+5 -5
View File
@@ -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" />
+4 -4
View File
@@ -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({
+5 -5
View File
@@ -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} />
+1 -1
View File
@@ -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..." />
+19 -7
View File
@@ -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}>
+5 -24
View File
@@ -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>
-35
View File
@@ -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>
+3 -3
View File
@@ -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">
+8 -2
View File
@@ -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) => {
+1 -1
View File
@@ -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">
+25 -28
View File
@@ -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>
+5 -10
View File
@@ -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>
+5 -11
View File
@@ -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">
+14 -7
View File
@@ -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" />
+8 -3
View File
@@ -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>
+2 -2
View File
@@ -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">
+2 -2
View File
@@ -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}
+2 -2
View File
@@ -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}
+2 -2
View File
@@ -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} />
+19 -8
View File
@@ -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}
+21
View File
@@ -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} />
+5 -1
View File
@@ -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>
+37
View File
@@ -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"
@@ -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>
+28 -162
View File
@@ -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>
+190
View File
@@ -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>
+53
View File
@@ -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>
+206
View File
@@ -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>
+22
View File
@@ -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>
@@ -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} />
@@ -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>
@@ -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>
+17
View File
@@ -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>
+2 -2
View File
@@ -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})
+26 -6
View File
@@ -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>
+29 -6
View File
@@ -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>
-20
View File
@@ -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} />
+15 -8
View File
@@ -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>
-76
View File
@@ -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
+23 -1
View File
@@ -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>
+202
View File
@@ -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>
+9 -4
View File
@@ -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} />
+2 -20
View File
@@ -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)}>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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?" />
+5 -5
View File
@@ -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" />
+4 -4
View File
@@ -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({
+7 -5
View File
@@ -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} />
+1 -1
View File
@@ -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
View File
@@ -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
+6 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+2 -2
View File
@@ -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")
+2 -2
View File
@@ -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>
+1
View File
@@ -6,6 +6,7 @@ import {page} from "$app/stores"
export type ModalOptions = {
drawer?: boolean
noEscape?: boolean
fullscreen?: boolean
replaceState?: boolean
path?: string
+1 -8
View File
@@ -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,
+3 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
-30
View File
@@ -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>
+2 -2
View File
@@ -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">
+7 -2
View File
@@ -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>
+1 -1
View File
@@ -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