Compare commits

...

40 Commits

Author SHA1 Message Date
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
Jon Staab df42085be6 Sync messages at the space level 2025-10-28 15:46:25 -07:00
Jon Staab b09d3065ae Fix app url on capacitor deployments 2025-10-28 15:40:28 -07:00
Jon Staab c050f5a9e3 Update changelog 2025-10-28 15:37:36 -07:00
Jon Staab 78e6c0eca0 Bump version 2025-10-28 15:35:39 -07:00
Jon Staab da4da45348 Load rooms correctly 2025-10-28 14:53:44 -07:00
Jon Staab dc2af86db8 Bump welshman 2025-10-28 13:26:02 -07:00
Jon Staab 7502004aba Improve syncing 2025-10-28 11:29:59 -07:00
85 changed files with 1570 additions and 1255 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_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_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
VITE_BURROW_URL= 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_TERMS=https://flotilla.social/terms
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
VITE_PLATFORM_NAME=Flotilla VITE_PLATFORM_NAME=Flotilla
+24
View File
@@ -1,5 +1,29 @@
# Changelog # Changelog
# 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
* Fix app url on capacitor deployments
# 1.4.0 # 1.4.0
* Allow "editing" chat messages * Allow "editing" chat messages
+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. - 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. - 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) - "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. `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): 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_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_PLATFORM_URL` - The url where the app will be hosted
- `VITE_PLATFORM_NAME` - The name of the app - `VITE_PLATFORM_NAME` - The name of the app
- `VITE_PLATFORM_LOGO` - A logo url for 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. - `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" applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion targetSdk rootProject.ext.targetSdkVersion
versionCode 29 versionCode 33
versionName "1.4.0" versionName "1.5.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // 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 source .env.template
fi fi
if [ -f .env ]; then
source .env
fi
# Avoid overwriting env vars provided directly # Avoid overwriting env vars provided directly
# https://stackoverflow.com/a/69127685/1467342 # https://stackoverflow.com/a/69127685/1467342
eval "$temp_env" eval "$temp_env"
+4 -4
View File
@@ -358,14 +358,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements"; CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.4.0; MARKETING_VERSION = 1.5.2;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -384,14 +384,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements"; CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.4.0; MARKETING_VERSION = 1.5.2;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
+11 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "1.4.0", "version": "1.5.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -60,16 +60,16 @@
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8", "@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.6.2", "@welshman/app": "^0.6.5",
"@welshman/content": "^0.6.2", "@welshman/content": "^0.6.5",
"@welshman/editor": "^0.6.2", "@welshman/editor": "^0.6.5",
"@welshman/feeds": "^0.6.2", "@welshman/feeds": "^0.6.5",
"@welshman/lib": "^0.6.2", "@welshman/lib": "^0.6.5",
"@welshman/net": "^0.6.2", "@welshman/net": "^0.6.5",
"@welshman/router": "^0.6.2", "@welshman/router": "^0.6.5",
"@welshman/signer": "^0.6.2", "@welshman/signer": "^0.6.5",
"@welshman/store": "^0.6.2", "@welshman/store": "^0.6.5",
"@welshman/util": "^0.6.2", "@welshman/util": "^0.6.5",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"daisyui": "^4.12.24", "daisyui": "^4.12.24",
"date-picker-svelte": "^2.16.0", "date-picker-svelte": "^2.16.0",
+132 -132
View File
@@ -72,35 +72,35 @@ importers:
specifier: ^0.6.8 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)) 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': '@welshman/app':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) version: 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/content': '@welshman/content':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2(typescript@5.9.3) version: 0.6.5(typescript@5.9.3)
'@welshman/editor': '@welshman/editor':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2(@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) version: 0.6.5(@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': '@welshman/feeds':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) version: 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/lib': '@welshman/lib':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2 version: 0.6.5
'@welshman/net': '@welshman/net':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2(typescript@5.9.3)(ws@8.18.3) version: 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': '@welshman/router':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2(typescript@5.9.3)(ws@8.18.3) version: 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/signer': '@welshman/signer':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) version: 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/store': '@welshman/store':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2(typescript@5.9.3)(ws@8.18.3) version: 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': '@welshman/util':
specifier: ^0.6.2 specifier: ^0.6.5
version: 0.6.2(typescript@5.9.3) version: 0.6.5(typescript@5.9.3)
compressorjs: compressorjs:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
@@ -1451,77 +1451,77 @@ packages:
peerDependencies: peerDependencies:
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-code-block@2.26.3': '@tiptap/extension-code-block@2.27.1':
resolution: {integrity: sha512-3DbzKRfMqw9EGS7mGkpyopbRWTO+qpV52Mby4Ll2+OfhvGnHzSN4Q7xOsp+VeZr14GMEmua5Oq2e/gRypqXatQ==} resolution: {integrity: sha512-wCI5VIOfSAdkenCWFvh4m8FFCJ51EOK+CUmOC/PWUjyo2Dgn8QC8HMi015q8XF7886T0KvYVVoqxmxJSUDAYNg==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-code@2.26.3': '@tiptap/extension-code@2.27.1':
resolution: {integrity: sha512-bAkUNzV+tA1J1RYbtbAGTFqkRw9+yRpAd+d3S9jy/dAD+uOe1ZD1EIngyEf2GTonnoy4bpDYtytbCjUt9PozoA==} resolution: {integrity: sha512-i65wUGJevzBTIIUBHBc1ggVa27bgemvGl/tY1/89fEuS/0Xmre+OQjw8rCtSLevoHSiYYLgLRlvjtUSUhE4kgg==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-document@2.26.3': '@tiptap/extension-document@2.27.1':
resolution: {integrity: sha512-gcJg4Otchilr4eSUwhPNwbhPUkEYvXhkUZ/1MAhVGD40Ovq2P8ZWkJipA3tKOCJinL5MJK59ccZBstnKSTw+JA==} resolution: {integrity: sha512-NtJzJY7Q/6XWjpOm5OXKrnEaofrcc1XOTYlo/SaTwl8k2bZo918Vl0IDBWhPVDsUN7kx767uHwbtuQZ+9I82hA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-dropcursor@2.26.3': '@tiptap/extension-dropcursor@2.27.1':
resolution: {integrity: sha512-54rgDTmRStVmXZR7KdCvSOCAbumh5luXgticUkRM8OM8PBe1c0T9X8jfV7+XEFGugRVl8mtCZZpgUt5vhuxHog==} resolution: {integrity: sha512-3MBQRGHHZ0by3OT0CWbLKS7J3PH9PpobrXjmIR7kr0nde7+bHqxXiVNuuIf501oKU9rnEUSedipSHkLYGkmfsA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-gapcursor@2.26.3': '@tiptap/extension-gapcursor@2.27.1':
resolution: {integrity: sha512-ZDNSkpz7ik2PJOjrys27rwko5Ufe6GtLjaAxjvkWmyzcgAOTadDeth9NaRdBVMDGgSLBKbXihYZZXLkiAP9RLA==} resolution: {integrity: sha512-A9e1jr+jGhDWzNSXtIO6PYVYhf5j/udjbZwMja+wCE/3KvZU9V3IrnGKz1xNW+2Q2BDOe1QO7j5uVL9ElR6nTA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-hard-break@2.26.3': '@tiptap/extension-hard-break@2.27.1':
resolution: {integrity: sha512-KJWUi+2KOZejVRb2KI0NM3LgCpNimxcunbOCKsZKygV/UByzhUl7UaCAIa+ySMM+kbu/Ec3hkTzafGfaU9ZkLg==} resolution: {integrity: sha512-W4hHa4Io6QCTwpyTlN6UAvqMIQ7t56kIUByZhyY9EWrg/+JpbfpxE1kXFLPB4ZGgwBknFOw+e4bJ1j3oAbTJFw==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-history@2.26.3': '@tiptap/extension-history@2.27.1':
resolution: {integrity: sha512-Qg4+WWf/hDgiBspxLbrhrIFUy7lzi2eBKPSoF/haEYFw/t/FeN60NXYYYtpLimUNpUzyJSOSIwsngFcVJO5X+g==} resolution: {integrity: sha512-K8PHC9gegSAt0wzSlsd4aUpoEyIJYOmVVeyniHr1P1mIblW1KYEDbRGbDlrLALTyUEfMcBhdIm8zrB9X2Nihvg==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-image@2.26.3': '@tiptap/extension-image@2.27.1':
resolution: {integrity: sha512-juAAY1QuzCgfl66Q8AHITLVKbwXpv+BmLNCi8Cl4j6a+IkySzcS8gENJee0hMMyRvc9K1U75o4vokvy580u4kA==} resolution: {integrity: sha512-wu3vMKDYWJwKS6Hrw5PPCKBO2RxyHNeFLiA/uDErEV7axzNpievK/U9DyaDXmtK3K/h1XzJAJz19X+2d/pY68w==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-link@2.26.3': '@tiptap/extension-link@2.27.1':
resolution: {integrity: sha512-cNYqAeiaG/65ctVEUOHt1MQnTF1JcdZqBkN9pLf3grzcmkmdr3w1/JbKOphZc84vOB2rxuhGZx9NFV2lrC5Qwg==} resolution: {integrity: sha512-cCwWPZsnVh9MXnGOqSIRXPPuUixRDK8eMN2TvqwbxUBb1TU7b/HtNvfMU4tAOqAuMRJ0aJkFuf3eB0Gi8LVb1g==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-paragraph@2.26.3': '@tiptap/extension-paragraph@2.27.1':
resolution: {integrity: sha512-eBC5UsaTJRUMhePtK1dcCAfes0CpqqFiewpIM0lWk4XMtpG2aoczVVVkImybbFKfqsvEEo3vgHJ2YiE5YZFCSg==} resolution: {integrity: sha512-R3QdrHcUdFAsdsn2UAIvhY0yWyHjqGyP/Rv8RRdN0OyFiTKtwTPqreKMHKJOflgX4sMJl/OpHTpNG1Kaf7Lo2A==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-placeholder@2.26.3': '@tiptap/extension-placeholder@2.27.1':
resolution: {integrity: sha512-HDF4FZj8CmQQvbSyXb/G+Ujqoue7TMQPMAe1h1OMJAXq856Y0AsVLXYKiBojUTfI11I7zVwYe08D8atIXHLZZw==} resolution: {integrity: sha512-UbXaibHHFE+lOTlw/vs3jPzBoj1sAfbXuTAhXChjgYIcTTY5Cr6yxwcymLcimbQ79gf04Xkua2FCN3YsJxIFmw==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-text@2.26.3': '@tiptap/extension-text@2.27.1':
resolution: {integrity: sha512-sGRbX96ss4jQeKw9d0iphuAWja8Dv4w4ryTDKfxD7Lizx3UaIxQB/y+Wna89tM3kfbi/qJcrD3AF7NJgfc/tEA==} resolution: {integrity: sha512-a4GCT+GZ9tUwl82F4CEum9/+WsuW0/De9Be/NqrMmi7eNfAwbUTbLCTFU0gEvv25WMHCoUzaeNk/qGmzeVPJ1Q==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm@2.26.3': '@tiptap/pm@2.26.3':
resolution: {integrity: sha512-8gUmdxWlUevmgq2mNvGxvf2CpDW097tVKECMWKEn8sf846kXv3CoqaGRhI3db4kfR+09uWZeRM7rtrjRBmUThg==} resolution: {integrity: sha512-8gUmdxWlUevmgq2mNvGxvf2CpDW097tVKECMWKEn8sf846kXv3CoqaGRhI3db4kfR+09uWZeRM7rtrjRBmUThg==}
'@tiptap/suggestion@2.26.3': '@tiptap/suggestion@2.27.1':
resolution: {integrity: sha512-kcyiyKEEDnqFImGQQEEuRa6N/N+/vU/OrI99wRfJnDnN8c3dP6UHJ4wr2qX6bUpx3Z0QTu6GGCpMpaqwtHTtJg==} resolution: {integrity: sha512-yTy75ZMYgVWM18cl7YxLqMJ7TorQTGysSd1aKmBA9qd8uzYlvLMmHKE9qBDxM9HXODBz1DA/BLLm9esv2enmFw==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
@@ -1692,38 +1692,38 @@ packages:
'@vite-pwa/assets-generator': '@vite-pwa/assets-generator':
optional: true optional: true
'@welshman/app@0.6.2': '@welshman/app@0.6.5':
resolution: {integrity: sha512-URvw8IMCkZpotyKyCKYyyHkNmF9ghYUAelZmfQrC0YttiDUVQGE0qq5/3gwZ1HAy6f9hbMWH2hfPLVnaEh4K2A==} resolution: {integrity: sha512-hk39kKzptldZxtFbYzgrEK8Y151o75GwV6P2sK5LlkyafWlhx3SwteAcuNelcJZitoAPXi7w06W34bbwRYPx+Q==}
'@welshman/content@0.6.2': '@welshman/content@0.6.5':
resolution: {integrity: sha512-eiDpxKhm87i2JCsINtt8SGkt58JCL0UB6i0rzHsyZC1oOSM0W3OJ3NkZGDNooneDe02GpvgzDAAJa3m8QgnnzA==} resolution: {integrity: sha512-QSlkuko+2r72q3VFlOXpnnoJ6GioCgan1ysHMlKqKarKNFTL4kfqdq1yxYrFRJdQou7WuB+f9iULO0AFWkXmXg==}
'@welshman/editor@0.6.2': '@welshman/editor@0.6.5':
resolution: {integrity: sha512-+sCnDCvjPLy94maRPLJdZEdQgys6XyYPOmxpXaj/AmkhqGIGovA/7YiIB86vS1NHmRy02hgM/eMV311jaVe6ww==} resolution: {integrity: sha512-3sUnUFBeaVJbJgnkZlIqFqXv/NtnxXt3Pr6BkMYi2ocDMxHsMOIsOrCcyoXg8G5IYz7FzkWHtUtM3mhaDU7YVg==}
'@welshman/feeds@0.6.2': '@welshman/feeds@0.6.5':
resolution: {integrity: sha512-bOgcLwVd/n5HlhxuflBZLXJZfKhQAJDbYh40sNr1dtKh4eM6vd5ujMZyj8D2OwdfazOAVX3ZwRenJCdy5+gwVw==} resolution: {integrity: sha512-IT1kSN+Xf/MaoHAOHJftORDwJZxl3UCLizc+mvJ4ktvOT/oVu9YX5zcb0YMwiJN2N4C4FpK/BIBjxivS6QIaRQ==}
'@welshman/lib@0.6.2': '@welshman/lib@0.6.5':
resolution: {integrity: sha512-1GpCOr0pXTDdy87PEvAV0+lvjgQbFODU5RsNM0LTyeU0885uzPHrfoJbZoNXayY9klwuvS9NBHFkUN7lySITzA==} resolution: {integrity: sha512-L6NQm1QNBOTQ+ymiSFPfL+TDiW1AP64AEp633Fh1ciopaU73JFbV0P6xpLxt3qJkQFZfJRxk7gWDMaVDo3Y24w==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
'@welshman/net@0.6.2': '@welshman/net@0.6.5':
resolution: {integrity: sha512-MX2nGqH2/P/Q9WeIiN9AHo4/n2S4mABE6gCACB5vA1v3FdwKjinN/otoUJGo+fTlaIMJHnxoDYV4vOT3F9mZuA==} resolution: {integrity: sha512-JcmPdWzT0aUaOWytw4EJpl9EooOstm4LcJczc9pJYk1hQE4gDix1AfW6bqBiDlTnJ5fplOW/KLgXuV1rhsKEWA==}
'@welshman/router@0.6.2': '@welshman/router@0.6.5':
resolution: {integrity: sha512-dyTFn5h1oxP7bh/G+kydqfvXTPjmq2aZ1ikwJ2rOsCaNNqBnm/d9o6r9knZoWGJpLD6a5szY5lA9Vo9DtUuouA==} resolution: {integrity: sha512-7ZxAkCg09ZIeYh49LUlL7nFRvU4880DsMstEu2KRQQIO/wg6VZuMJh8+uKGQq5arul09rtNl1bhg0/YUFiAc/A==}
'@welshman/signer@0.6.2': '@welshman/signer@0.6.5':
resolution: {integrity: sha512-UYCr0c9xj2qbknDD5HH94+cfdX7v27PGJPZ9V7JuQQHdDzK8LUOLXf5NvGqcTbAb6IlUnTGbvA19oJZP6l0vNw==} resolution: {integrity: sha512-SgQCtb0du3vpyaRVGXM43CM5S0fTh+1WWLnZEWUBkEpyRzRtI9DegTowL6+vhbNxsWB6oHn/FqY7O3HLS5rIEw==}
peerDependencies: peerDependencies:
nostr-signer-capacitor-plugin: ~0.0.4 nostr-signer-capacitor-plugin: ~0.0.4
'@welshman/store@0.6.2': '@welshman/store@0.6.5':
resolution: {integrity: sha512-ZRELLUtadbUoLV678qJ8Lt2KmcCe+Qi3frsKJmPW5HeuPHKgNJBY9QgqYK2D07T42nLGqrqLdVfv3Mz0xp3eGA==} resolution: {integrity: sha512-Fdl8ygK2/pZRxbLGSWtJJGtf2wTm46RDrCF2zURDJL6e80NGmTXl7LhqpSeRKmR1sTQiwEhsRvD1lqnKAWB3xQ==}
'@welshman/util@0.6.2': '@welshman/util@0.6.5':
resolution: {integrity: sha512-stBlkzlso4A0mpicQjwrHvfXdbZ5XTT2K4iuTFe9a4DN5bKkDiHYEWvEjbd3751TJK1BKH9snOfgdKuxnkvSeg==} resolution: {integrity: sha512-BmKgDtgSk0RSnw3YyExN8Mm25TJuJnjvE/7foTENpf2bMo2+PTXwVERNmgDEHRq9MCbmTkPv4h7kJTbpywLMVA==}
'@xml-tools/parser@1.0.11': '@xml-tools/parser@1.0.11':
resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==} resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==}
@@ -3489,8 +3489,8 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
nostr-editor@1.0.1: nostr-editor@1.0.2:
resolution: {integrity: sha512-HXqXjxtIN0CcC7sLV5xYjEsQF0bFYLmNKxS75ya2yZGQ/z16U+uK6bb2Hd72QyqXlHXyWN0m24E5Gcws8/NhRQ==} resolution: {integrity: sha512-z1XfVH0cDsDBvIfsNfIjjD1MI+ugChMbJToNIlKXi6aMkm8KgZOkHl9nkKdkAfZXU5yk+DPTEvv433NPZp2yKA==}
engines: {node: '>=18.16.1'} engines: {node: '>=18.16.1'}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.6.6 '@tiptap/core': ^2.6.6
@@ -6346,58 +6346,58 @@ snapshots:
dependencies: dependencies:
'@tiptap/pm': 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-code-block@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@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-dropcursor@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@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-history@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@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-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@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 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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@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-placeholder@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
@@ -6422,7 +6422,7 @@ snapshots:
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.4
prosemirror-view: 1.41.3 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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
@@ -6651,16 +6651,16 @@ snapshots:
optionalDependencies: optionalDependencies:
'@vite-pwa/assets-generator': 0.2.6 '@vite-pwa/assets-generator': 0.2.6
'@welshman/app@0.6.2(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)': '@welshman/app@0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@types/throttle-debounce': 5.0.2 '@types/throttle-debounce': 5.0.2
'@welshman/feeds': 0.6.2(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) '@welshman/feeds': 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/lib': 0.6.2 '@welshman/lib': 0.6.5
'@welshman/net': 0.6.2(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': 0.6.2(typescript@5.9.3)(ws@8.18.3) '@welshman/router': 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/signer': 0.6.2(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) '@welshman/signer': 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/store': 0.6.2(typescript@5.9.3)(ws@8.18.3) '@welshman/store': 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.2(typescript@5.9.3) '@welshman/util': 0.6.5(typescript@5.9.3)
fuse.js: 7.1.0 fuse.js: 7.1.0
svelte: 4.2.20 svelte: 4.2.20
throttle-debounce: 5.0.2 throttle-debounce: 5.0.2
@@ -6669,31 +6669,31 @@ snapshots:
- typescript - typescript
- ws - ws
'@welshman/content@0.6.2(typescript@5.9.3)': '@welshman/content@0.6.5(typescript@5.9.3)':
dependencies: dependencies:
'@braintree/sanitize-url': 7.1.1 '@braintree/sanitize-url': 7.1.1
nostr-tools: 2.17.0(typescript@5.9.3) nostr-tools: 2.17.0(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@welshman/editor@0.6.2(@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.5(@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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@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': 2.27.1(@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-code-block': 2.27.1(@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-document': 2.27.1(@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)
'@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)
'@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))
'@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)
'@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))
'@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)
'@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))
'@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) '@tiptap/suggestion': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
'@welshman/lib': 0.6.2 '@welshman/lib': 0.6.5
'@welshman/util': 0.6.2(typescript@5.9.3) '@welshman/util': 0.6.5(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))) 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) nostr-tools: 2.17.0(typescript@5.9.3)
tippy.js: 6.3.7 tippy.js: 6.3.7
transitivePeerDependencies: transitivePeerDependencies:
@@ -6707,71 +6707,71 @@ snapshots:
- tiptap-markdown - tiptap-markdown
- typescript - typescript
'@welshman/feeds@0.6.2(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)': '@welshman/feeds@0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.2 '@welshman/lib': 0.6.5
'@welshman/net': 0.6.2(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': 0.6.2(typescript@5.9.3)(ws@8.18.3) '@welshman/router': 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/signer': 0.6.2(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) '@welshman/signer': 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.2(typescript@5.9.3) '@welshman/util': 0.6.5(typescript@5.9.3)
trava: 1.2.1 trava: 1.2.1
transitivePeerDependencies: transitivePeerDependencies:
- nostr-signer-capacitor-plugin - nostr-signer-capacitor-plugin
- typescript - typescript
- ws - ws
'@welshman/lib@0.6.2': '@welshman/lib@0.6.5':
dependencies: dependencies:
'@scure/base': 1.2.6 '@scure/base': 1.2.6
'@types/events': 3.0.3 '@types/events': 3.0.3
events: 3.3.0 events: 3.3.0
'@welshman/net@0.6.2(typescript@5.9.3)(ws@8.18.3)': '@welshman/net@0.6.5(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.2 '@welshman/lib': 0.6.5
'@welshman/util': 0.6.2(typescript@5.9.3) '@welshman/util': 0.6.5(typescript@5.9.3)
events: 3.3.0 events: 3.3.0
isomorphic-ws: 5.0.0(ws@8.18.3) isomorphic-ws: 5.0.0(ws@8.18.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/router@0.6.2(typescript@5.9.3)(ws@8.18.3)': '@welshman/router@0.6.5(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.2 '@welshman/lib': 0.6.5
'@welshman/net': 0.6.2(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.2(typescript@5.9.3) '@welshman/util': 0.6.5(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/signer@0.6.2(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)': '@welshman/signer@0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@noble/curves': 1.9.7 '@noble/curves': 1.9.7
'@noble/hashes': 1.8.0 '@noble/hashes': 1.8.0
'@welshman/lib': 0.6.2 '@welshman/lib': 0.6.5
'@welshman/net': 0.6.2(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.2(typescript@5.9.3) '@welshman/util': 0.6.5(typescript@5.9.3)
nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.4.3) nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.4.3)
nostr-tools: 2.17.0(typescript@5.9.3) nostr-tools: 2.17.0(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/store@0.6.2(typescript@5.9.3)(ws@8.18.3)': '@welshman/store@0.6.5(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.2 '@welshman/lib': 0.6.5
'@welshman/net': 0.6.2(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.2(typescript@5.9.3) '@welshman/util': 0.6.5(typescript@5.9.3)
svelte: 4.2.20 svelte: 4.2.20
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/util@0.6.2(typescript@5.9.3)': '@welshman/util@0.6.5(typescript@5.9.3)':
dependencies: dependencies:
'@types/ws': 8.18.1 '@types/ws': 8.18.1
'@welshman/lib': 0.6.2 '@welshman/lib': 0.6.5
js-base64: 3.7.8 js-base64: 3.7.8
nostr-tools: 2.17.0(typescript@5.9.3) nostr-tools: 2.17.0(typescript@5.9.3)
nostr-wasm: 0.1.0 nostr-wasm: 0.1.0
@@ -8625,11 +8625,11 @@ snapshots:
normalize-range@0.1.2: {} 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: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@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-image': 2.27.1(@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)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
js-base64: 3.7.8 js-base64: 3.7.8
light-bolt11-decoder: 3.2.0 light-bolt11-decoder: 3.2.0
+1 -1
View File
@@ -395,7 +395,7 @@ progress[value]::-webkit-progress-value {
/* chat view */ /* chat view */
.chat__compose { .chat__compose {
@apply cb cw fixed; @apply cb cw fixed z-compose;
} }
.chat__scroll-down { .chat__scroll-down {
@@ -5,7 +5,7 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.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 ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte" import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte" import EventActivity from "@app/components/EventActivity.svelte"
@@ -25,7 +25,7 @@
const {url, event, showRoom, showActivity}: Props = $props() 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 path = makeCalendarPath(url, event.id)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -39,9 +39,9 @@
</script> </script>
<div class="flex flex-grow flex-wrap justify-end gap-2"> <div class="flex flex-grow flex-wrap justify-end gap-2">
{#if room && showRoom} {#if h && showRoom}
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full"> <Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<ChannelName {room} {url} /> Posted in #<RoomName {h} {url} />
</Link> </Link>
{/if} {/if}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" /> <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
@@ -4,13 +4,13 @@
type Props = { type Props = {
url: string url: string
room?: string h?: string
} }
const {url, room}: Props = $props() const {url, h}: Props = $props()
</script> </script>
<CalendarEventForm {url} {room}> <CalendarEventForm {url} {h}>
{#snippet header()} {#snippet header()}
<ModalHeader> <ModalHeader>
{#snippet title()} {#snippet title()}
+4 -4
View File
@@ -23,7 +23,7 @@
type Props = { type Props = {
url: string url: string
room?: string h?: string
header: Snippet header: Snippet
initialValues?: { initialValues?: {
d: string d: string
@@ -35,7 +35,7 @@
} }
} }
const {url, room, header, initialValues}: Props = $props() const {url, h, header, initialValues}: Props = $props()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -85,8 +85,8 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
if (room) { if (h) {
tags.push(["h", room]) tags.push(["h", h])
} }
const event = makeEvent(EVENT_TIME, {content, tags}) const event = makeEvent(EVENT_TIME, {content, tags})
+7 -5
View File
@@ -5,7 +5,7 @@
import CalendarEventActions from "@app/components/CalendarEventActions.svelte" import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte" import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
import ProfileLink from "@app/components/ProfileLink.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" import {makeCalendarPath} from "@app/util/routes"
type Props = { type Props = {
@@ -15,16 +15,18 @@
const {url, event}: Props = $props() const {url, event}: Props = $props()
const room = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
</script> </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-xl"
href={makeCalendarPath(url, event.id)}>
<CalendarEventHeader {event} /> <CalendarEventHeader {event} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <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"> <span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by <ProfileLink pubkey={event.pubkey} {url} /> Posted by <ProfileLink pubkey={event.pubkey} {url} />
{#if room} {#if h}
in <ChannelLink {url} {room} /> in <RoomLink {url} {h} />
{/if} {/if}
</span> </span>
<CalendarEventActions showActivity {url} {event} /> <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}
+5 -5
View File
@@ -13,16 +13,16 @@
type Props = { type Props = {
url: string url: string
onClick: () => void 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 let ul: Element
+6 -6
View File
@@ -8,29 +8,29 @@
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte" import ProfileCircles from "@app/components/ProfileCircles.svelte"
import {goToEvent} from "@app/util/routes" import {goToEvent} from "@app/util/routes"
import {displayChannel} from "@app/core/state" import {displayRoom} from "@app/core/state"
type Props = { type Props = {
url: string url: string
room?: string h?: string
events: TrustedEvent[] events: TrustedEvent[]
latest: TrustedEvent latest: TrustedEvent
earliest: TrustedEvent earliest: TrustedEvent
participants: string[] participants: string[]
} }
const {url, room, events, latest, earliest, participants}: Props = $props() const {url, h, events, latest, earliest, participants}: Props = $props()
</script> </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 flex-col gap-3">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<ProfileCircle pubkey={earliest.pubkey} size={10} /> <ProfileCircle pubkey={earliest.pubkey} size={10} />
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-2 text-sm opacity-70"> <div class="flex items-center gap-2 text-sm opacity-70">
{#if room} {#if h}
<span class="truncate font-medium text-blue-400"> <span class="truncate font-medium text-blue-400">
#{displayChannel(url, room)} #{displayRoom(url, h)}
</span> </span>
<span class="opacity-50"></span> <span class="opacity-50"></span>
{/if} {/if}
+9 -9
View File
@@ -9,8 +9,8 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import RoomName from "@app/components/RoomName.svelte"
import {channelsByUrl} from "@app/core/state" import {roomsByUrl} from "@app/core/state"
import {makeRoomPath} from "@app/util/routes" import {makeRoomPath} from "@app/util/routes"
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props() const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
@@ -22,8 +22,8 @@
goto(makeRoomPath(url, selection), {replaceState: true}) goto(makeRoomPath(url, selection), {replaceState: true})
} }
const toggleRoom = (room: string) => { const toggleRoom = (h: string) => {
selection = room === selection ? "" : room selection = h === selection ? "" : h
} }
let selection = $state("") let selection = $state("")
@@ -39,14 +39,14 @@
{/snippet} {/snippet}
</ModalHeader> </ModalHeader>
<div class="grid grid-cols-3 gap-2"> <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 <button
type="button" type="button"
class="btn" class="btn"
class:btn-neutral={selection !== channel.room} class:btn-neutral={selection !== room.h}
class:btn-primary={selection === channel.room} class:btn-primary={selection === room.h}
onclick={() => toggleRoom(channel.room)}> onclick={() => toggleRoom(room.h)}>
#<ChannelName {...channel} /> #<RoomName {...room} />
</button> </button>
{/each} {/each}
</div> </div>
+5 -5
View File
@@ -6,7 +6,7 @@
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte" import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte" import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.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 {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
import {makeGoalPath, makeSpacePath} from "@app/util/routes" import {makeGoalPath, makeSpacePath} from "@app/util/routes"
@@ -20,7 +20,7 @@
const {url, event, showRoom, showActivity}: Props = $props() const {url, event, showRoom, showActivity}: Props = $props()
const path = makeGoalPath(url, event.id) const path = makeGoalPath(url, event.id)
const room = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
@@ -31,9 +31,9 @@
</script> </script>
<div class="flex flex-grow flex-wrap justify-end gap-2"> <div class="flex flex-grow flex-wrap justify-end gap-2">
{#if room && showRoom} {#if h && showRoom}
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full"> <Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<ChannelName {room} {url} /> Posted in #<RoomName {h} {url} />
</Link> </Link>
{/if} {/if}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" /> <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
+4 -4
View File
@@ -20,10 +20,10 @@
type Props = { type Props = {
url: string url: string
room?: string h?: string
} }
const {url, room}: Props = $props() const {url, h}: Props = $props()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -64,8 +64,8 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
if (room) { if (h) {
tags.push(["h", room]) tags.push(["h", h])
} }
publishThunk({ publishThunk({
+5 -5
View File
@@ -6,7 +6,7 @@
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
import GoalActions from "@app/components/GoalActions.svelte" import GoalActions from "@app/components/GoalActions.svelte"
import GoalSummary from "@app/components/GoalSummary.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" import {makeGoalPath} from "@app/util/routes"
type Props = { type Props = {
@@ -17,10 +17,10 @@
const {url, event}: Props = $props() const {url, event}: Props = $props()
const summary = getTagValue("summary", event.tags) const summary = getTagValue("summary", event.tags)
const room = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
</script> </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-xl" href={makeGoalPath(url, event.id)}>
<p class="text-2xl">{event.content}</p> <p class="text-2xl">{event.content}</p>
<Content <Content
event={{content: summary, tags: event.tags}} 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"> <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"> <span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by <ProfileLink pubkey={event.pubkey} {url} /> Posted by <ProfileLink pubkey={event.pubkey} {url} />
{#if room} {#if h}
in <ChannelLink {url} {room} /> in <RoomLink {url} {h} />
{/if} {/if}
</span> </span>
<GoalActions showActivity {url} {event} /> <GoalActions showActivity {url} {event} />
+17 -7
View File
@@ -12,6 +12,7 @@
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Login from "@assets/icons/login-3.svg?dataurl" import Login from "@assets/icons/login-3.svg?dataurl"
import History from "@assets/icons/history.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 StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl" import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl" import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
@@ -38,7 +39,7 @@
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte" import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
import { import {
ENABLE_ZAPS, ENABLE_ZAPS,
MESSAGE_FILTER, CONTENT_KINDS,
deriveSpaceMembers, deriveSpaceMembers,
deriveEventsForUrl, deriveEventsForUrl,
deriveUserRooms, deriveUserRooms,
@@ -47,6 +48,7 @@
hasNip29, hasNip29,
alerts, alerts,
deriveUserCanCreateRoom, deriveUserCanCreateRoom,
deriveUserIsSpaceAdmin,
} from "@app/core/state" } from "@app/core/state"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
@@ -63,9 +65,10 @@
const otherRooms = deriveOtherRooms(url) const otherRooms = deriveOtherRooms(url)
const members = deriveSpaceMembers(url) const members = deriveSpaceMembers(url)
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url))) const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const spaceKinds = derived( const spaceKinds = derived(
deriveEventsForUrl(url, [MESSAGE_FILTER]), deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
$events => new Set($events.map(e => e.kind)), $events => new Set($events.map(e => e.kind)),
) )
@@ -149,7 +152,14 @@
View Members ({$members.length}) View Members ({$members.length})
</Button> </Button>
</li> </li>
{#if $relay?.pubkey} {#if $userIsAdmin}
<li>
<Link external href="https://landlubber.coracle.social">
<Icon icon={Tuning2} />
Manage Space
</Link>
</li>
{:else if $relay?.pubkey}
<li> <li>
<Link href={makeChatPath([$relay.pubkey])}> <Link href={makeChatPath([$relay.pubkey])}>
<Icon icon={Letter} /> <Icon icon={Letter} />
@@ -216,8 +226,8 @@
<div class="h-2"></div> <div class="h-2"></div>
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader> <SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
{/if} {/if}
{#each $userRooms as room, i (room)} {#each $userRooms as h, i (h)}
<MenuSpaceRoomItem {replaceState} notify {url} {room} /> <MenuSpaceRoomItem {replaceState} notify {url} {h} />
{/each} {/each}
{#if $otherRooms.length > 0} {#if $otherRooms.length > 0}
<div class="h-2"></div> <div class="h-2"></div>
@@ -229,8 +239,8 @@
{/if} {/if}
</SecondaryNavHeader> </SecondaryNavHeader>
{/if} {/if}
{#each $otherRooms as room, i (room)} {#each $otherRooms as h, i (h)}
<MenuSpaceRoomItem {replaceState} {url} {room} /> <MenuSpaceRoomItem {replaceState} {url} {h} />
{/each} {/each}
{#if $canCreateRoom} {#if $canCreateRoom}
<SecondaryNavItem {replaceState} onclick={addRoom}> <SecondaryNavItem {replaceState} onclick={addRoom}>
+5 -24
View File
@@ -1,43 +1,24 @@
<script lang="ts"> <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 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 {makeRoomPath} from "@app/util/routes"
import {deriveChannel} from "@app/core/state"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
interface Props { interface Props {
url: any url: any
room: any h: any
notify?: boolean notify?: boolean
replaceState?: 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 path = makeRoomPath(url, h)
const channel = deriveChannel(url, room)
</script> </script>
<SecondaryNavItem <SecondaryNavItem
href={path} href={path}
{replaceState} {replaceState}
notification={notify ? $notifications.has(path) : false}> notification={notify ? $notifications.has(path) : false}>
{#if $channel?.picture} <RoomNameWithImage {url} {h} />
{@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>
</SecondaryNavItem> </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>
+8 -2
View File
@@ -4,9 +4,15 @@
import Dialog from "@lib/components/Dialog.svelte" import Dialog from "@lib/components/Dialog.svelte"
import {modal, clearModals} from "@app/util/modal" import {modal, clearModals} from "@app/util/modal"
const closeModals = () => {
if ($modal && !$modal.options.noEscape) {
clearModals()
}
}
const onKeyDown = (e: any) => { const onKeyDown = (e: any) => {
if (e.code === "Escape" && e.target === document.body) { if (e.code === "Escape" && e.target === document.body) {
clearModals() closeModals()
} }
} }
@@ -27,7 +33,7 @@
instance = mount(wrapper as any, { instance = mount(wrapper as any, {
target: element, target: element,
props: { props: {
onClose: clearModals, onClose: closeModals,
children: createRawSnippet(() => ({ children: createRawSnippet(() => ({
render: () => "<div></div>", render: () => "<div></div>",
setup: (target: Element) => { setup: (target: Element) => {
+2 -10
View File
@@ -7,9 +7,7 @@
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte" import ChatEnable from "@app/components/ChatEnable.svelte"
import MenuSpaces from "@app/components/MenuSpaces.svelte"
import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte" import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte"
import MenuSettings from "@app/components/MenuSettings.svelte" import MenuSettings from "@app/components/MenuSettings.svelte"
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte" import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
@@ -31,9 +29,6 @@
const {children}: Props = $props() const {children}: Props = $props()
const showSpacesMenu = () =>
$userSpaceUrls.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd)
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls}) const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
const showSettingsMenu = () => pushModal(MenuSettings) const showSettingsMenu = () => pushModal(MenuSettings)
@@ -118,7 +113,7 @@
<div <div
class="border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden"> 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="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"> <PrimaryNavItem title="Home" href="/home">
<Avatar icon={HomeSmile} class="!h-10 !w-10" /> <Avatar icon={HomeSmile} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
@@ -129,10 +124,7 @@
<Avatar icon={Letter} class="!h-10 !w-10" /> <Avatar icon={Letter} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
{#if PLATFORM_RELAYS.length !== 1} {#if PLATFORM_RELAYS.length !== 1}
<PrimaryNavItem <PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
title="Spaces"
onclick={showSpacesMenu}
notification={anySpaceNotifications}>
<Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" /> <Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
{/if} {/if}
@@ -1,24 +1,19 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte" import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
import {encodeRelay} from "@app/core/state" import {makeSpacePath, goToSpace} from "@app/util/routes"
import {makeSpacePath} from "@app/util/routes"
import {lastPageBySpaceUrl} from "@app/util/history"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
const {url} = $props() const {url} = $props()
const path = makeSpacePath(url) const onClick = () => goToSpace(url)
const onClick = () => goto(lastPageBySpaceUrl.get(encodeRelay(url)) || path)
</script> </script>
<PrimaryNavItem <PrimaryNavItem
onclick={onClick} onclick={onClick}
title={displayRelayUrl(url)} title={displayRelayUrl(url)}
class="tooltip-right" class="tooltip-right"
notification={$notifications.has(path)}> notification={$notifications.has(makeSpacePath(url))}>
<SpaceAvatar {url} /> <SpaceAvatar {url} />
</PrimaryNavItem> </PrimaryNavItem>
+19 -8
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames"
import {onMount} from "svelte" import {onMount} from "svelte"
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib" import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
@@ -134,10 +135,15 @@
<button <button
type="button" type="button"
data-tip={tooltip} data-tip={tooltip}
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full text-xs font-normal {reactionClass}" class={cx(
class:tooltip={!noTooltip && !isMobile} reactionClass,
class:btn-neutral={!isOwn} "flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full text-xs font-normal",
class:btn-primary={isOwn}> {
tooltip: !noTooltip && !isMobile,
"border-neutral-content/20": !isOwn,
"btn-primary": isOwn,
},
)}>
<Reaction event={zaps[0].request} /> <Reaction event={zaps[0].request} />
<span>{amount}</span> <span>{amount}</span>
</button> </button>
@@ -151,10 +157,15 @@
<button <button
type="button" type="button"
data-tip={tooltip} data-tip={tooltip}
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full font-normal {reactionClass}" class={cx(
class:tooltip={!noTooltip && !isMobile} reactionClass,
class:btn-neutral={!isOwn} "flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal",
class:btn-primary={isOwn} {
tooltip: !noTooltip && !isMobile,
"border-neutral-content/20": !isOwn,
"btn-primary": isOwn,
},
)}
onclick={stopPropagation(preventDefault(onClick))}> onclick={stopPropagation(preventDefault(onClick))}>
<Reaction event={events[0]} /> <Reaction event={events[0]} />
{#if events.length > 1} {#if events.length > 1}
@@ -16,13 +16,14 @@
type Props = { type Props = {
url?: string url?: string
room?: string h?: string
content?: string content?: string
onEscape?: () => void
onEditPrevious?: () => void onEditPrevious?: () => void
onSubmit: (event: EventContent) => 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 const autofocus = !isMobile
@@ -34,6 +35,10 @@
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "") editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
const handleKeyDown = async (event: KeyboardEvent) => { const handleKeyDown = async (event: KeyboardEvent) => {
if (event.key === "Escape") {
onEscape?.()
}
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) { if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
onEditPrevious?.() onEditPrevious?.()
} }
@@ -74,7 +79,7 @@
}) })
</script> </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"> <div class="join">
<Button <Button
data-tip="Add an image" data-tip="Add an image"
@@ -90,7 +95,7 @@
<Tippy <Tippy
bind:popover bind:popover
component={ComposeMenu} component={ComposeMenu}
props={{url, room, onClick: hidePopover}} props={{url, h, onClick: hidePopover}}
params={{trigger: "manual", interactive: true}}> params={{trigger: "manual", interactive: true}}>
<Button <Button
data-tip="More options" data-tip="More options"
@@ -12,10 +12,10 @@
</script> </script>
<div <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> transition:slide>
<p class="text-primary">Editing message</p> <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} /> <Icon icon={CloseCircle} />
</Button> </Button>
</div> </div>
+28 -162
View File
@@ -1,181 +1,47 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {uniqBy, nth} from "@welshman/lib" import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util" import {displayRelayUrl} 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 AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.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 Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import IconPickerButton from "@lib/components/IconPickerButton.svelte" import RoomForm from "@app/components/RoomForm.svelte"
import {hasNip29, loadChannel} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {pushToast} from "@app/util/toast"
import {uploadFile} from "@app/core/commands"
const {url} = $props() const {url} = $props()
const room = makeRoomMeta()
const relay = deriveRelay(url)
const back = () => history.back() const back = () => history.back()
const tryCreate = async () => { const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, room.h))
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
}
</script> </script>
<form class="column gap-4" onsubmit={preventDefault(create)}> <RoomForm {url} {onsubmit}>
<ModalHeader> {#snippet header()}
{#snippet title()} <ModalHeader>
<div>Create a Room</div> {#snippet title()}
{/snippet} <div>Create a Room</div>
{#snippet info()}
<div>
On <span class="text-primary">{displayRelayUrl(url)}</span>
</div>
{/snippet}
</ModalHeader>
{#if hasNip29($relay)}
<FieldInline>
{#snippet label()}
<p>Room Name</p>
{/snippet} {/snippet}
{#snippet input()} {#snippet info()}
<label class="input input-bordered flex w-full items-center gap-2"> <div>
<Icon icon={Hashtag} /> On <span class="text-primary">{displayRelayUrl(url)}</span>
<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>
</div> </div>
</div> {/snippet}
</div> </ModalHeader>
{:else} {/snippet}
<p class="bg-alt card2 row-2"> {#snippet footer({loading})}
<Icon icon={Danger} /> <ModalFooter>
This relay does not support creating rooms. <Button class="btn btn-link" onclick={back}>
</p> <Icon icon={AltArrowLeft} />
{/if} Go back
<ModalFooter> </Button>
<Button class="btn btn-link" onclick={back}> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Icon icon={AltArrowLeft} /> <Spinner {loading}>Create Room</Spinner>
Go back <Icon icon={AltArrowRight} />
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={!name || loading || !hasNip29($relay)}> </ModalFooter>
<Spinner {loading}>Create Room</Spinner> {/snippet}
<Icon icon={AltArrowRight} /> </RoomForm>
</Button>
</ModalFooter>
</form>
+82
View File
@@ -0,0 +1,82 @@
<script lang="ts">
import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util"
import {deleteRoom, waitForThunkError, repository} from "@welshman/app"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
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 Confirm from "@lib/components/Confirm.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"
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 back = () => history.back()
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, h))
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))
}
},
})
</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>
<div class="flex gap-2">
<Button class="btn btn-outline btn-error" onclick={startDelete}>
<Icon icon={TrashBin2} />
<span class="hidden md:inline">Delete Room</span>
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Save Changes</Spinner>
</Button>
</div>
</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>
@@ -23,14 +23,14 @@
import ThunkFailure from "@app/components/ThunkFailure.svelte" import ThunkFailure from "@app/components/ThunkFailure.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ChannelItemZapButton from "@app/components/ChannelItemZapButton.svelte" import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte"
import ChannelItemEmojiButton from "@app/components/ChannelItemEmojiButton.svelte" import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte"
import ChannelItemMenuButton from "@app/components/ChannelItemMenuButton.svelte" import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
import ChannelItemMenuMobile from "@app/components/ChannelItemMenuMobile.svelte" import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
import ChannelItemContent from "@app/components/ChannelItemContent.svelte" import RoomItemContent from "@app/components/RoomItemContent.svelte"
import {colors, ENABLE_ZAPS, deriveEventsForUrl} from "@app/core/state" import {colors, ENABLE_ZAPS, deriveEventsForUrl} from "@app/core/state"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands" 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" import {pushModal} from "@app/util/modal"
interface Props { interface Props {
@@ -53,7 +53,7 @@
onEdit, onEdit,
}: Props = $props() }: Props = $props()
const path = getChannelItemPath(url, event) const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const today = formatTimestampAsDate(now()) const today = formatTimestampAsDate(now())
const profile = deriveProfile(event.pubkey, [url]) const profile = deriveProfile(event.pubkey, [url])
@@ -65,7 +65,7 @@
const reply = () => replyTo!(event) const reply = () => replyTo!(event)
const edit = canEdit(event) ? () => onEdit(event) : undefined 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}) const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
@@ -105,7 +105,7 @@
</div> </div>
{/if} {/if}
<div class:mt-2={showPubkey && event.kind !== MESSAGE}> <div class:mt-2={showPubkey && event.kind !== MESSAGE}>
<ChannelItemContent {url} {event} /> <RoomItemContent {url} {event} />
{#if thunk} {#if thunk}
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" /> <ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
{/if} {/if}
@@ -142,9 +142,9 @@
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all" class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
class:group-hover:opacity-100={!isMobile}> class:group-hover:opacity-100={!isMobile}>
{#if ENABLE_ZAPS} {#if ENABLE_ZAPS}
<ChannelItemZapButton {url} {event} /> <RoomItemZapButton {url} {event} />
{/if} {/if}
<ChannelItemEmojiButton {url} {event} /> <RoomItemEmojiButton {url} {event} />
{#if replyTo} {#if replyTo}
<Button class="btn join-item btn-xs" onclick={reply}> <Button class="btn join-item btn-xs" onclick={reply}>
<Icon icon={Reply} size={4} /> <Icon icon={Reply} size={4} />
@@ -155,7 +155,7 @@
<Icon icon={Pen} size={4} /> <Icon icon={Pen} size={4} />
</Button> </Button>
{/if} {/if}
<ChannelItemMenuButton {url} {event} /> <RoomItemMenuButton {url} {event} />
</button> </button>
{/if} {/if}
</TapTarget> </TapTarget>
@@ -5,11 +5,11 @@
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import NoteContent from "@app/components/NoteContent.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 props: ComponentProps<typeof NoteContent> = $props()
const path = getChannelItemPath(props.url!, props.event) const path = getRoomItemPath(props.url!, props.event)
</script> </script>
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}> <div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
@@ -5,7 +5,7 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Tippy from "@lib/components/Tippy.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() const {url, event} = $props()
@@ -34,7 +34,7 @@
</Button> </Button>
<Tippy <Tippy
bind:popover bind:popover
component={ChannelItemMenu} component={RoomItemMenu}
props={{url, event, onClick}} props={{url, event, onClick}}
params={{trigger: "manual", interactive: true}} /> params={{trigger: "manual", interactive: true}} />
</div> </div>
@@ -17,7 +17,7 @@
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte" import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import {ENABLE_ZAPS} from "@app/core/state" import {ENABLE_ZAPS} from "@app/core/state"
import {publishReaction, canEnforceNip70} from "@app/core/commands" 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" import {pushModal} from "@app/util/modal"
type Props = { type Props = {
@@ -28,7 +28,7 @@
const {url, event, reply}: Props = $props() const {url, event, reply}: Props = $props()
const path = getChannelItemPath(url, event) const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -1,21 +1,21 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import Link from "@lib/components/Link.svelte" 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" import {makeSpacePath} from "@app/util/routes"
type Props = { type Props = {
room: string h: string
url: string url: string
class?: string class?: string
unstyled?: boolean 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> </script>
<Link href={path} class={cx(props.class, {"link-content bg-alt": !unstyled})}> <Link href={path} class={cx(props.class, {"link-content bg-alt": !unstyled})}>
#<ChannelName {room} {url} /> #<RoomName {h} {url} />
</Link> </Link>
+7
View File
@@ -0,0 +1,7 @@
<script lang="ts">
import {roomsById, makeRoomId} from "@app/core/state"
const {url, h} = $props()
</script>
{$roomsById.get(makeRoomId(url, h))?.name || h}
@@ -0,0 +1,26 @@
<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 RoomName from "@app/components/RoomName.svelte"
import {deriveRoom} from "@app/core/state"
interface Props {
url: any
h: any
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
</script>
{#if $room.picture}
{@const src = $room.picture}
<ImageIcon {src} alt="Room icon" />
{:else}
<Icon icon={Hashtag} />
{/if}
<div class="min-w-0 overflow-hidden text-ellipsis">
<RoomName {url} {h} />
</div>
+2 -2
View File
@@ -14,7 +14,7 @@
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte" import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {checkRelayAccess} from "@app/core/commands" import {attemptRelayAccess} from "@app/core/commands"
import {deriveSocket} from "@app/core/state" import {deriveSocket} from "@app/core/state"
type Props = { type Props = {
@@ -31,7 +31,7 @@
loading = true loading = true
try { try {
const message = await checkRelayAccess(url, claim) const message = await attemptRelayAccess(url, claim)
if (message) { if (message) {
return pushToast({theme: "error", message, timeout: 30_000}) return pushToast({theme: "error", message, timeout: 30_000})
+26 -6
View File
@@ -1,20 +1,25 @@
<script lang="ts"> <script lang="ts">
import Login from "@assets/icons/login-3.svg?dataurl" import Login from "@assets/icons/login-3.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.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 Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import SpaceCreateExternal from "@app/components/SpaceCreateExternal.svelte"
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte" import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
const startCreate = () => pushModal(SpaceCreateExternal) type Props = {
hideDiscover?: boolean
}
const {hideDiscover}: Props = $props()
const startJoin = () => pushModal(SpaceInviteAccept) const startJoin = () => pushModal(SpaceInviteAccept)
</script> </script>
<div class="column gap-4"> <div class="column gap-2">
<ModalHeader> <ModalHeader>
{#snippet title()} {#snippet title()}
<div>Add a Space</div> <div>Add a Space</div>
@@ -23,8 +28,23 @@
<div>Spaces are places where communities come together to work, play, and hang out.</div> <div>Spaces are places where communities come together to work, play, and hang out.</div>
{/snippet} {/snippet}
</ModalHeader> </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}> <Button onclick={startJoin}>
<CardButton class="btn-primary"> <CardButton class={hideDiscover ? "btn-primary" : "btn-neutral"}>
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Login} size={7} /></div> <div><Icon icon={Login} size={7} /></div>
{/snippet} {/snippet}
@@ -36,7 +56,7 @@
{/snippet} {/snippet}
</CardButton> </CardButton>
</Button> </Button>
<Button onclick={startCreate}> <Link href="/spaces/create">
<CardButton class="btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div> <div><Icon icon={AddCircle} size={7} /></div>
@@ -48,5 +68,5 @@
<div>Just a few questions and you'll be on your way.</div> <div>Just a few questions and you'll be on your way.</div>
{/snippet} {/snippet}
</CardButton> </CardButton>
</Button> </Link>
</div> </div>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {parse, renderAsHtml} from "@welshman/content" import {parse, renderAsHtml} from "@welshman/content"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -14,7 +15,7 @@
const {url, error} = $props() const {url, error} = $props()
const back = () => history.back() const back = () => goto("/home")
const requestAccess = () => pushModal(SpaceAccessRequest, {url}) const requestAccess = () => pushModal(SpaceAccessRequest, {url})
</script> </script>
@@ -37,7 +38,7 @@
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go Home
</Button> </Button>
<Button type="submit" class="btn btn-primary"> <Button type="submit" class="btn btn-primary">
Request Access Request Access
+4 -2
View File
@@ -8,7 +8,6 @@
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import SpaceVisitConfirm, {confirmSpaceVisit} from "@app/components/SpaceVisitConfirm.svelte" import SpaceVisitConfirm, {confirmSpaceVisit} from "@app/components/SpaceVisitConfirm.svelte"
@@ -49,7 +48,10 @@
</ModalHeader> </ModalHeader>
<div class="m-auto flex flex-col gap-4"> <div class="m-auto flex flex-col gap-4">
{#if loading} {#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} {:else if error}
<p>Oops! We ran into some problems:</p> <p>Oops! We ran into some problems:</p>
<p class="card2 bg-alt">{error}</p> <p class="card2 bg-alt">{error}</p>
-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
+9 -4
View File
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {sleep, nthEq} from "@welshman/lib" import {sleep} from "@welshman/lib"
import {request} from "@welshman/net" 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 LinkRound from "@assets/icons/link-round.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl" import Copy from "@assets/icons/copy.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -13,10 +13,12 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import QRCode from "@app/components/QRCode.svelte" import QRCode from "@app/components/QRCode.svelte"
import {clip} from "@app/util/toast" 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 {url} = $props()
const authError = deriveRelayAuthError(url)
const back = () => history.back() const back = () => history.back()
const copyInvite = () => clip(invite) const copyInvite = () => clip(invite)
@@ -38,12 +40,13 @@
request({ request({
relays: [url], relays: [url],
autoClose: true, autoClose: true,
signal: AbortSignal.timeout(3000),
filters: [{kinds: [RELAY_INVITE]}], filters: [{kinds: [RELAY_INVITE]}],
}), }),
sleep(2000), sleep(2000),
]) ])
claim = event?.tags.find(nthEq(0, "claim"))?.[1] || "" claim = getTagValue("claim", event?.tags || []) || ""
loading = false loading = false
}) })
</script> </script>
@@ -65,6 +68,8 @@
<p class="center"> <p class="center">
<Spinner {loading}>Requesting an invite link...</Spinner> <Spinner {loading}>Requesting an invite link...</Spinner>
</p> </p>
{:else if $authError}
<p class="center">Oops! It looks like you're not a member of this relay.</p>
{:else} {:else}
<div class="flex flex-col items-center gap-6"> <div class="flex flex-col items-center gap-6">
<QRCode code={invite} /> <QRCode code={invite} />
+2 -20
View File
@@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {tryCatch, fromPairs} from "@welshman/lib"
import {isRelayUrl, normalizeRelayUrl} from "@welshman/util"
import {Pool, AuthStatus} from "@welshman/net" import {Pool, AuthStatus} from "@welshman/net"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {slideAndFade} from "@lib/transition" import {slideAndFade} from "@lib/transition"
@@ -19,6 +17,7 @@
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {attemptRelayAccess} from "@app/core/commands" import {attemptRelayAccess} from "@app/core/commands"
import {parseInviteLink} from "@app/core/state"
type Props = { type Props = {
invite: string invite: string
@@ -57,24 +56,7 @@
let loading = $state(false) let loading = $state(false)
const inviteData = $derived.by( const inviteData = $derived(parseInviteLink(invite))
() =>
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: ""}
}
}),
)
</script> </script>
<form class="column gap-4" onsubmit={preventDefault(join)}> <form class="column gap-4" onsubmit={preventDefault(join)}>
+1 -1
View File
@@ -28,7 +28,7 @@
try { try {
await removeSpaceMembership(url) await removeSpaceMembership(url)
await removeTrustedRelay(url) await removeTrustedRelay(url)
goto("/") goto("/home")
} finally { } finally {
loading = false loading = false
} }
+5 -5
View File
@@ -2,7 +2,7 @@
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue} from "@welshman/util" import {getTagValue} from "@welshman/util"
import Link from "@lib/components/Link.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 ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte" import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte" import EventActivity from "@app/components/EventActivity.svelte"
@@ -19,8 +19,8 @@
const {url, event, showRoom, showActivity}: Props = $props() const {url, event, showRoom, showActivity}: Props = $props()
const h = getTagValue("h", event.tags)
const path = makeThreadPath(url, event.id) const path = makeThreadPath(url, event.id)
const room = getTagValue("h", event.tags)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
@@ -31,9 +31,9 @@
</script> </script>
<div class="flex flex-grow flex-wrap justify-end gap-2"> <div class="flex flex-grow flex-wrap justify-end gap-2">
{#if room && showRoom} {#if h && showRoom}
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full"> <Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<ChannelName {room} {url} /> Posted in #<RoomName {h} {url} />
</Link> </Link>
{/if} {/if}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" /> <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
+4 -4
View File
@@ -18,10 +18,10 @@
type Props = { type Props = {
url: string url: string
room?: string h?: string
} }
const {url, room}: Props = $props() const {url, h}: Props = $props()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -57,8 +57,8 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
if (room) { if (h) {
tags.push(["h", room]) tags.push(["h", h])
} }
publishThunk({ publishThunk({
+7 -5
View File
@@ -6,7 +6,7 @@
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
import ThreadActions from "@app/components/ThreadActions.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" import {makeThreadPath} from "@app/util/routes"
type Props = { type Props = {
@@ -17,10 +17,12 @@
const {url, event}: Props = $props() const {url, event}: Props = $props()
const title = getTagValue("title", event.tags) const title = getTagValue("title", event.tags)
const room = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
</script> </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} {#if title}
<div class="flex w-full items-center justify-between gap-2"> <div class="flex w-full items-center justify-between gap-2">
<p class="text-xl">{title}</p> <p class="text-xl">{title}</p>
@@ -38,8 +40,8 @@
<span class="whitespace-nowrap py-1 text-sm opacity-75"> <span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by Posted by
<ProfileLink pubkey={event.pubkey} {url} /> <ProfileLink pubkey={event.pubkey} {url} />
{#if room} {#if h}
in <ChannelLink {url} {room} /> in <RoomLink {url} {h} />
{/if} {/if}
</span> </span>
<ThreadActions showActivity {url} {event} /> <ThreadActions showActivity {url} {event} />
+23 -72
View File
@@ -74,6 +74,7 @@ import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import { import {
pubkey, pubkey,
sign,
signer, signer,
session, session,
repository, repository,
@@ -84,7 +85,6 @@ import {
userRelaySelections, userRelaySelections,
userInboxRelaySelections, userInboxRelaySelections,
nip44EncryptToSelf, nip44EncryptToSelf,
loadRelay,
dropSession, dropSession,
tagEventForComment, tagEventForComment,
tagEventForQuote, tagEventForQuote,
@@ -104,8 +104,10 @@ import {
DEFAULT_BLOSSOM_SERVERS, DEFAULT_BLOSSOM_SERVERS,
userSpaceUrls, userSpaceUrls,
userSettingsValues, userSettingsValues,
getSetting,
userInboxRelays, userInboxRelays,
userGroupSelections, userGroupSelections,
shouldIgnoreError,
} from "@app/core/state" } from "@app/core/state"
import {loadAlertStatuses} from "@app/core/requests" import {loadAlertStatuses} from "@app/core/requests"
import {platform, platformName, getPushInfo} from "@app/util/push" import {platform, platformName, getPushInfo} from "@app/util/push"
@@ -191,11 +193,11 @@ export const removeSpaceMembership = async (url: string) => {
return publishThunk({event, relays}) 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 list = get(userGroupSelections) || makeList({kind: ROOMS})
const newTags = [ const newTags = [
["r", url], ["r", url],
["group", room, url], ["group", h, url],
] ]
const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf)
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)]) 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}) 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 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 event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)]) const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
@@ -250,57 +252,15 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
// Relay access // 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) => { export const canEnforceNip70 = async (url: string) => {
const socket = Pool.get().get(url) 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 return socket.auth.status !== AuthStatus.None
} }
export const checkRelayAccess = async (url: string, claim = "") => { export const attemptRelayAccess = 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) => {
const socket = Pool.get().get(url) const socket = Pool.get().get(url)
socket.attemptToOpen() socket.attemptToOpen()
@@ -313,38 +273,29 @@ export const checkRelayConnection = async (url: string) => {
if (socket.status !== SocketStatus.Open) { if (socket.status !== SocketStatus.Open) {
return `Failed to connect` return `Failed to connect`
} }
}
export const checkRelayAuth = async (url: string) => { await socket.auth.attemptAuth(sign)
const socket = Pool.get().get(url)
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
await attemptAuth(url)
// Only raise an error if it's not a timeout. // 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 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) { if (socket.auth.details) {
return `Failed to authenticate (${socket.auth.details})` return `Failed to authenticate (${socket.auth.details})`
} else { } else {
return `Failed to authenticate (${last(socket.auth.status.split(":"))})` return `Failed to authenticate (${last(socket.auth.status.split(":"))})`
} }
} }
}
export const attemptRelayAccess = async (url: string, claim = "") => { const thunk = publishJoinRequest({url, claim})
const checks = [ const error = await waitForThunkError(thunk)
() => checkRelayConnection(url),
() => checkRelayAccess(url, claim),
() => checkRelayAuth(url),
]
for (const check of checks) { if (shouldIgnoreError(error)) return
const error = await check()
if (error) { if (claim) {
return error 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 // 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 thunk = await publishAlert(params as AlertParams)
const error = await waitForThunkError(thunk) const error = await waitForThunkError(thunk)
@@ -599,7 +550,7 @@ export const createDmAlert = async () => {
// Settings // Settings
export const makeSettings = async (params: Partial<SettingsValues>) => { 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 content = await signer.get().nip44.encrypt(pubkey.get()!, json)
const tags = [["d", SETTINGS]] 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()}) publishThunk({event: await makeSettings(params), relays: Router.get().FromUser().getUrls()})
export const addTrustedRelay = async (url: string) => 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) => 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 // Join request
+4 -2
View File
@@ -248,13 +248,15 @@ export const makeCalendarFeed = ({
// Domain specific // Domain specific
export const loadAlerts = (pubkey: string) => export const loadAlerts = (pubkey: string) =>
load({ request({
autoClose: true,
relays: [NOTIFIER_RELAY], relays: [NOTIFIER_RELAY],
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID], authors: [pubkey]}], filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID], authors: [pubkey]}],
}) })
export const loadAlertStatuses = (pubkey: string) => export const loadAlertStatuses = (pubkey: string) =>
load({ request({
autoClose: true,
relays: [NOTIFIER_RELAY], relays: [NOTIFIER_RELAY],
filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}], filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}],
}) })
+175 -166
View File
@@ -4,6 +4,8 @@ import {get, derived, writable} from "svelte/store"
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import { import {
on, on,
gt,
max,
spec, spec,
call, call,
first, first,
@@ -17,13 +19,13 @@ import {
pushToMapKey, pushToMapKey,
shuffle, shuffle,
parseJson, parseJson,
fromPairs,
memoize, memoize,
addToMapKey, addToMapKey,
identity, identity,
groupBy, groupBy,
always, always,
tryCatch, tryCatch,
fromPairs,
} from "@welshman/lib" } from "@welshman/lib"
import type {Socket} from "@welshman/net" import type {Socket} from "@welshman/net"
import { import {
@@ -35,14 +37,7 @@ import {
SocketEvent, SocketEvent,
netContext, netContext,
} from "@welshman/net" } from "@welshman/net"
import { import {collection, custom, throttled, deriveEvents, deriveEventsMapped} from "@welshman/store"
collection,
custom,
throttled,
deriveEvents,
deriveEventsMapped,
withGetter,
} from "@welshman/store"
import {isKindFeed, findFeed} from "@welshman/feeds" import {isKindFeed, findFeed} from "@welshman/feeds"
import { import {
ALERT_ANDROID, ALERT_ANDROID,
@@ -88,7 +83,6 @@ import {
getPubkeyTagValues, getPubkeyTagValues,
getRelaysFromList, getRelaysFromList,
getRelayTagValues, getRelayTagValues,
getTag,
getTagValue, getTagValue,
getTagValues, getTagValues,
isRelayUrl, isRelayUrl,
@@ -97,8 +91,18 @@ import {
readList, readList,
RelayMode, RelayMode,
verifyEvent, verifyEvent,
readRoomMeta,
makeRoomMeta,
ManagementMethod,
} from "@welshman/util"
import type {
TrustedEvent,
RelayProfile,
PublishedList,
PublishedRoomMeta,
List,
Filter,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, RelayProfile, PublishedList, List, Filter} from "@welshman/util"
import {decrypt} from "@welshman/signer" import {decrypt} from "@welshman/signer"
import {routerContext, Router} from "@welshman/router" import {routerContext, Router} from "@welshman/router"
import { import {
@@ -112,6 +116,7 @@ import {
userFollows, userFollows,
ensurePlaintext, ensurePlaintext,
thunks, thunks,
sign,
signer, signer,
makeOutboxLoader, makeOutboxLoader,
appContext, appContext,
@@ -122,6 +127,7 @@ import {
deriveRelay, deriveRelay,
makeUserData, makeUserData,
makeUserLoader, makeUserLoader,
manageRelay,
} from "@welshman/app" } from "@welshman/app"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
@@ -143,7 +149,7 @@ export const INDEXER_RELAYS = fromCsv(import.meta.env.VITE_INDEXER_RELAYS)
export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS) export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS)
export const PLATFORM_URL = window.location.origin export const PLATFORM_URL = import.meta.env.VITE_PLATFORM_URL
export const PLATFORM_TERMS = import.meta.env.VITE_PLATFORM_TERMS export const PLATFORM_TERMS = import.meta.env.VITE_PLATFORM_TERMS
@@ -232,31 +238,29 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
) )
} }
export const getUrlsForEvent = withGetter( export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thunks]) => {
derived([trackerStore, thunks], ([$tracker, $thunks]) => { const getThunksByEventId = memoize(() => {
const getThunksByEventId = memoize(() => { const thunksByEventId = new Map<string, Thunk[]>()
const thunksByEventId = new Map<string, Thunk[]>()
for (const thunk of $thunks) { for (const thunk of $thunks) {
pushToMapKey(thunksByEventId, thunk.event.id, thunk) 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)
} }
}),
) 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[]) => { export const getEventsForUrl = (url: string, filters: Filter[]) => {
const ids = uniq([ const ids = uniq([
@@ -310,20 +314,9 @@ if (ENABLE_ZAPS) {
REACTION_KINDS.push(ZAP_RESPONSE) REACTION_KINDS.push(ZAP_RESPONSE)
} }
export const MESSAGE_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE] export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD]
export const MESSAGE_FILTER = {kinds: MESSAGE_KINDS} export const MESSAGE_KINDS = [...CONTENT_KINDS, MESSAGE]
export const COMMENT_FILTER = makeCommentFilter(MESSAGE_KINDS)
export const MEMBERSHIP_KINDS = [
ROOM_ADD_MEMBER,
ROOM_REMOVE_MEMBER,
RELAY_ADD_MEMBER,
RELAY_REMOVE_MEMBER,
]
export const MEMBERSHIP_FILTER = {kinds: MEMBERSHIP_KINDS}
// Settings // Settings
@@ -385,15 +378,13 @@ export const userSettings = makeUserData({
export const loadUserSettings = makeUserLoader(loadSettings) export const loadUserSettings = makeUserLoader(loadSettings)
export const userSettingsValues = withGetter( export const userSettingsValues = derived(userSettings, $s => $s?.values || defaultSettings)
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 // 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 // Relays that mostly send restricted responses to requests and events
@@ -418,21 +409,19 @@ export type Alert = {
tags: string[][] tags: string[][]
} }
export const alerts = withGetter( export const alerts = deriveEventsMapped<Alert>(repository, {
deriveEventsMapped<Alert>(repository, { filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}], itemToEvent: item => item.event,
itemToEvent: item => item.event, eventToItem: async event => {
eventToItem: async event => { const $signer = signer.get()
const $signer = signer.get()
if ($signer) { if ($signer) {
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content)) const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
return {event, tags} return {event, tags}
} }
}, },
}), })
)
export const getAlertFeed = (alert: Alert) => export const getAlertFeed = (alert: Alert) =>
tryCatch(() => JSON.parse(getTagValue("feed", alert.tags)!)) tryCatch(() => JSON.parse(getTagValue("feed", alert.tags)!))
@@ -452,21 +441,19 @@ export type AlertStatus = {
tags: string[][] tags: string[][]
} }
export const alertStatuses = withGetter( export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
deriveEventsMapped<AlertStatus>(repository, { filters: [{kinds: [ALERT_STATUS]}],
filters: [{kinds: [ALERT_STATUS]}], itemToEvent: item => item.event,
itemToEvent: item => item.event, eventToItem: async event => {
eventToItem: async event => { const $signer = signer.get()
const $signer = signer.get()
if ($signer) { if ($signer) {
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content)) const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
return {event, tags} return {event, tags}
} }
}, },
}), })
)
export const deriveAlertStatus = (address: string) => export const deriveAlertStatus = (address: string) =>
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address)) derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
@@ -541,62 +528,53 @@ export const chatSearch = derived(chats, $chats =>
}), }),
) )
// Channels // Rooms
export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]}) export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]})
export type Channel = { export type Room = PublishedRoomMeta & {
id: string id: string
url: 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) => export const hasNip29 = (relay?: RelayProfile) =>
relay?.supported_nips?.map?.(String)?.includes?.("29") relay?.supported_nips?.map?.(String)?.includes?.("29")
export const channels = derived( export const roomMetas = deriveEventsMapped<PublishedRoomMeta>(repository, {
[deriveEvents(repository, {filters: [{kinds: [ROOM_META, ROOM_DELETE]}]}), getUrlsForEvent], filters: [{kinds: [ROOM_META]}],
([$events, $getUrlsForEvent]) => { itemToEvent: item => item.event,
const result = new Map<string, Channel>() eventToItem: readRoomMeta,
})
for (const event of sortBy(e => e.created_at, $events)) { export const roomDeletes = deriveEvents(repository, {
for (const url of $getUrlsForEvent(event.id)) { filters: [{kinds: [ROOM_DELETE]}],
if (event.kind === ROOM_META) { })
const meta = fromPairs(event.tags)
const room = meta.d
if (room) { export const rooms = derived(
const id = makeChannelId(url, room) [roomMetas, roomDeletes, getUrlsForEvent],
([$roomMetas, $roomDeletes, $getUrlsForEvent]) => {
const result = new Map<string, Room>()
const deletedByH = new Map<string, number>()
result.set(id, { for (const event of $roomDeletes) {
id, for (const h of getTagValues("h", event.tags)) {
url, deletedByH.set(h, max([deletedByH.get(h), event.created_at]))
room, }
event, }
name: meta.name || room,
closed: Boolean(getTag("closed", event.tags)),
private: Boolean(getTag("private", event.tags)),
picture: meta.picture,
about: meta.about,
})
}
}
if (event.kind === ROOM_DELETE) { for (const meta of $roomMetas) {
for (const room of getTagValues("h", event.tags)) { if (gt(deletedByH.get(meta.h), meta.event.created_at)) {
result.delete(makeChannelId(url, room)) continue
} }
}
for (const url of $getUrlsForEvent(meta.event.id)) {
const id = makeRoomId(url, meta.h)
result.set(id, {...meta, url, id})
} }
} }
@@ -604,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 { export const {
indexStore: channelsById, indexStore: roomsById,
deriveItem: _deriveChannel, deriveItem: _deriveRoom,
loadItem: _loadChannel, loadItem: _loadRoom,
} = collection({ } = collection({
name: "channels", name: "rooms",
store: channels, store: rooms,
getKey: channel => channel.id, getKey: room => room.id,
load: async (id: string) => { load: async (id: string) => {
const [url, room] = splitChannelId(id) const [url, h] = splitRoomId(id)
await load({ await load({
relays: [url], 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) => export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
channelsById.get().get(makeChannelId(url, room))?.name || room
export const roomComparator = (url: string) => (room: string) =>
displayChannel(url, room).toLowerCase()
// User space/room selections // User space/room selections
@@ -696,9 +672,9 @@ export const getSpaceRoomsFromGroupSelections = (
) => { ) => {
const rooms: string[] = [] const rooms: string[] = []
for (const [_, room, relay] of getGroupTags(getListTags($groupSelections))) { for (const [_, h, relay] of getGroupTags(getListTags($groupSelections))) {
if (url === relay) { if (url === relay) {
rooms.push(room) rooms.push(h)
} }
} }
@@ -715,12 +691,12 @@ export const loadUserGroupSelections = makeUserLoader(loadGroupSelections)
export const userSpaceUrls = derived(userGroupSelections, getSpaceUrlsFromGroupSelections) export const userSpaceUrls = derived(userGroupSelections, getSpaceUrlsFromGroupSelections)
export const deriveUserRooms = (url: string) => export const deriveUserRooms = (url: string) =>
derived([userGroupSelections, channelsById], ([$userGroupSelections, $channelsById]) => { derived([userGroupSelections, roomsById], ([$userGroupSelections, $roomsById]) => {
const rooms: string[] = [] const rooms: string[] = []
for (const room of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) { for (const h of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
if ($channelsById.has(makeChannelId(url, room))) { if ($roomsById.has(makeRoomId(url, h))) {
rooms.push(room) rooms.push(h)
} }
} }
@@ -728,12 +704,12 @@ export const deriveUserRooms = (url: string) =>
}) })
export const deriveOtherRooms = (url: string) => export const deriveOtherRooms = (url: string) =>
derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) => { derived([deriveUserRooms(url), roomsByUrl], ([$userRooms, $roomsByUrl]) => {
const rooms: string[] = [] const rooms: string[] = []
for (const {room} of $channelsByUrl.get(url) || []) { for (const {h} of $roomsByUrl.get(url) || []) {
if (!$userRooms.includes(room)) { if (!$userRooms.includes(h)) {
rooms.push(room) rooms.push(h)
} }
} }
@@ -776,11 +752,11 @@ export const deriveSpaceMembers = (url: string) =>
}, },
) )
export const deriveRoomMembers = (url: string, room: string) => export const deriveRoomMembers = (url: string, h: string) =>
derived( derived(
deriveEventsForUrl(url, [ deriveEventsForUrl(url, [
{kinds: [ROOM_MEMBERS], "#d": [room]}, {kinds: [ROOM_MEMBERS], "#d": [h]},
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]}, {kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
]), ]),
$events => { $events => {
const membersEvent = $events.find(spec({kind: ROOM_MEMBERS})) const membersEvent = $events.find(spec({kind: ROOM_MEMBERS}))
@@ -811,8 +787,8 @@ export const deriveRoomMembers = (url: string, room: string) =>
}, },
) )
export const deriveRoomAdmins = (url: string, room: string) => export const deriveRoomAdmins = (url: string, h: string) =>
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [room]}]), $events => { derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [h]}]), $events => {
const adminsEvent = first($events) const adminsEvent = first($events)
if (adminsEvent) { if (adminsEvent) {
@@ -841,7 +817,7 @@ export const deriveUserSpaceMembershipStatus = (url: string) =>
const isMember = $members.includes($pubkey) const isMember = $members.includes($pubkey)
for (const event of $events) { for (const event of $events) {
if (event.pubkey !== $pubkey) { if (!getPubkeyTagValues(event.tags).includes($pubkey!)) {
continue continue
} }
@@ -858,18 +834,18 @@ export const deriveUserSpaceMembershipStatus = (url: string) =>
}, },
) )
export const deriveUserRoomMembershipStatus = (url: string, room: string) => export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
derived( derived(
[ [
pubkey, pubkey,
deriveRoomMembers(url, room), deriveRoomMembers(url, h),
deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [room]}]), deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]),
], ],
([$pubkey, $members, $events]) => { ([$pubkey, $members, $events]) => {
const isMember = $members.includes($pubkey) const isMember = $members.includes($pubkey)
for (const event of $events) { for (const event of $events) {
if (event.pubkey !== $pubkey) { if (!getPubkeyTagValues(event.tags).includes($pubkey!)) {
continue continue
} }
@@ -896,8 +872,18 @@ export const deriveUserCanCreateRoom = (url: string) =>
}, },
) )
export const deriveUserIsRoomAdmin = (url: string, room: string) => export const deriveUserIsRoomAdmin = (url: string, h: string) =>
derived([pubkey, deriveRoomAdmins(url, room)], ([$pubkey, $admins]) => $admins.includes($pubkey!)) derived([pubkey, deriveRoomAdmins(url, h)], ([$pubkey, $admins]) => $admins.includes($pubkey!))
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
}
// Other utils // Other utils
@@ -988,13 +974,19 @@ export const deriveTimeout = (timeout: number) => {
return derived(store, identity) 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 = "") => { export const deriveRelayAuthError = (url: string, claim = "") => {
const $signer = signer.get()
const socket = Pool.get().get(url)
const stripPrefix = (m: string) => m.replace(/^\w+: /, "") const stripPrefix = (m: string) => m.replace(/^\w+: /, "")
// Kick off the auth process // Kick off the auth process
socket.auth.attemptAuth($signer.sign) Pool.get().get(url).auth.attemptAuth(sign)
// Attempt to join the relay // Attempt to join the relay
const thunk = publishThunk({ const thunk = publishThunk({
@@ -1003,8 +995,8 @@ export const deriveRelayAuthError = (url: string, claim = "") => {
}) })
return derived( return derived(
[relaysMostlyRestricted, deriveSocket(url)], [thunk, relaysMostlyRestricted, deriveSocket(url)],
([$relaysMostlyRestricted, $socket]) => { ([$thunk, $relaysMostlyRestricted, $socket]) => {
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) { if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
return stripPrefix($socket.auth.details) return stripPrefix($socket.auth.details)
} }
@@ -1013,17 +1005,34 @@ export const deriveRelayAuthError = (url: string, claim = "") => {
return stripPrefix($relaysMostlyRestricted[url]) return stripPrefix($relaysMostlyRestricted[url])
} }
const error = getThunkError(thunk) const error = getThunkError($thunk)
if (error) { if (error) {
const isIgnored = error.startsWith("mute: ")
const isEmptyInvite = !claim && error.includes("invite code") 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" 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: ""}
}
})
+221 -126
View File
@@ -1,23 +1,12 @@
import {page} from "$app/stores" import {page} from "$app/stores"
import type {Unsubscriber} from "svelte/store" import type {Unsubscriber} from "svelte/store"
import {derived, get} from "svelte/store" import {derived, get} from "svelte/store"
import { import {partition, call, sortBy, assoc, chunk, sleep, identity, WEEK, ago} from "@welshman/lib"
partition,
call,
sortBy,
assoc,
chunk,
sleep,
now,
identity,
WEEK,
MONTH,
ago,
} from "@welshman/lib"
import { import {
getListTags, getListTags,
getRelayTagValues, getRelayTagValues,
WRAP, WRAP,
MESSAGE,
ROOM_META, ROOM_META,
ROOM_DELETE, ROOM_DELETE,
ROOM_ADMINS, ROOM_ADMINS,
@@ -26,7 +15,10 @@ import {
ROOM_REMOVE_MEMBER, ROOM_REMOVE_MEMBER,
ROOM_CREATE_PERMISSION, ROOM_CREATE_PERMISSION,
RELAY_MEMBERS, RELAY_MEMBERS,
RELAY_ADD_MEMBER,
RELAY_REMOVE_MEMBER,
isSignedEvent, isSignedEvent,
unionFilters,
} from "@welshman/util" } from "@welshman/util"
import type {Filter, TrustedEvent} from "@welshman/util" import type {Filter, TrustedEvent} from "@welshman/util"
import {request, load, pull} from "@welshman/net" import {request, load, pull} from "@welshman/net"
@@ -45,19 +37,23 @@ import {
repository, repository,
shouldUnwrap, shouldUnwrap,
hasNegentropy, hasNegentropy,
relaysByUrl,
} from "@welshman/app" } from "@welshman/app"
import { import {
MESSAGE_FILTER, MESSAGE_KINDS,
COMMENT_FILTER, CONTENT_KINDS,
MEMBERSHIP_FILTER,
INDEXER_RELAYS, INDEXER_RELAYS,
REACTION_KINDS,
loadSettings, loadSettings,
loadGroupSelections, loadGroupSelections,
userSpaceUrls, userSpaceUrls,
userGroupSelections,
bootstrapPubkeys, bootstrapPubkeys,
decodeRelay, decodeRelay,
getUrlsForEvent, getUrlsForEvent,
hasNip29,
getSpaceUrlsFromGroupSelections,
getSpaceRoomsFromGroupSelections,
makeCommentFilter,
} from "@app/core/state" } from "@app/core/state"
import {loadAlerts, loadAlertStatuses} from "@app/core/requests" import {loadAlerts, loadAlertStatuses} from "@app/core/requests"
import {hasBlossomSupport} from "@app/core/commands" import {hasBlossomSupport} from "@app/core/commands"
@@ -70,7 +66,7 @@ type PullOpts = {
signal: AbortSignal signal: AbortSignal
} }
const pullConservatively = ({relays, filters, signal}: PullOpts) => { const pullWithFallback = ({relays, filters, signal}: PullOpts) => {
const $getUrlsForEvent = get(getUrlsForEvent) const $getUrlsForEvent = get(getUrlsForEvent)
const [smart, dumb] = partition(hasNegentropy, relays) const [smart, dumb] = partition(hasNegentropy, relays)
const events = repository.query(filters, {shouldSort: false}).filter(isSignedEvent) const events = repository.query(filters, {shouldSort: false}).filter(isSignedEvent)
@@ -91,6 +87,20 @@ const pullConservatively = ({relays, filters, signal}: PullOpts) => {
return Promise.all(promises) return Promise.all(promises)
} }
const pullAndListen = ({relays, filters, signal}: PullOpts) => {
pullWithFallback({
relays,
signal,
filters: filters.map(f => ({limit: 100, ...f})),
})
request({
relays,
signal,
filters: unionFilters(filters).map(assoc("limit", 0)),
})
}
// Relays // Relays
const syncRelays = () => { const syncRelays = () => {
@@ -121,10 +131,80 @@ const syncRelays = () => {
// User data // User data
const syncUserSpaceMembership = (url: string) => {
const $pubkey = pubkey.get()
const controller = new AbortController()
if ($pubkey) {
pullAndListen({
relays: [url],
signal: controller.signal,
filters: [
{
kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, ROOM_CREATE_PERMISSION],
"#p": [$pubkey],
},
],
})
}
return () => controller.abort()
}
const syncUserRoomMembership = (url: string, h: string) => {
const $pubkey = pubkey.get()
const controller = new AbortController()
if ($pubkey) {
pullAndListen({
relays: [url],
signal: controller.signal,
filters: [
{
kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
"#p": [$pubkey],
"#h": [h],
},
],
})
}
return () => controller.abort()
}
const syncUserData = () => { const syncUserData = () => {
const unsubscribePubkey = pubkey.subscribe($pubkey => { const unsubscribersByKey = new Map<string, Unsubscriber>()
const unsubscribeGroupSelections = userGroupSelections.subscribe($l => {
const $pubkey = pubkey.get()
if ($pubkey) { if ($pubkey) {
loadRelaySelections($pubkey) const keys = new Set<string>()
for (const url of getSpaceUrlsFromGroupSelections($l)) {
if (!unsubscribersByKey.has(url)) {
unsubscribersByKey.set(url, syncUserSpaceMembership(url))
}
keys.add(url)
for (const h of getSpaceRoomsFromGroupSelections(url, $l)) {
const key = `${url}'${h}`
if (!unsubscribersByKey.has(key)) {
unsubscribersByKey.set(key, syncUserRoomMembership(url, h))
}
keys.add(key)
}
}
for (const [key, unsubscribe] of unsubscribersByKey.entries()) {
if (!keys.has(key)) {
unsubscribersByKey.delete(key)
unsubscribe()
}
}
} }
}) })
@@ -148,154 +228,175 @@ const syncUserData = () => {
// This isn't urgent, avoid clogging other stuff up // This isn't urgent, avoid clogging other stuff up
await sleep(1000) await sleep(1000)
for (const pk of pubkeys) { await Promise.all(
loadRelaySelections(pk).then(() => { pubkeys.map(async pk => {
loadGroupSelections(pk) await loadRelaySelections(pk)
loadProfile(pk) await loadGroupSelections(pk)
loadFollows(pk) await loadProfile(pk)
loadMutes(pk) await loadFollows(pk)
}) await loadMutes(pk)
} }),
)
}
})
const unsubscribePubkey = pubkey.subscribe($pubkey => {
if ($pubkey) {
loadRelaySelections($pubkey)
} }
}) })
return () => { return () => {
unsubscribePubkey() unsubscribersByKey.forEach(call)
unsubscribeGroupSelections()
unsubscribeSelections() unsubscribeSelections()
unsubscribeFollows() unsubscribeFollows()
unsubscribePubkey()
} }
} }
// Memberships // Spaces
const syncMembership = (url: string) => {
const controller = new AbortController()
const relayFilter = {kinds: [RELAY_MEMBERS, ROOM_CREATE_PERMISSION]}
const roomsFilter = {kinds: [ROOM_ADMINS, ROOM_MEMBERS, ROOM_META, ROOM_DELETE]}
// Load group metadata and member lists
pullConservatively({
relays: [url],
signal: controller.signal,
filters: [relayFilter, roomsFilter],
})
// Load historical data from up to a month ago for quick page loading
pullConservatively({
relays: [url],
signal: controller.signal,
filters: [MESSAGE_FILTER, COMMENT_FILTER, MEMBERSHIP_FILTER].map(assoc("since", ago(MONTH))),
})
// Listen for new events
request({
relays: [url],
signal: controller.signal,
filters: [relayFilter, roomsFilter, MESSAGE_FILTER, COMMENT_FILTER, MEMBERSHIP_FILTER].map(
assoc("since", now()),
),
})
return () => controller.abort()
}
const syncMemberships = () => {
const unsubscribersByUrl = new Map<string, Unsubscriber>()
const unsubscribeSpaceUrls = userSpaceUrls.subscribe(urls => {
// stop syncing removed spaces
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
if (!urls.includes(url)) {
unsubscribersByUrl.delete(url)
unsubscribe()
}
}
// Start syncing newly added spaces
for (const url of urls) {
if (!unsubscribersByUrl.has(url)) {
unsubscribersByUrl.set(url, syncMembership(url))
}
}
})
return () => {
Array.from(unsubscribersByUrl.values()).forEach(call)
unsubscribeSpaceUrls()
}
}
// Sync extra stuff for the current space
const syncSpace = (url: string) => { const syncSpace = (url: string) => {
const $pubkey = pubkey.get()
const controller = new AbortController() const controller = new AbortController()
// Load all membership changes for the current user pullAndListen({
if ($pubkey) {
pullConservatively({
relays: [url],
signal: controller.signal,
filters: [
{
kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
"#p": [$pubkey],
},
],
})
}
// Listen actively for all current membership changes, reports, reactions, zaps, etc
request({
relays: [url], relays: [url],
signal: controller.signal, signal: controller.signal,
filters: [ filters: [
{ {kinds: [RELAY_MEMBERS]},
kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER, ...REACTION_KINDS], {kinds: [ROOM_META, ROOM_DELETE]},
since: now(), {kinds: [ROOM_ADMINS, ROOM_MEMBERS]},
}, {kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]},
...MESSAGE_KINDS.map(kind => ({kinds: [kind]})),
makeCommentFilter(CONTENT_KINDS),
], ],
}) })
return () => controller.abort() return () => controller.abort()
} }
const syncCurrentSpace = () => { const syncSpaces = () => {
const unsubscribersByUrl = new Map<string, Unsubscriber>() const membershipUnsubscribersByUrl = new Map<string, Unsubscriber>()
const unsubscribeSpaceUrls = userSpaceUrls.subscribe(urls => {
// stop syncing removed spaces
for (const [url, unsubscribe] of membershipUnsubscribersByUrl.entries()) {
if (!urls.includes(url)) {
membershipUnsubscribersByUrl.delete(url)
unsubscribe()
}
}
// Start syncing newly added spaces
for (const url of urls) {
if (!membershipUnsubscribersByUrl.has(url)) {
membershipUnsubscribersByUrl.set(url, syncSpace(url))
}
}
})
const pageUnsubscribersByUrl = new Map<string, Unsubscriber>()
// Sync the space the user is currently visiting // Sync the space the user is currently visiting
const unsubscribePage = page.subscribe($page => { const unsubscribePage = page.subscribe($page => {
if ($page.params.relay) { if ($page.params.relay) {
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
if (!unsubscribersByUrl.has(url)) { // Don't subscribe twice if the user is a member
unsubscribersByUrl.set(url, syncSpace(url)) if (!pageUnsubscribersByUrl.has(url) && !get(userSpaceUrls).includes(url)) {
pageUnsubscribersByUrl.set(url, syncSpace(url))
} }
for (const [oldUrl, unsubscribe] of unsubscribersByUrl.entries()) { // Clean up old subscriptions
for (const [oldUrl, unsubscribe] of pageUnsubscribersByUrl.entries()) {
if (url !== oldUrl) { if (url !== oldUrl) {
unsubscribersByUrl.delete(oldUrl) pageUnsubscribersByUrl.delete(oldUrl)
unsubscribe() unsubscribe()
} }
} }
} else { } else {
Array.from(unsubscribersByUrl.values()).forEach(call) Array.from(pageUnsubscribersByUrl.values()).forEach(call)
} }
}) })
return () => { return () => {
Array.from(unsubscribersByUrl.values()).forEach(call) Array.from(membershipUnsubscribersByUrl.values()).forEach(call)
Array.from(pageUnsubscribersByUrl.values()).forEach(call)
unsubscribeSpaceUrls()
unsubscribePage() unsubscribePage()
} }
} }
// Chat
const syncRoom = (url: string, h: string) => {
const controller = new AbortController()
pullAndListen({
relays: [url],
signal: controller.signal,
filters: [
{kinds: [ROOM_ADMINS, ROOM_MEMBERS], "#d": [h]},
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
{kinds: [MESSAGE], "#h": [h]},
],
})
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 h of getSpaceRoomsFromGroupSelections(url, $l)) {
const key = `${url}'${h}`
if (!unsubscribersByKey.has(key)) {
newUnsubscribersByKey.set(key, syncRoom(url, h))
}
keys.add(key)
}
}
}
// 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 // DMs
const syncDMRelay = (url: string, pubkey: string) => { const syncDMRelay = (url: string, pubkey: string) => {
const controller = new AbortController() const controller = new AbortController()
// Load historical data // Load historical data
pullConservatively({ pullWithFallback({
relays: [url], relays: [url],
signal: controller.signal, signal: controller.signal,
filters: [{kinds: [WRAP], "#p": [pubkey], until: ago(WEEK, 2)}], filters: [{kinds: [WRAP], "#p": [pubkey], until: ago(WEEK, 2)}],
@@ -378,13 +479,7 @@ const syncDMs = () => {
// Merge all synchronization functions // Merge all synchronization functions
export const syncApplicationData = () => { export const syncApplicationData = () => {
const unsubscribers = [ const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncRooms(), syncDMs()]
syncRelays(),
syncUserData(),
syncMemberships(),
syncCurrentSpace(),
syncDMs(),
]
return () => unsubscribers.forEach(call) return () => unsubscribers.forEach(call)
} }
+1
View File
@@ -6,6 +6,7 @@ import {page} from "$app/stores"
export type ModalOptions = { export type ModalOptions = {
drawer?: boolean drawer?: boolean
noEscape?: boolean
fullscreen?: boolean fullscreen?: boolean
replaceState?: boolean replaceState?: boolean
path?: string path?: string
+3 -5
View File
@@ -171,11 +171,9 @@ export const notifications = derived(
} }
if (hasNip29($relaysByUrl.get(url))) { if (hasNip29($relaysByUrl.get(url))) {
for (const room of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) { for (const h of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
const roomPath = makeRoomPath(url, room) const roomPath = makeRoomPath(url, h)
const latestEvent = allMessages.find( const latestEvent = messages.find(e => e.tags.some(spec(["h", h])))
e => $getUrlsForEvent(e.id).includes(url) && e.tags.find(spec(["h", room])),
)
if (hasNotification(roomPath, latestEvent)) { if (hasNotification(roomPath, latestEvent)) {
paths.add(spacePathMobile) paths.add(spacePathMobile)
+30 -17
View File
@@ -1,5 +1,4 @@
import {on, call, dissoc, assoc, uniq} from "@welshman/lib" import {on, always, call, dissoc, assoc, uniq} from "@welshman/lib"
import type {StampedEvent} from "@welshman/util"
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net" import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
import { import {
makeSocketPolicyAuth, makeSocketPolicyAuth,
@@ -7,11 +6,14 @@ import {
isRelayEvent, isRelayEvent,
isRelayOk, isRelayOk,
isRelayClosed, isRelayClosed,
isRelayNegErr,
isClientReq, isClientReq,
isClientEvent, isClientEvent,
isClientClose, isClientClose,
isClientNegOpen,
isClientNegClose,
} from "@welshman/net" } from "@welshman/net"
import {signer} from "@welshman/app" import {sign} from "@welshman/app"
import { import {
userSettingsValues, userSettingsValues,
getSetting, getSetting,
@@ -19,10 +21,7 @@ import {
relaysMostlyRestricted, relaysMostlyRestricted,
} from "@app/core/state" } from "@app/core/state"
export const authPolicy = makeSocketPolicyAuth({ export const authPolicy = makeSocketPolicyAuth({sign, shouldAuth: always(true)})
sign: (event: StampedEvent) => signer.get()?.sign(event),
shouldAuth: (socket: Socket) => true,
})
export const trustPolicy = (socket: Socket) => { export const trustPolicy = (socket: Socket) => {
const buffer: RelayMessage[] = [] const buffer: RelayMessage[] = []
@@ -76,20 +75,32 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
if (pending.has(id)) { if (pending.has(id)) {
pending.delete(id) pending.delete(id)
if (!ok && details.startsWith("restricted: ")) { if (!ok) {
restricted++ if (details.startsWith("auth-required: ")) {
error = details total--
updateStatus() updateStatus()
}
if (details.startsWith("restricted: ")) {
restricted++
error = details
updateStatus()
}
} }
} }
} }
if (isRelayClosed(message)) { if (isRelayClosed(message) || isRelayNegErr(message)) {
const [_, id, details = ""] = message const [_, id, details = ""] = message
if (pending.has(id)) { if (pending.has(id)) {
pending.delete(id) pending.delete(id)
if (details.startsWith("auth-required: ")) {
total--
updateStatus()
}
if (details.startsWith("restricted: ")) { if (details.startsWith("restricted: ")) {
restricted++ restricted++
error = details error = details
@@ -99,10 +110,12 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
} }
}), }),
on(socket, SocketEvent.Send, (message: ClientMessage) => { on(socket, SocketEvent.Send, (message: ClientMessage) => {
if (isClientReq(message)) { if (isClientReq(message) || isClientNegOpen(message)) {
total++ if (!pending.has(message[1])) {
pending.add(message[1]) total++
updateStatus() pending.add(message[1])
updateStatus()
}
} }
if (isClientEvent(message)) { if (isClientEvent(message)) {
@@ -111,7 +124,7 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
updateStatus() updateStatus()
} }
if (isClientClose(message)) { if (isClientClose(message) || isClientNegClose(message)) {
pending.delete(message[1]) 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 {goto} from "$app/navigation"
import {nthEq, sleep} from "@welshman/lib" import {nthEq, sleep} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {tracker, relaysByUrl} from "@welshman/app" import {tracker, loadRelay} from "@welshman/app"
import {scrollToEvent} from "@lib/html" import {scrollToEvent} from "@lib/html"
import {identity} from "@welshman/lib" import {identity} from "@welshman/lib"
import { import {
@@ -26,6 +26,7 @@ import {
hasNip29, hasNip29,
ROOM, ROOM,
} from "@app/core/state" } from "@app/core/state"
import {lastPageBySpaceUrl} from "@app/util/history"
export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) => { export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) => {
let path = `/spaces/${encodeRelay(url)}` let path = `/spaces/${encodeRelay(url)}`
@@ -37,22 +38,26 @@ export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) =>
.filter(identity) .filter(identity)
.map(s => encodeURIComponent(s as string)) .map(s => encodeURIComponent(s as string))
.join("/") .join("/")
} else {
const relay = relaysByUrl.get().get(url)
if (hasNip29(relay)) {
path += "/recent"
} else {
path += "/chat"
}
} }
return path 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 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") 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)]) return makeChatPath([event.pubkey, ...getPubkeyTagValues(event.tags)])
} }
const room = getTagValue(ROOM, event.tags) const h = getTagValue(ROOM, event.tags)
if (urls.length > 0) { if (urls.length > 0) {
const url = urls[0] const url = urls[0]
@@ -121,7 +126,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
} }
if (event.kind === MESSAGE) { 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] 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) { 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})) 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) { switch (event.kind) {
case THREAD: case THREAD:
return makeThreadPath(url, event.id) return makeThreadPath(url, event.id)
+14 -5
View File
@@ -25,7 +25,9 @@ import {
ROOM_ADD_MEMBER, ROOM_ADD_MEMBER,
ROOM_CREATE_PERMISSION, ROOM_CREATE_PERMISSION,
ROOM_MEMBERS, ROOM_MEMBERS,
ROOM_ADMINS,
ROOM_META, ROOM_META,
ROOM_DELETE,
ROOM_REMOVE_MEMBER, ROOM_REMOVE_MEMBER,
ROOMS, ROOMS,
THREAD, THREAD,
@@ -48,6 +50,7 @@ import {
wrapManager, wrapManager,
} from "@welshman/app" } from "@welshman/app"
import {Collection} from "@lib/storage" import {Collection} from "@lib/storage"
import {isMobile} from "@lib/html"
const syncEvents = async () => { const syncEvents = async () => {
const collection = new Collection<TrustedEvent>({table: "events", getId: prop("id")}) 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 spaceKinds = [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE]
const roomKinds = [ const roomKinds = [
ROOM_META, ROOM_META,
ROOM_DELETE,
ROOM_ADMINS,
ROOM_MEMBERS, ROOM_MEMBERS,
ROOM_ADD_MEMBER, ROOM_ADD_MEMBER,
ROOM_REMOVE_MEMBER, ROOM_REMOVE_MEMBER,
@@ -87,7 +92,7 @@ const syncEvents = async () => {
if (alertKinds.includes(event.kind)) return 8 if (alertKinds.includes(event.kind)) return 8
if (spaceKinds.includes(event.kind)) return 7 if (spaceKinds.includes(event.kind)) return 7
if (roomKinds.includes(event.kind)) return 6 if (roomKinds.includes(event.kind)) return 6
if (contentKinds.includes(event.kind)) return 5 if (!isMobile && contentKinds.includes(event.kind)) return 5
return 0 return 0
} }
@@ -236,17 +241,21 @@ const syncWrapManager = async () => {
} }
export const syncDataStores = async () => { export const syncDataStores = async () => {
const unsubscribers = await Promise.all([ const promises = [
syncEvents(), syncEvents(),
syncTracker(), syncTracker(),
syncRelays(), syncRelays(),
syncRelayStats(),
syncHandles(), syncHandles(),
syncZappers(), syncZappers(),
syncFreshness(),
syncPlaintext(), syncPlaintext(),
syncWrapManager(), syncWrapManager(),
]) ]
if (!isMobile) {
promises.push(syncFreshness(), syncRelayStats())
}
const unsubscribers = await Promise.all(promises)
return () => unsubscribers.forEach(call) return () => unsubscribers.forEach(call)
} }
+7 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames"
import {noop} from "@welshman/lib" import {noop} from "@welshman/lib"
import {fade, fly} from "@lib/transition" import {fade, fly} from "@lib/transition"
@@ -12,7 +13,11 @@
const extraClass = $derived( const extraClass = $derived(
!fullscreen && !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-xl",
"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> </script>
@@ -23,7 +28,7 @@
transition:fade={{duration: 300}} transition:fade={{duration: 300}}
onclick={onClose}> onclick={onClose}>
</button> </button>
<div class="scroll-container relative {extraClass}" transition:fly={{duration: 300}}> <div class="scroll-container {extraClass}" transition:fly={{duration: 300}}>
{@render children?.()} {@render children?.()}
</div> </div>
</div> </div>
+1 -1
View File
@@ -16,7 +16,7 @@
<div class="col-span-2 flex items-center gap-2"> <div class="col-span-2 flex items-center gap-2">
{@render props.input?.()} {@render props.input?.()}
</div> </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} {#if props.info}
{@render props.info?.()} {@render props.info?.()}
{/if} {/if}
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Icon from "@lib/components/Icon.svelte"
type Props = {
src: string
alt: string
}
const {src, alt}: Props = $props()
</script>
{#if src.includes("image/svg") || src.endsWith(".svg")}
<Icon icon={src} />
{:else}
<img {src} {alt} class="h-5 w-5 rounded-lg object-cover" />
{/if}
+1 -1
View File
@@ -9,5 +9,5 @@
<div class="column m-auto max-w-xs gap-2 py-4"> <div class="column m-auto max-w-xs gap-2 py-4">
<h1 class="heading">{@render title?.()}</h1> <h1 class="heading">{@render title?.()}</h1>
<p class="text-center">{@render info?.()}</p> <p class="text-center text-sm opacity-75">{@render info?.()}</p>
</div> </div>
+1 -1
View File
@@ -11,7 +11,7 @@
const {...props}: Props = $props() const {...props}: Props = $props()
</script> </script>
<div data-component="PageBar" class="cw top-sai fixed z-feature p-2"> <div data-component="PageBar" class="cw top-sai fixed z-feature p-2 {props.class}">
<div <div
class="flex min-h-12 items-center justify-between gap-4 rounded-xl bg-base-100 px-4 shadow-xl"> class="flex min-h-12 items-center justify-between gap-4 rounded-xl bg-base-100 px-4 shadow-xl">
<div class="ellipsize flex items-center gap-4 whitespace-nowrap"> <div class="ellipsize flex items-center gap-4 whitespace-nowrap">
+1 -1
View File
@@ -14,6 +14,6 @@
{...props} {...props}
bind:this={element} bind:this={element}
data-component="PageContent" data-component="PageContent"
class="scroll-container cw md:bottom-sai fixed bottom-[calc(var(--saib)+3.5rem)] top-[calc(var(--sait)+3rem)] overflow-y-auto overflow-x-hidden {props.class}"> class="scroll-container cw md:bottom-sai fixed bottom-[calc(var(--saib)+3.5rem)] top-[calc(var(--sait)+3rem)] z-feature overflow-y-auto overflow-x-hidden {props.class}">
{@render children?.()} {@render children?.()}
</div> </div>
+3 -3
View File
@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {makeSpacePath} from "@app/util/routes" import {goToSpace} from "@app/util/routes"
import {PLATFORM_RELAYS} from "@app/core/state" import {PLATFORM_RELAYS} from "@app/core/state"
onMount(() => { onMount(async () => {
if (PLATFORM_RELAYS.length > 0) { if (PLATFORM_RELAYS.length > 0) {
goto(makeSpacePath(PLATFORM_RELAYS[0])) goToSpace(PLATFORM_RELAYS[0])
} else { } else {
goto("/home") goto("/home")
} }
+26 -14
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {dec, tryCatch} from "@welshman/lib" import {dec} from "@welshman/lib"
import type {RelayProfile} from "@welshman/util" import type {RelayProfile} from "@welshman/util"
import {ROOMS, normalizeRelayUrl, isRelayUrl} from "@welshman/util" import {ROOMS} from "@welshman/util"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {relays, createSearch, loadRelay} from "@welshman/app" import {relays, createSearch, loadRelay} from "@welshman/app"
@@ -28,12 +28,11 @@
loadGroupSelections, loadGroupSelections,
getSpaceUrlsFromGroupSelections, getSpaceUrlsFromGroupSelections,
groupSelectionsPubkeysByUrl, groupSelectionsPubkeysByUrl,
parseInviteLink,
} from "@app/core/state" } from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
const openMenu = () => pushModal(SpaceAdd) const openMenu = () => pushModal(SpaceAdd, {hideDiscover: true})
const termUrl = $derived(tryCatch(() => normalizeRelayUrl(term)) || "")
const toggleScanner = () => { const toggleScanner = () => {
showScanner = !showScanner showScanner = !showScanner
@@ -60,7 +59,7 @@
const relaySearch = $derived( const relaySearch = $derived(
createSearch( createSearch(
$relays.filter(r => $groupSelectionsPubkeysByUrl.has(r.url) && r.url !== termUrl), $relays.filter(r => $groupSelectionsPubkeysByUrl.has(r.url) && r.url !== inviteData?.url),
{ {
getValue: (relay: RelayProfile) => relay.url, getValue: (relay: RelayProfile) => relay.url,
sortFn: ({score, item}) => { sortFn: ({score, item}) => {
@@ -78,13 +77,21 @@
), ),
) )
const openSpace = (url: string) => pushModal(SpaceCheck, {url}) const openSpace = (url: string, claim = "") => {
if (claim) {
pushModal(SpaceInviteAccept, {invite: term})
} else {
pushModal(SpaceCheck, {url})
}
}
let term = $state("") let term = $state("")
let limit = $state(20) let limit = $state(20)
let showScanner = $state(false) let showScanner = $state(false)
let element: Element let element: Element
const inviteData = $derived(parseInviteLink(term))
onMount(() => { onMount(() => {
const scroller = createScroller({ const scroller = createScroller({
element, element,
@@ -114,13 +121,18 @@
<div class="row-2 min-w-0 flex-grow items-center"> <div class="row-2 min-w-0 flex-grow items-center">
<label class="input input-bordered flex flex-grow items-center gap-2"> <label class="input input-bordered flex flex-grow items-center gap-2">
<Icon icon={Magnifier} /> <Icon icon={Magnifier} />
<input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." /> <input
bind:value={term}
class="grow"
type="text"
placeholder="Search for spaces or paste invite link..." />
<Button onclick={toggleScanner} class="center"> <Button onclick={toggleScanner} class="center">
<Icon icon={QrCode} /> <Icon icon={QrCode} />
</Button> </Button>
</label> </label>
<Button class="btn btn-primary" onclick={openMenu}> <Button class="btn btn-primary" onclick={openMenu}>
<Icon icon={AddCircle} /> <Icon icon={AddCircle} />
<span class="hidden sm:inline">Add Space</span>
</Button> </Button>
</div> </div>
{#if showScanner} {#if showScanner}
@@ -130,15 +142,15 @@
{/snippet} {/snippet}
{#snippet content()} {#snippet content()}
<div class="col-2 scroll-container" bind:this={element}> <div class="col-2 scroll-container" bind:this={element}>
{#key termUrl} {#if inviteData}
{#if isRelayUrl(termUrl)} {#key inviteData.url}
<Button <Button
class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]" class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]"
onclick={() => openSpace(termUrl)}> onclick={() => openSpace(inviteData.url, inviteData.claim)}>
<RelaySummary url={termUrl} /> <RelaySummary url={inviteData.url} />
</Button> </Button>
{/if} {/key}
{/key} {/if}
{#each relaySearch.searchOptions(term).slice(0, limit) as relay (relay.url)} {#each relaySearch.searchOptions(term).slice(0, limit) as relay (relay.url)}
<Button <Button
class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]" class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]"
+3 -3
View File
@@ -12,16 +12,16 @@
import SpaceAdd from "@app/components/SpaceAdd.svelte" import SpaceAdd from "@app/components/SpaceAdd.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte" import ChatEnable from "@app/components/ChatEnable.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {makeSpacePath} from "@app/util/routes" import {goToSpace} from "@app/util/routes"
import {PLATFORM_NAME, PLATFORM_RELAYS} from "@app/core/state" import {PLATFORM_NAME, PLATFORM_RELAYS} from "@app/core/state"
const addSpace = () => pushModal(SpaceAdd) const addSpace = () => pushModal(SpaceAdd)
const openChat = () => ($shouldUnwrap ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"})) const openChat = () => ($shouldUnwrap ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
onMount(() => { onMount(async () => {
if (PLATFORM_RELAYS.length > 0) { if (PLATFORM_RELAYS.length > 0) {
goto(makeSpacePath(PLATFORM_RELAYS[0])) goToSpace(PLATFORM_RELAYS[0])
} }
}) })
</script> </script>
+1 -1
View File
@@ -9,7 +9,7 @@
import Key from "@assets/icons/key-minimalistic.svg?dataurl" import Key from "@assets/icons/key-minimalistic.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl" import Copy from "@assets/icons/copy.svg?dataurl"
import Settings from "@assets/icons/settings-minimalistic.svg?dataurl" import Settings from "@assets/icons/settings.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl" import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
import AltArrowUp from "@assets/icons/alt-arrow-up.svg?dataurl" import AltArrowUp from "@assets/icons/alt-arrow-up.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
+53
View File
@@ -0,0 +1,53 @@
<script lang="ts">
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Page from "@lib/components/Page.svelte"
import PageBar from "@lib/components/PageBar.svelte"
import PageContent from "@lib/components/PageContent.svelte"
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import {userSpaceUrls, PLATFORM_RELAYS} from "@app/core/state"
import {pushModal} from "@app/util/modal"
const addSpace = () => pushModal(SpaceAdd)
</script>
<Page class="cw-full">
<PageBar class="cw-full">
{#snippet icon()}
<div class="center">
<Icon icon={SettingsMinimalistic} />
</div>
{/snippet}
{#snippet title()}
<strong>Your Spaces</strong>
{/snippet}
{#snippet action()}
{#if $userSpaceUrls.length > 0 && PLATFORM_RELAYS.length === 0}
<Button class="btn btn-primary btn-sm" onclick={addSpace}>
<Icon icon={AddCircle} />
Add Space
</Button>
{/if}
{/snippet}
</PageBar>
<PageContent class="cw-full flex flex-col gap-2 p-2 pt-4">
{#each PLATFORM_RELAYS as url (url)}
<MenuSpacesItem {url} />
{:else}
{#each $userSpaceUrls as url (url)}
<MenuSpacesItem {url} />
{:else}
<div class="flex flex-col gap-8 items-center py-20">
<p>You haven't added any spaces yet!</p>
<Button class="btn btn-primary" onclick={addSpace}>
<Icon icon={AddCircle} />
Add a Space
</Button>
</div>
{/each}
{/each}
</PageContent>
</Page>
+7 -8
View File
@@ -3,7 +3,6 @@
import {page} from "$app/stores" import {page} from "$app/stores"
import {once} from "@welshman/lib" import {once} from "@welshman/lib"
import Page from "@lib/components/Page.svelte" import Page from "@lib/components/Page.svelte"
import Dialog from "@lib/components/Dialog.svelte"
import SecondaryNav from "@lib/components/SecondaryNav.svelte" import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import MenuSpace from "@app/components/MenuSpace.svelte" import MenuSpace from "@app/components/MenuSpace.svelte"
import SpaceAuthError from "@app/components/SpaceAuthError.svelte" import SpaceAuthError from "@app/components/SpaceAuthError.svelte"
@@ -23,7 +22,11 @@
const authError = deriveRelayAuthError(url) const authError = deriveRelayAuthError(url)
const showAuthError = once(() => pushModal(SpaceAuthError, {url, error: $authError})) const showAuthError = once(() =>
pushModal(SpaceAuthError, {url, error: $authError}, {noEscape: true}),
)
const showPendingTrust = once(() => pushModal(SpaceTrustRelay, {url}, {noEscape: true}))
// We have to watch this one, since on mobile the badge will be visible when active // We have to watch this one, since on mobile the badge will be visible when active
$effect(() => { $effect(() => {
@@ -36,6 +39,8 @@
$effect(() => { $effect(() => {
if ($authError) { if ($authError) {
showAuthError() showAuthError()
} else if ($relaysPendingTrust.includes(url)) {
showPendingTrust()
} }
}) })
</script> </script>
@@ -48,9 +53,3 @@
{@render children?.()} {@render children?.()}
{/key} {/key}
</Page> </Page>
{#if $relaysPendingTrust.includes(url)}
<Dialog>
<SpaceTrustRelay {url} />
</Dialog>
{/if}
@@ -3,7 +3,6 @@
import {readable} from "svelte/store" import {readable} from "svelte/store"
import {onMount, onDestroy} from "svelte" import {onMount, onDestroy} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {goto} from "$app/navigation"
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import type {MakeNonOptional} from "@welshman/lib" import type {MakeNonOptional} from "@welshman/lib"
import {now, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib" import {now, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
@@ -15,20 +14,12 @@
ROOM_ADD_MEMBER, ROOM_ADD_MEMBER,
ROOM_REMOVE_MEMBER, ROOM_REMOVE_MEMBER,
} from "@welshman/util" } from "@welshman/util"
import { import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
pubkey,
publishThunk,
waitForThunkError,
deleteRoom,
joinRoom,
leaveRoom,
repository,
} from "@welshman/app"
import {slide, fade, fly} from "@lib/transition" import {slide, fade, fly} from "@lib/transition"
import Hashtag from "@assets/icons/hashtag.svg?dataurl" import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl" import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
import Login2 from "@assets/icons/login-3.svg?dataurl" import Login2 from "@assets/icons/login-3.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl" import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
import Logout2 from "@assets/icons/logout-3.svg?dataurl" import Logout2 from "@assets/icons/logout-3.svg?dataurl"
import Bookmark from "@assets/icons/bookmark.svg?dataurl" import Bookmark from "@assets/icons/bookmark.svg?dataurl"
@@ -38,22 +29,22 @@
import PageBar from "@lib/components/PageBar.svelte" import PageBar from "@lib/components/PageBar.svelte"
import PageContent from "@lib/components/PageContent.svelte" import PageContent from "@lib/components/PageContent.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte" import ThunkToast from "@app/components/ThunkToast.svelte"
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import RoomEdit from "@app/components/RoomEdit.svelte"
import ChannelItem from "@app/components/ChannelItem.svelte" import RoomName from "@app/components/RoomName.svelte"
import ChannelItemAddMember from "@src/app/components/ChannelItemAddMember.svelte" import RoomItem from "@app/components/RoomItem.svelte"
import ChannelItemRemoveMember from "@src/app/components/ChannelItemRemoveMember.svelte" import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
import ChannelCompose from "@app/components/ChannelCompose.svelte" import RoomItemRemoveMember from "@src/app/components/RoomItemRemoveMember.svelte"
import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte" import RoomCompose from "@app/components/RoomCompose.svelte"
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte" import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
import { import {
deriveUserRooms, deriveUserRooms,
userSettingsValues, userSettingsValues,
decodeRelay, decodeRelay,
deriveUserRoomMembershipStatus, deriveUserRoomMembershipStatus,
deriveChannel, deriveRoom,
MembershipStatus, MembershipStatus,
PROTECTED, PROTECTED,
MESSAGE_KINDS, MESSAGE_KINDS,
@@ -71,28 +62,27 @@
import {popKey} from "@lib/implicit" import {popKey} from "@lib/implicit"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {makeSpacePath} from "@app/util/routes"
const {room, relay} = $page.params as MakeNonOptional<typeof $page.params> const {h, relay} = $page.params as MakeNonOptional<typeof $page.params>
const mounted = now() const mounted = now()
const lastChecked = $checked[$page.url.pathname] const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay(relay) const url = decodeRelay(relay)
const channel = deriveChannel(url, room) const room = deriveRoom(url, h)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const userRooms = deriveUserRooms(url) const userRooms = deriveUserRooms(url)
const userIsAdmin = deriveUserIsRoomAdmin(url, room) const userIsAdmin = deriveUserIsRoomAdmin(url, h)
const isFavorite = $derived($userRooms.includes(room)) const isFavorite = $derived($userRooms.includes(h))
const membershipStatus = deriveUserRoomMembershipStatus(url, room) const membershipStatus = deriveUserRoomMembershipStatus(url, h)
const addFavorite = () => addRoomMembership(url, room) const addFavorite = () => addRoomMembership(url, h)
const removeFavorite = () => removeRoomMembership(url, room) const removeFavorite = () => removeRoomMembership(url, h)
const join = async () => { const join = async () => {
joining = true joining = true
try { try {
const message = await waitForThunkError(joinRoom(url, makeRoomMeta({id: room}))) const message = await waitForThunkError(joinRoom(url, makeRoomMeta({h})))
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
return pushToast({theme: "error", message}) return pushToast({theme: "error", message})
@@ -108,7 +98,7 @@
const leave = async () => { const leave = async () => {
leaving = true leaving = true
try { try {
const message = await waitForThunkError(leaveRoom(url, makeRoomMeta({id: room}))) const message = await waitForThunkError(leaveRoom(url, makeRoomMeta({h})))
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message}) pushToast({theme: "error", message})
@@ -136,7 +126,7 @@
} }
const onSubmit = async ({content, tags}: EventContent) => { const onSubmit = async ({content, tags}: EventContent) => {
tags.push(["h", room]) tags.push(["h", h])
if (await shouldProtect) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
@@ -214,7 +204,7 @@
let showScrollButton = $state(false) let showScrollButton = $state(false)
let cleanup: () => void let cleanup: () => void
let events: Readable<TrustedEvent[]> = $state(readable([])) let events: Readable<TrustedEvent[]> = $state(readable([]))
let compose: ChannelCompose | undefined = $state() let compose: RoomCompose | undefined = $state()
let eventToEdit: TrustedEvent | undefined = $state() let eventToEdit: TrustedEvent | undefined = $state()
const elements = $derived.by(() => { const elements = $derived.by(() => {
@@ -280,7 +270,7 @@
const feed = makeFeed({ const feed = makeFeed({
url, url,
element: element!, element: element!,
filters: [{kinds: [...MESSAGE_KINDS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]}], filters: [{kinds: [...MESSAGE_KINDS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]}],
onExhausted: () => { onExhausted: () => {
loadingEvents = false loadingEvents = false
}, },
@@ -290,6 +280,12 @@
cleanup = feed.cleanup cleanup = feed.cleanup
} }
const onEscape = () => {
clearParent()
clearShare()
eventToEdit = undefined
}
const canEditEvent = (event: TrustedEvent) => const canEditEvent = (event: TrustedEvent) =>
event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE) event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE)
@@ -307,23 +303,7 @@
} }
} }
const startDelete = () => const startEdit = () => pushModal(RoomEdit, {url, h})
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, makeRoomMeta({id: room}))
const message = await waitForThunkError(thunk)
if (message) {
repository.removeEvent(thunk.event.id)
pushToast({theme: "error", message})
} else {
goto(makeSpacePath(url))
}
},
})
onMount(() => { onMount(() => {
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
@@ -360,17 +340,17 @@
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<strong class="ellipsize"> <strong class="ellipsize">
<ChannelName {url} {room} /> <RoomName {url} {h} />
</strong> </strong>
{/snippet} {/snippet}
{#snippet action()} {#snippet action()}
<div class="row-2"> <div class="row-2">
{#if $userIsAdmin || true} {#if $userIsAdmin}
<Button <Button
class="btn btn-neutral btn-sm tooltip tooltip-left" class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Delete this room" data-tip="Edit room information"
onclick={startDelete}> onclick={startEdit}>
<Icon size={4} icon={TrashBin2} /> <Icon size={4} icon={Pen} />
</Button> </Button>
{:else if $membershipStatus === MembershipStatus.Initial} {:else if $membershipStatus === MembershipStatus.Initial}
<Button <Button
@@ -412,24 +392,26 @@
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4"> <PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4">
<div bind:this={dynamicPadding}></div> <div bind:this={dynamicPadding}></div>
{#if $channel?.private && $membershipStatus !== MembershipStatus.Granted} {#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
<div class="py-20"> <div class="py-20">
<div class="card2 col-8 m-auto max-w-md items-center text-center"> <div class="card2 col-8 m-auto max-w-md items-center text-center">
<p class="row-2">You aren't currently a member of this room.</p> <p class="opacity-75">You aren't currently a member of this room.</p>
{#if $membershipStatus === MembershipStatus.Pending} {#if !$room.isClosed}
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}> {#if $membershipStatus === MembershipStatus.Pending}
<Icon icon={ClockCircle} /> <Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
Access Pending <Icon icon={ClockCircle} />
</Button> Access Pending
{:else} </Button>
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}> {:else}
{#if joining} <Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
<span class="loading loading-spinner loading-sm"></span> {#if joining}
{:else} <span class="loading loading-spinner loading-sm"></span>
<Icon icon={Login2} /> {:else}
{/if} <Icon icon={Login2} />
Join Room {/if}
</Button> Join Room
</Button>
{/if}
{/if} {/if}
</div> </div>
</div> </div>
@@ -449,12 +431,12 @@
{:else} {:else}
{@const event = $state.snapshot(value as TrustedEvent)} {@const event = $state.snapshot(value as TrustedEvent)}
{#if event.kind === ROOM_ADD_MEMBER} {#if event.kind === ROOM_ADD_MEMBER}
<ChannelItemAddMember {url} {event} /> <RoomItemAddMember {url} {event} />
{:else if event.kind === ROOM_REMOVE_MEMBER} {:else if event.kind === ROOM_REMOVE_MEMBER}
<ChannelItemRemoveMember {url} {event} /> <RoomItemRemoveMember {url} {event} />
{:else} {:else}
<div in:slide class:-mt-1={!showPubkey}> <div in:slide class:-mt-1={!showPubkey}>
<ChannelItem <RoomItem
{url} {url}
{event} {event}
{replyTo} {replyTo}
@@ -476,44 +458,47 @@
</PageContent> </PageContent>
<div class="chat__compose bg-base-200" bind:this={chatCompose}> <div class="chat__compose bg-base-200" bind:this={chatCompose}>
{#if $channel?.private && $membershipStatus !== MembershipStatus.Granted} {#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
<!-- pass --> <!-- pass -->
{:else if $channel?.closed && $membershipStatus !== MembershipStatus.Granted} {:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3"> <div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
<p>Only members are allowed to post to this room.</p> <p class="opacity-75">Only members are allowed to post to this room.</p>
{#if $membershipStatus === MembershipStatus.Pending} {#if !$room.isClosed}
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}> {#if $membershipStatus === MembershipStatus.Pending}
<Icon icon={ClockCircle} /> <Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
Access Pending <Icon icon={ClockCircle} />
</Button> Access Pending
{:else} </Button>
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}> {:else}
{#if joining} <Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
<span class="loading loading-spinner loading-sm"></span> {#if joining}
{:else} <span class="loading loading-spinner loading-sm"></span>
<Icon icon={Login2} /> {:else}
{/if} <Icon icon={Login2} />
Ask to Join {/if}
</Button> Ask to Join
</Button>
{/if}
{/if} {/if}
</div> </div>
{:else} {:else}
<div> <div>
{#if parent} {#if parent}
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" /> <RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if} {/if}
{#if share} {#if share}
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" /> <RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
{/if} {/if}
{#if eventToEdit} {#if eventToEdit}
<ChannelComposeEdit clear={clearEventToEdit} /> <RoomComposeEdit clear={clearEventToEdit} />
{/if} {/if}
</div> </div>
{#key eventToEdit} {#key eventToEdit}
<ChannelCompose <RoomCompose
{url} {url}
{room} {h}
{onSubmit} {onSubmit}
{onEscape}
{onEditPrevious} {onEditPrevious}
content={eventToEdit?.content} content={eventToEdit?.content}
bind:this={compose} /> bind:this={compose} />
+21 -14
View File
@@ -18,12 +18,12 @@
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte" import ThunkToast from "@app/components/ThunkToast.svelte"
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import ChannelItem from "@app/components/ChannelItem.svelte" import RoomItem from "@app/components/RoomItem.svelte"
import ChannelItemAddMember from "@src/app/components/ChannelItemAddMember.svelte" import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
import ChannelItemRemoveMember from "@src/app/components/ChannelItemRemoveMember.svelte" import RoomItemRemoveMember from "@src/app/components/RoomItemRemoveMember.svelte"
import ChannelCompose from "@app/components/ChannelCompose.svelte" import RoomCompose from "@app/components/RoomCompose.svelte"
import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte" import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte" import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
import {userSettingsValues, decodeRelay, PROTECTED, MESSAGE_KINDS} from "@app/core/state" import {userSettingsValues, decodeRelay, PROTECTED, MESSAGE_KINDS} from "@app/core/state"
import {prependParent, canEnforceNip70, publishDelete} from "@app/core/commands" import {prependParent, canEnforceNip70, publishDelete} from "@app/core/commands"
import {setChecked, checked} from "@app/util/notifications" import {setChecked, checked} from "@app/util/notifications"
@@ -128,7 +128,7 @@
let showScrollButton = $state(false) let showScrollButton = $state(false)
let cleanup: () => void let cleanup: () => void
let events: Readable<TrustedEvent[]> = $state(readable([])) let events: Readable<TrustedEvent[]> = $state(readable([]))
let compose: ChannelCompose | undefined = $state() let compose: RoomCompose | undefined = $state()
let eventToEdit: TrustedEvent | undefined = $state() let eventToEdit: TrustedEvent | undefined = $state()
const elements = $derived.by(() => { const elements = $derived.by(() => {
@@ -188,6 +188,12 @@
return elements return elements
}) })
const onEscape = () => {
clearParent()
clearShare()
eventToEdit = undefined
}
const canEditEvent = (event: TrustedEvent) => const canEditEvent = (event: TrustedEvent) =>
event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE) event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE)
@@ -274,12 +280,12 @@
{:else} {:else}
{@const event = $state.snapshot(value as TrustedEvent)} {@const event = $state.snapshot(value as TrustedEvent)}
{#if event.kind === RELAY_ADD_MEMBER} {#if event.kind === RELAY_ADD_MEMBER}
<ChannelItemAddMember {url} {event} /> <RoomItemAddMember {url} {event} />
{:else if event.kind === RELAY_REMOVE_MEMBER} {:else if event.kind === RELAY_REMOVE_MEMBER}
<ChannelItemRemoveMember {url} {event} /> <RoomItemRemoveMember {url} {event} />
{:else} {:else}
<div class:-mt-1={!showPubkey}> <div class:-mt-1={!showPubkey}>
<ChannelItem <RoomItem
{url} {url}
{event} {event}
{replyTo} {replyTo}
@@ -302,19 +308,20 @@
<div class="chat__compose bg-base-200" bind:this={chatCompose}> <div class="chat__compose bg-base-200" bind:this={chatCompose}>
<div> <div>
{#if parent} {#if parent}
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" /> <RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if} {/if}
{#if share} {#if share}
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" /> <RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
{/if} {/if}
{#if eventToEdit} {#if eventToEdit}
<ChannelComposeEdit clear={clearEventToEdit} /> <RoomComposeEdit clear={clearEventToEdit} />
{/if} {/if}
</div> </div>
{#key eventToEdit} {#key eventToEdit}
<ChannelCompose <RoomCompose
{url} {url}
{onSubmit} {onSubmit}
{onEscape}
{onEditPrevious} {onEditPrevious}
content={eventToEdit?.content} content={eventToEdit?.content}
bind:this={compose} /> bind:this={compose} />
@@ -21,7 +21,7 @@
const conversations = derived(messages, $messages => { const conversations = derived(messages, $messages => {
const convs = [] const convs = []
for (const [room, messages] of groupBy(e => getTagValue("h", e.tags), $messages).entries()) { for (const [h, messages] of groupBy(e => getTagValue("h", e.tags), $messages).entries()) {
const avgTime = avg(overlappingPairs(messages).map(([a, b]) => a.created_at - b.created_at)) const avgTime = avg(overlappingPairs(messages).map(([a, b]) => a.created_at - b.created_at))
const groups: TrustedEvent[][] = [] const groups: TrustedEvent[][] = []
const group: TrustedEvent[] = [] const group: TrustedEvent[] = []
@@ -52,7 +52,7 @@
const earliest = last(events)! const earliest = last(events)!
const participants = uniq(events.map(msg => msg.pubkey)) const participants = uniq(events.map(msg => msg.pubkey))
convs.push({room, events, latest, earliest, participants}) convs.push({h, events, latest, earliest, participants})
} }
} }
@@ -96,10 +96,10 @@
{#if $messages.length > 0} {#if $messages.length > 0}
{@const events = $messages.slice(0, 1)} {@const events = $messages.slice(0, 1)}
{@const event = events[0]} {@const event = events[0]}
{@const room = getTagValue("h", event.tags)} {@const h = getTagValue("h", event.tags)}
<ConversationCard <ConversationCard
{h}
{url} {url}
{room}
{events} {events}
latest={event} latest={event}
earliest={event} earliest={event}
@@ -110,8 +110,8 @@
</div> </div>
{/if} {/if}
{:else} {:else}
{#each $conversations.slice(0, limit) as { room, events, latest, earliest, participants } (latest.id)} {#each $conversations.slice(0, limit) as { h, events, latest, earliest, participants } (latest.id)}
<ConversationCard {url} {room} {events} {latest} {earliest} {participants} /> <ConversationCard {h} {url} {events} {latest} {earliest} {participants} />
{/each} {/each}
{/if} {/if}
</PageContent> </PageContent>
+111
View File
@@ -0,0 +1,111 @@
<script lang="ts">
import Server from "@assets/icons/server.svg?dataurl"
import ArrowRight from "@assets/icons/arrow-right.svg?dataurl"
import HandShake from "@assets/icons/hand-shake.svg?dataurl"
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte"
import Page from "@lib/components/Page.svelte"
import PageHeader from "@lib/components/PageHeader.svelte"
import PageContent from "@lib/components/PageContent.svelte"
</script>
<Page class="cw-full">
<PageContent class="cw-full flex flex-col items-center gap-2 p-2 pt-4">
<PageHeader>
{#snippet title()}
<div>Create your own Space</div>
{/snippet}
{#snippet info()}
<p>Get started with one of our trusted partners, or learn how to host your own space.</p>
{/snippet}
</PageHeader>
<div class="grid w-full max-w-lg grid-cols-1 gap-2 lg:max-w-4xl lg:grid-cols-2">
<div class="card2 bg-alt flex flex-col gap-4">
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Icon icon={Server} />
<h3 class="text-lg font-bold">Self-Host your Space</h3>
</div>
<div class="badge badge-neutral">Recommended</div>
</div>
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
<li>Unlimited customization and control</li>
<li>Free and open source software</li>
<li>Full-featured admin dashboards available</li>
<li>Requires some technical skills</li>
</ul>
</div>
<Link class="btn btn-primary" href="https://github.com/coracle-social/zooid">
Get Started
<Icon icon={ArrowRight} />
</Link>
</div>
<div class="card2 bg-alt flex flex-col gap-4">
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<img alt="Coracle Logo" src="/coracle.png" class="h-7 w-7" />
<h3 class="text-lg font-bold">Coracle Hosting</h3>
</div>
<div class="badge badge-neutral">Recommended</div>
</div>
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
<li>Simple setup, support included</li>
<li>Free and open source software — no vendor lock-in</li>
<li>Advanced access controls and relay policies</li>
<li>Full-featured admin dashboard</li>
</ul>
</div>
<Link external class="btn btn-neutral" href="https://hosting.coracle.social">
Get Started
<Icon icon={ArrowRight} />
</Link>
</div>
<div class="card2 bg-alt flex flex-col gap-4">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<Icon icon={HandShake} />
<h3 class="text-lg font-bold">Holis Communities</h3>
</div>
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
<li>Simple self-serve space creation</li>
<li>Built-in moderation tools</li>
<li>Room-level access controls</li>
<li>Membship lists and invite codes</li>
</ul>
</div>
<Link external class="btn btn-neutral" href="https://hol.is">
Get Started
<Icon icon={ArrowRight} />
</Link>
</div>
<div class="card2 bg-alt flex flex-col gap-4">
<div class="flex flex-col gap-4">
<div class="self-start">
<img
alt="Relay Tools"
src="https://relay.tools/17.svg"
class="-my-20 -ml-2 hidden h-48 dark:block"
style="filter: contrast(50%)" />
<img
alt="Relay Tools"
src="https://relay.tools/19.svg"
class="-my-20 -ml-2 h-48 dark:hidden"
style="filter: contrast(50%)" />
</div>
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
<li>Independently run</li>
<li>Customizable relay policies</li>
<li>Simple management dashboard</li>
<li>Support available</li>
</ul>
</div>
<Link class="btn btn-neutral" href="https://relay.tools/signup">
Get Started
<Icon icon={ArrowRight} />
</Link>
</div>
</div>
</PageContent>
</Page>
+10 -12
View File
@@ -8,11 +8,8 @@ config({path: ".env.template"})
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ["./src/**/*.{html,js,svelte,ts}"], content: ["./src/**/*.{html,js,svelte,ts}"],
darkMode: ['selector', '[data-theme="dark"]'], darkMode: ["selector", '[data-theme="dark"]'],
safelist: [ safelist: ["bg-success", "bg-warning"],
'bg-success',
'bg-warning',
],
theme: { theme: {
extend: {}, extend: {},
zIndex: { zIndex: {
@@ -20,11 +17,12 @@ export default {
"nav-active": 1, "nav-active": 1,
"nav-item": 2, "nav-item": 2,
feature: 3, feature: 3,
nav: 4, compose: 4,
popover: 5, nav: 5,
modal: 6, popover: 6,
"modal-feature": 7, modal: 7,
toast: 8, "modal-feature": 8,
toast: 9,
}, },
}, },
plugins: [daisyui], plugins: [daisyui],
@@ -40,8 +38,8 @@ export default {
}, },
light: { light: {
...themes["winter"], ...themes["winter"],
neutral: '#F2F7FF', neutral: "#F2F7FF",
warning: '#FD8D0B', warning: "#FD8D0B",
primary: process.env.VITE_PLATFORM_ACCENT, primary: process.env.VITE_PLATFORM_ACCENT,
"primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF", "primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF",
secondary: process.env.VITE_PLATFORM_SECONDARY, secondary: process.env.VITE_PLATFORM_SECONDARY,