Compare commits

...

13 Commits

Author SHA1 Message Date
Jon Staab 57447e5bf4 Bump version 2025-11-11 14:09:05 -08:00
Jon Staab 8e411daaef Refactor avatar components, add space edit form 2025-11-11 13:50:45 -08:00
Jon Staab 183aebf841 Improve room syncing 2025-11-10 16:19:50 -08:00
Jon Staab e3e500ccc2 Return better blossom errors 2025-11-10 16:02:02 -08:00
Jon Staab e7a2535ece Fix access restricted after successful invite code 2025-11-10 15:24:11 -08:00
Jon Staab 761e369313 Add room detail, assume admins are members 2025-11-10 14:59:15 -08:00
Jon Staab 5248275d73 Fix nav index 2025-11-10 13:20:42 -08:00
Jon Staab cb033279dd Fix link 2025-11-06 11:25:07 -08:00
Jon Staab 41d50d8c28 Add room policy indicator 2025-11-05 16:59:17 -08:00
Jon Staab a52c2b4c3c Lighten up shadows 2025-11-05 15:32:55 -08:00
Jon Staab b5917cb184 Show loading on spaces menu 2025-11-05 15:24:46 -08:00
Jon Staab 57348472f8 Always join spaces when visiting them 2025-11-05 15:09:23 -08:00
Jon Staab 4b6223dc00 Update changelog 2025-11-05 09:46:05 -08:00
68 changed files with 953 additions and 522 deletions
+15
View File
@@ -1,5 +1,20 @@
# Changelog # Changelog
# 1.5.3
* Add space edit form
* Improve room syncing
* Return better blossom errors
* Fix access restricted bugs
* Add room detail dialog
* Fix broken link to self hosting
* Tweak shadows
* Always join spaces when visiting them
# 1.5.2
* Fix negentropy room syncing
# 1.5.1 # 1.5.1
* Fix chat path link * Fix chat path link
+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 33 versionCode 34
versionName "1.5.2" versionName "1.5.3"
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 -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 = 24; CURRENT_PROJECT_VERSION = 25;
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.5.2; MARKETING_VERSION = 1.5.3;
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 = 24; CURRENT_PROJECT_VERSION = 25;
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.5.2; MARKETING_VERSION = 1.5.3;
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.5.2", "version": "1.5.3",
"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.5", "@welshman/app": "^0.6.8",
"@welshman/content": "^0.6.5", "@welshman/content": "^0.6.8",
"@welshman/editor": "^0.6.5", "@welshman/editor": "^0.6.8",
"@welshman/feeds": "^0.6.5", "@welshman/feeds": "^0.6.8",
"@welshman/lib": "^0.6.5", "@welshman/lib": "^0.6.8",
"@welshman/net": "^0.6.5", "@welshman/net": "^0.6.8",
"@welshman/router": "^0.6.5", "@welshman/router": "^0.6.8",
"@welshman/signer": "^0.6.5", "@welshman/signer": "^0.6.8",
"@welshman/store": "^0.6.5", "@welshman/store": "^0.6.8",
"@welshman/util": "^0.6.5", "@welshman/util": "^0.6.8",
"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",
+76 -76
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.5 specifier: ^0.6.8
version: 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/content': '@welshman/content':
specifier: ^0.6.5 specifier: ^0.6.8
version: 0.6.5(typescript@5.9.3) version: 0.6.8(typescript@5.9.3)
'@welshman/editor': '@welshman/editor':
specifier: ^0.6.5 specifier: ^0.6.8
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) version: 0.6.8(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)
'@welshman/feeds': '@welshman/feeds':
specifier: ^0.6.5 specifier: ^0.6.8
version: 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/lib': '@welshman/lib':
specifier: ^0.6.5 specifier: ^0.6.8
version: 0.6.5 version: 0.6.8
'@welshman/net': '@welshman/net':
specifier: ^0.6.5 specifier: ^0.6.8
version: 0.6.5(typescript@5.9.3)(ws@8.18.3) version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': '@welshman/router':
specifier: ^0.6.5 specifier: ^0.6.8
version: 0.6.5(typescript@5.9.3)(ws@8.18.3) version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/signer': '@welshman/signer':
specifier: ^0.6.5 specifier: ^0.6.8
version: 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/store': '@welshman/store':
specifier: ^0.6.5 specifier: ^0.6.8
version: 0.6.5(typescript@5.9.3)(ws@8.18.3) version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': '@welshman/util':
specifier: ^0.6.5 specifier: ^0.6.8
version: 0.6.5(typescript@5.9.3) version: 0.6.8(typescript@5.9.3)
compressorjs: compressorjs:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
@@ -1692,38 +1692,38 @@ packages:
'@vite-pwa/assets-generator': '@vite-pwa/assets-generator':
optional: true optional: true
'@welshman/app@0.6.5': '@welshman/app@0.6.8':
resolution: {integrity: sha512-hk39kKzptldZxtFbYzgrEK8Y151o75GwV6P2sK5LlkyafWlhx3SwteAcuNelcJZitoAPXi7w06W34bbwRYPx+Q==} resolution: {integrity: sha512-bhl18VWA9tzHLY7D+b2xlkc/RbJr03XiA7+otcjzf8X48S4pih/F4TDw1yJbAWOMOx9G3NI6sWLffpZQeSUPiQ==}
'@welshman/content@0.6.5': '@welshman/content@0.6.8':
resolution: {integrity: sha512-QSlkuko+2r72q3VFlOXpnnoJ6GioCgan1ysHMlKqKarKNFTL4kfqdq1yxYrFRJdQou7WuB+f9iULO0AFWkXmXg==} resolution: {integrity: sha512-VLek8oOoMMTrEtpIfqFqM9BsbifWYwPC7UiuVuWYqaTSmiAbU3DM2J+tYFcrgnQF8xMnUi/JoVXJ+b2AtpjFrw==}
'@welshman/editor@0.6.5': '@welshman/editor@0.6.8':
resolution: {integrity: sha512-3sUnUFBeaVJbJgnkZlIqFqXv/NtnxXt3Pr6BkMYi2ocDMxHsMOIsOrCcyoXg8G5IYz7FzkWHtUtM3mhaDU7YVg==} resolution: {integrity: sha512-QzNX7/Nobkh+bpjFnuW2REVpX7Sa+lj70LDdGmEJpjtXlTKlLuNZzpFLee5F9fSObcKCl1G2xBN3tYbZD3vHUA==}
'@welshman/feeds@0.6.5': '@welshman/feeds@0.6.8':
resolution: {integrity: sha512-IT1kSN+Xf/MaoHAOHJftORDwJZxl3UCLizc+mvJ4ktvOT/oVu9YX5zcb0YMwiJN2N4C4FpK/BIBjxivS6QIaRQ==} resolution: {integrity: sha512-95VRR2QmGrBUyzYgdsMxhntVoOnaEMsMHRsci1/GX1oOFZPJFTiV7e/m/dD/aWVLUQV1hlRxxXosFFtEDkpjIw==}
'@welshman/lib@0.6.5': '@welshman/lib@0.6.8':
resolution: {integrity: sha512-L6NQm1QNBOTQ+ymiSFPfL+TDiW1AP64AEp633Fh1ciopaU73JFbV0P6xpLxt3qJkQFZfJRxk7gWDMaVDo3Y24w==} resolution: {integrity: sha512-1Wybkk8+vBdqv9nRhnNwIW9YVbhu3di07A2fUYWAQvldto49X26U8u7EV2CkUsz4iNC/799EBYuelcc6W9oZYw==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
'@welshman/net@0.6.5': '@welshman/net@0.6.8':
resolution: {integrity: sha512-JcmPdWzT0aUaOWytw4EJpl9EooOstm4LcJczc9pJYk1hQE4gDix1AfW6bqBiDlTnJ5fplOW/KLgXuV1rhsKEWA==} resolution: {integrity: sha512-Lc1nIckdxW2ILiknowcbaKo+192QWQOBn6FLhFCEUZNyRNEOJYkAgDu4jKn7GXu91xpfJUFnq5KDvvq7hUeHqg==}
'@welshman/router@0.6.5': '@welshman/router@0.6.8':
resolution: {integrity: sha512-7ZxAkCg09ZIeYh49LUlL7nFRvU4880DsMstEu2KRQQIO/wg6VZuMJh8+uKGQq5arul09rtNl1bhg0/YUFiAc/A==} resolution: {integrity: sha512-+OJoD2Jm+yFiLc5FYb4/za66639CeIMYk7j4UAzR7n1z/gFQYMDviXqFYbcWHln3fgy4G7UF1HWBoU0sQD8EEw==}
'@welshman/signer@0.6.5': '@welshman/signer@0.6.8':
resolution: {integrity: sha512-SgQCtb0du3vpyaRVGXM43CM5S0fTh+1WWLnZEWUBkEpyRzRtI9DegTowL6+vhbNxsWB6oHn/FqY7O3HLS5rIEw==} resolution: {integrity: sha512-lt9Qq89TWyx/zSWgHkeVUX7MBCx86iBCkvzTdUIS7Ad6KfjjcYtsL9wAtfCc+TlvE87okOg97hAOvw18yIwfbw==}
peerDependencies: peerDependencies:
nostr-signer-capacitor-plugin: ~0.0.4 nostr-signer-capacitor-plugin: ~0.0.4
'@welshman/store@0.6.5': '@welshman/store@0.6.8':
resolution: {integrity: sha512-Fdl8ygK2/pZRxbLGSWtJJGtf2wTm46RDrCF2zURDJL6e80NGmTXl7LhqpSeRKmR1sTQiwEhsRvD1lqnKAWB3xQ==} resolution: {integrity: sha512-s5s5+tdPyXB1m2vLn2wfo7nx+uNKWBdwCyomk+soWKWEY3LWvg4DAKgQ1gF5hyOcja+UIHOJY9hS3BBEo0DtDA==}
'@welshman/util@0.6.5': '@welshman/util@0.6.8':
resolution: {integrity: sha512-BmKgDtgSk0RSnw3YyExN8Mm25TJuJnjvE/7foTENpf2bMo2+PTXwVERNmgDEHRq9MCbmTkPv4h7kJTbpywLMVA==} resolution: {integrity: sha512-Q4x3Jm3yIk4zORYOscMuxyC7fJGyZFetE5U4PVYNrvgtSLCtULYKs1y6WkAra4FD7zfAa7lqzTlQq4uIZWzdkA==}
'@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==}
@@ -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.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)': '@welshman/app@0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@types/throttle-debounce': 5.0.2 '@types/throttle-debounce': 5.0.2
'@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/feeds': 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/lib': 0.6.5 '@welshman/lib': 0.6.8
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': 0.6.5(typescript@5.9.3)(ws@8.18.3) '@welshman/router': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/signer': 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) '@welshman/signer': 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/store': 0.6.5(typescript@5.9.3)(ws@8.18.3) '@welshman/store': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.5(typescript@5.9.3) '@welshman/util': 0.6.8(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,14 +6669,14 @@ snapshots:
- typescript - typescript
- ws - ws
'@welshman/content@0.6.5(typescript@5.9.3)': '@welshman/content@0.6.8(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.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/editor@0.6.8(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)':
dependencies: 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.27.1(@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))
@@ -6691,8 +6691,8 @@ snapshots:
'@tiptap/extension-text': 2.27.1(@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.27.1(@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.5 '@welshman/lib': 0.6.8
'@welshman/util': 0.6.5(typescript@5.9.3) '@welshman/util': 0.6.8(typescript@5.9.3)
nostr-editor: 1.0.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))) nostr-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
@@ -6707,71 +6707,71 @@ snapshots:
- tiptap-markdown - tiptap-markdown
- typescript - typescript
'@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/feeds@0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.5 '@welshman/lib': 0.6.8
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': 0.6.5(typescript@5.9.3)(ws@8.18.3) '@welshman/router': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/signer': 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) '@welshman/signer': 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.5(typescript@5.9.3) '@welshman/util': 0.6.8(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.5': '@welshman/lib@0.6.8':
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.5(typescript@5.9.3)(ws@8.18.3)': '@welshman/net@0.6.8(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.5 '@welshman/lib': 0.6.8
'@welshman/util': 0.6.5(typescript@5.9.3) '@welshman/util': 0.6.8(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.5(typescript@5.9.3)(ws@8.18.3)': '@welshman/router@0.6.8(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.5 '@welshman/lib': 0.6.8
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.5(typescript@5.9.3) '@welshman/util': 0.6.8(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@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/signer@0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
dependencies: 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.5 '@welshman/lib': 0.6.8
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.5(typescript@5.9.3) '@welshman/util': 0.6.8(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.5(typescript@5.9.3)(ws@8.18.3)': '@welshman/store@0.6.8(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.5 '@welshman/lib': 0.6.8
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.5(typescript@5.9.3) '@welshman/util': 0.6.8(typescript@5.9.3)
svelte: 4.2.20 svelte: 4.2.20
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/util@0.6.5(typescript@5.9.3)': '@welshman/util@0.6.8(typescript@5.9.3)':
dependencies: dependencies:
'@types/ws': 8.18.1 '@types/ws': 8.18.1
'@welshman/lib': 0.6.5 '@welshman/lib': 0.6.8
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
+2 -2
View File
@@ -89,7 +89,7 @@
</script> </script>
<div class="col-4"> <div class="col-4">
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-6 shadow-md">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<strong class="flex items-center gap-3"> <strong class="flex items-center gap-3">
<Icon icon={Inbox} /> <Icon icon={Inbox} />
@@ -108,7 +108,7 @@
{/each} {/each}
</div> </div>
</div> </div>
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-4 shadow-md">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<strong class="flex items-center gap-3"> <strong class="flex items-center gap-3">
<Icon icon={Bell} /> <Icon icon={Bell} />
+1 -1
View File
@@ -19,7 +19,7 @@
</script> </script>
<Link <Link
class="col-3 card2 bg-alt w-full cursor-pointer shadow-xl" class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
href={makeCalendarPath(url, event.id)}> 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">
+4 -12
View File
@@ -2,21 +2,14 @@
import {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {hash, formatTimestampAsTime} from "@welshman/lib" import {hash, formatTimestampAsTime} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import { import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
thunks,
mergeThunks,
pubkey,
deriveProfile,
deriveProfileDisplay,
sendWrapped,
} from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.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 Tippy from "@lib/components/Tippy.svelte" import Tippy from "@lib/components/Tippy.svelte"
import TapTarget from "@lib/components/TapTarget.svelte" import TapTarget from "@lib/components/TapTarget.svelte"
import Avatar from "@lib/components/Avatar.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkFailure from "@app/components/ThunkFailure.svelte" import ThunkFailure from "@app/components/ThunkFailure.svelte"
@@ -37,7 +30,6 @@
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props() const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
const isOwn = event.pubkey === $pubkey const isOwn = event.pubkey === $pubkey
const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id)) const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
@@ -107,8 +99,8 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{#if !isOwn} {#if !isOwn}
<Button onclick={openProfile} class="flex items-center gap-1"> <Button onclick={openProfile} class="flex items-center gap-1">
<Avatar <ProfileCircle
src={$profile?.picture} pubkey={event.pubkey}
class="border border-solid border-base-content" class="border border-solid border-base-content"
size={4} /> size={4} />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
+1 -1
View File
@@ -31,7 +31,7 @@
}) })
</script> </script>
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}> <ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md" bind:this={ul}>
<li> <li>
<Button onclick={createGoal}> <Button onclick={createGoal}>
<Icon size={4} icon={StarFallMinimalistic} /> <Icon size={4} icon={StarFallMinimalistic} />
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import type {ProfilePointer} from "@welshman/content" import type {ProfilePointer} from "@welshman/content"
import {deriveProfileDisplay} from "@welshman/app" import {deriveProfileDisplay} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -13,7 +13,7 @@
const {value, url}: Props = $props() const {value, url}: Props = $props()
const display = deriveProfileDisplay(value.pubkey, removeNil([url])) const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
</script> </script>
+1 -1
View File
@@ -54,7 +54,7 @@
}) })
</script> </script>
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}> <ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md" bind:this={ul}>
{#if isRoot} {#if isRoot}
<li> <li>
<Button onclick={share}> <Button onclick={share}>
+1 -1
View File
@@ -20,7 +20,7 @@
const h = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
</script> </script>
<Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl" href={makeGoalPath(url, event.id)}> <Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-md" href={makeGoalPath(url, event.id)}>
<p class="text-2xl">{event.content}</p> <p class="text-2xl">{event.content}</p>
<Content <Content
event={{content: summary, tags: event.tags}} event={{content: summary, tags: event.tags}}
+1 -1
View File
@@ -43,7 +43,7 @@
} }
</script> </script>
<div class="w-96 rounded-box bg-base-100 p-4 shadow-lg"> <div class="w-96 rounded-box bg-base-100 p-4 shadow-2xl">
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Magnifier} /> <Icon icon={Magnifier} />
<input bind:value={searchTerm} class="grow" type="text" placeholder="Search icons..." /> <input bind:value={searchTerm} class="grow" type="text" placeholder="Search icons..." />
+4 -2
View File
@@ -8,6 +8,7 @@
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl" import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import SquareTopDown from "@assets/icons/square-top-down.svg?dataurl"
import Exit from "@assets/icons/logout-3.svg?dataurl" import Exit from "@assets/icons/logout-3.svg?dataurl"
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"
@@ -133,7 +134,7 @@
<Popover hideOnClick onClose={toggleMenu}> <Popover hideOnClick onClose={toggleMenu}>
<ul <ul
transition:fly transition:fly
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl"> class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-md">
<li> <li>
<Button onclick={createInvite}> <Button onclick={createInvite}>
<Icon icon={LinkRound} /> <Icon icon={LinkRound} />
@@ -157,6 +158,7 @@
<Link external href="https://landlubber.coracle.social"> <Link external href="https://landlubber.coracle.social">
<Icon icon={Tuning2} /> <Icon icon={Tuning2} />
Manage Space Manage Space
<Icon icon={SquareTopDown} size={4} class="opacity-50" />
</Link> </Link>
</li> </li>
{:else if $relay?.pubkey} {:else if $relay?.pubkey}
@@ -184,7 +186,7 @@
</Popover> </Popover>
{/if} {/if}
</div> </div>
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto"> <div class="flex max-h-[calc(100vh-250px)] min-h-0 flex-col gap-1 overflow-auto">
{#if hasNip29($relay)} {#if hasNip29($relay)}
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}> <SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
<Icon icon={History} /> Recent Activity <Icon icon={History} /> Recent Activity
+3 -3
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte" import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte" import RelayName from "@app/components/RelayName.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte" import RelayDescription from "@app/components/RelayDescription.svelte"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
@@ -13,9 +13,9 @@
</script> </script>
<Link replaceState href={path}> <Link replaceState href={path}>
<CardButton class="btn-neutral"> <CardButton class="btn-neutral shadow-md">
{#snippet icon()} {#snippet icon()}
<div><SpaceAvatar {url} /></div> <RelayIcon {url} size={12} />
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<div class="flex gap-1"> <div class="flex gap-1">
+1 -1
View File
@@ -18,7 +18,7 @@
const openProfile = () => pushModal(ProfileDetail, {pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
</script> </script>
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-4 shadow-md">
<div class="flex justify-between"> <div class="flex justify-between">
<Profile {pubkey} {url} /> <Profile {pubkey} {url} />
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex"> <Button onclick={openProfile} class="btn btn-primary hidden sm:flex">
+23 -18
View File
@@ -4,7 +4,15 @@
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {splitAt} from "@welshman/lib" import {splitAt} from "@welshman/lib"
import {userProfile, shouldUnwrap} from "@welshman/app" import {userProfile, shouldUnwrap} from "@welshman/app"
import Avatar from "@lib/components/Avatar.svelte" import Widget from "@assets/icons/widget.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import Settings from "@assets/icons/settings.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte" import ChatEnable from "@app/components/ChatEnable.svelte"
@@ -15,13 +23,6 @@
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
import Widget from "@assets/icons/widget.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import Settings from "@assets/icons/settings.svg?dataurl"
type Props = { type Props = {
children?: Snippet children?: Snippet
@@ -61,7 +62,7 @@
<PrimaryNavItemSpace {url} /> <PrimaryNavItemSpace {url} />
{:else} {:else}
<PrimaryNavItem title="Home" href="/home" class="tooltip-right"> <PrimaryNavItem title="Home" href="/home" class="tooltip-right">
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" /> <ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" />
</PrimaryNavItem> </PrimaryNavItem>
<Divider /> <Divider />
{#each primarySpaceUrls as url (url)} {#each primarySpaceUrls as url (url)}
@@ -73,11 +74,11 @@
class="tooltip-right" class="tooltip-right"
onclick={showOtherSpacesMenu} onclick={showOtherSpacesMenu}
notification={otherSpaceNotifications}> notification={otherSpaceNotifications}>
<Avatar icon={Widget} class="!h-10 !w-10" /> <ImageIcon alt="Other Spaces" src={Widget} />
</PrimaryNavItem> </PrimaryNavItem>
{/if} {/if}
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right"> <PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
<Avatar icon={Compass} class="!h-10 !w-10" /> <ImageIcon alt="Add a Space" src={Compass} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
{/each} {/each}
</div> </div>
@@ -90,17 +91,17 @@
href="/settings/profile" href="/settings/profile"
prefix="/settings" prefix="/settings"
class="tooltip-right"> class="tooltip-right">
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" /> <ImageIcon alt="Settings" src={$userProfile?.picture || UserRounded} class="rounded-full" />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem <PrimaryNavItem
title="Messages" title="Messages"
onclick={openChat} onclick={openChat}
class="tooltip-right" class="tooltip-right"
notification={$notifications.has("/chat")}> notification={$notifications.has("/chat")}>
<Avatar icon={Letter} class="!h-10 !w-10" /> <ImageIcon alt="Messages" src={Letter} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem title="Search" href="/people" class="tooltip-right"> <PrimaryNavItem title="Search" href="/people" class="tooltip-right">
<Avatar icon={Magnifier} class="!h-10 !w-10" /> <ImageIcon alt="Search" src={Magnifier} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
</div> </div>
</div> </div>
@@ -115,22 +116,26 @@
<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-6"> <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" /> <ImageIcon alt="Home" src={HomeSmile} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem <PrimaryNavItem
title="Messages" title="Messages"
onclick={openChat} onclick={openChat}
notification={$notifications.has("/chat")}> notification={$notifications.has("/chat")}>
<Avatar icon={Letter} class="!h-10 !w-10" /> <ImageIcon alt="Messages" src={Letter} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
{#if PLATFORM_RELAYS.length !== 1} {#if PLATFORM_RELAYS.length !== 1}
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}> <PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
<Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" /> <ImageIcon alt="Spaces" src={SettingsMinimalistic} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
{/if} {/if}
</div> </div>
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}> <PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
<Avatar icon={Settings} src={$userProfile?.picture} class="!h-10 !w-10" /> <ImageIcon
alt="Settings"
src={$userProfile?.picture || Settings}
size={7}
class="rounded-full" />
</PrimaryNavItem> </PrimaryNavItem>
</div> </div>
</div> </div>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
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 RelayIcon from "@app/components/RelayIcon.svelte"
import {makeSpacePath, goToSpace} from "@app/util/routes" import {makeSpacePath, goToSpace} from "@app/util/routes"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
@@ -15,5 +15,5 @@
title={displayRelayUrl(url)} title={displayRelayUrl(url)}
class="tooltip-right" class="tooltip-right"
notification={$notifications.has(makeSpacePath(url))}> notification={$notifications.has(makeSpacePath(url))}>
<SpaceAvatar {url} /> <RelayIcon {url} size={10} class="rounded-full" />
</PrimaryNavItem> </PrimaryNavItem>
+5 -11
View File
@@ -1,16 +1,11 @@
<script lang="ts"> <script lang="ts">
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {displayPubkey} from "@welshman/util" import {displayPubkey} from "@welshman/util"
import { import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
deriveHandleForPubkey,
displayHandle,
deriveProfile,
deriveProfileDisplay,
} from "@welshman/app"
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 Avatar from "@lib/components/Avatar.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import WotScore from "@app/components/WotScore.svelte" import WotScore from "@app/components/WotScore.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
@@ -26,8 +21,7 @@
const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props() const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props()
const relays = removeNil([url]) const relays = removeUndefined([url])
const profile = deriveProfile(pubkey, relays)
const profileDisplay = deriveProfileDisplay(pubkey, relays) const profileDisplay = deriveProfileDisplay(pubkey, relays)
const handle = deriveHandleForPubkey(pubkey) const handle = deriveHandleForPubkey(pubkey)
@@ -38,7 +32,7 @@
<div class="flex max-w-full items-start gap-3"> <div class="flex max-w-full items-start gap-3">
<Button onclick={openProfile} class="py-1"> <Button onclick={openProfile} class="py-1">
<Avatar src={$profile?.picture} size={avatarSize} /> <ProfileCircle {pubkey} size={avatarSize} />
</Button> </Button>
<div class="flex min-w-0 flex-col"> <div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
+14 -7
View File
@@ -1,17 +1,24 @@
<script lang="ts"> <script lang="ts">
import Avatar from "@lib/components/Avatar.svelte" import cx from "classnames"
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {deriveProfile} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import UserCircle from "@assets/icons/user-circle.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = { type Props = {
pubkey: string pubkey: string
class?: string
size?: number
url?: string url?: string
} & Record<string, any> }
const {pubkey, url, ...props}: Props = $props() const {pubkey, url, size = 7, ...props}: Props = $props()
const profile = deriveProfile(pubkey, removeNil([url])) const profile = deriveProfile(pubkey, removeUndefined([url]))
</script> </script>
<Avatar src={$profile?.picture} icon={UserCircle} {...props} /> <ImageIcon
{size}
class={cx(props.class, "rounded-full")}
src={$profile?.picture || UserRounded}
alt="Profile picture" />
+8 -3
View File
@@ -1,13 +1,18 @@
<script lang="ts"> <script lang="ts">
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
const {...props} = $props() type Props = {
pubkeys: string[]
size?: number
}
const {pubkeys, size = 7}: Props = $props()
</script> </script>
<div class="flex pr-3"> <div class="flex pr-3">
{#each props.pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)} {#each pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
<div class="z-feature -mr-3 inline-block"> <div class="z-feature -mr-3 inline-block">
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {...props} /> <ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
</div> </div>
{/each} {/each}
</div> </div>
+2 -2
View File
@@ -4,7 +4,7 @@
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl" import Letter from "@assets/icons/letter-opened.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Avatar from "@lib/components/Avatar.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
@@ -41,7 +41,7 @@
</Button> </Button>
<div class="flex gap-2"> <div class="flex gap-2">
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral"> <Link external href={pubkeyLink(pubkey)} class="btn btn-neutral">
<Avatar src="/coracle.png" /> <ImageIcon alt="Open in Coracle" src="/coracle.png" />
Open in Coracle Open in Coracle
</Link> </Link>
<Button onclick={openChat} class="btn btn-primary"> <Button onclick={openChat} class="btn btn-primary">
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {deriveProfile} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
@@ -10,7 +10,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profile = deriveProfile(pubkey, removeNil([url])) const profile = deriveProfile(pubkey, removeUndefined([url]))
</script> </script>
{#if $profile} {#if $profile}
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {deriveProfileDisplay} from "@welshman/app" import {deriveProfileDisplay} from "@welshman/app"
type Props = { type Props = {
@@ -9,7 +9,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profileDisplay = deriveProfileDisplay(pubkey, removeNil([url])) const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
</script> </script>
{$profileDisplay} {$profileDisplay}
+2 -2
View File
@@ -5,7 +5,7 @@
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte" import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte" import RelayName from "@app/components/RelayName.svelte"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {deriveGroupSelections, getSpaceUrlsFromGroupSelections} from "@app/core/state" import {deriveGroupSelections, getSpaceUrlsFromGroupSelections} from "@app/core/state"
@@ -26,7 +26,7 @@
{#each spaceUrls as url (url)} {#each spaceUrls as url (url)}
<div class="card2 bg-alt flex flex-row items-center gap-2"> <div class="card2 bg-alt flex flex-row items-center gap-2">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<SpaceAvatar {url} /> <RelayIcon {url} size={12} />
</div> </div>
<div class="flex flex-grow flex-col"> <div class="flex flex-grow flex-col">
<RelayName {url} /> <RelayName {url} />
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import {deriveRelay} from "@welshman/app"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = {
url: string
size?: number
class?: string
}
const {url, size = 7, ...props}: Props = $props()
const relay = deriveRelay(url)
</script>
<ImageIcon
{size}
src={$relay?.icon || RemoteControllerMinimalistic}
alt="Relay image"
class={props.class} />
+5 -1
View File
@@ -1,7 +1,11 @@
<script lang="ts"> <script lang="ts">
import {deriveRelayDisplay} from "@welshman/app" import {deriveRelayDisplay} from "@welshman/app"
const {url} = $props() type Props = {
url: string
}
const {url}: Props = $props()
const display = $derived(deriveRelayDisplay(url)) const display = $derived(deriveRelayDisplay(url))
</script> </script>
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
import Lock from "@assets/icons/lock.svg?dataurl"
import Microphone from "@assets/icons/microphone.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {deriveRoom} from "@app/core/state"
interface Props {
h: any
url: any
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
</script>
{#if $room.isHidden}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="This room is not visible to non-members.">
<Icon size={4} icon={EyeClosed} />
</Button>
{:else if $room.isPrivate}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Only members can view messages.">
<Icon size={4} icon={Lock} />
</Button>
{:else if $room.isRestricted}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Only members can send messages.">
<Icon size={4} icon={Microphone} />
</Button>
{/if}
+190
View File
@@ -0,0 +1,190 @@
<script lang="ts">
import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
import type {Thunk} from "@welshman/app"
import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
import Pen from "@assets/icons/pen.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Login3 from "@assets/icons/login-3.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import Lock from "@assets/icons/lock.svg?dataurl"
import Microphone from "@assets/icons/microphone.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import ProfileList from "@app/components/ProfileList.svelte"
import RoomEdit from "@app/components/RoomEdit.svelte"
import RoomName from "@app/components/RoomName.svelte"
import RoomImage from "@app/components/RoomImage.svelte"
import {
deriveRoom,
deriveRoomMembers,
deriveUserIsRoomAdmin,
deriveUserRoomMembershipStatus,
MembershipStatus,
} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
type Props = {
url: string
h: string
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
const members = deriveRoomMembers(url, h)
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
const back = () => history.back()
const startEdit = () => pushModal(RoomEdit, {url, h})
const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
loading = true
try {
const message = await waitForThunkError(f(url, makeRoomMeta({h})))
if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message})
}
} finally {
loading = false
}
}
const join = () => handleLoading(joinRoom)
const leave = () => handleLoading(leaveRoom)
const showMembers = () =>
pushModal(ProfileList, {
title: "Members",
subtitle: `of ${$room?.name || h}`,
pubkeys: $members,
})
const startDelete = () =>
pushModal(Confirm, {
title: "Are you sure you want to delete this room?",
message:
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
confirm: async () => {
const thunk = deleteRoom(url, $room)
const message = await waitForThunkError(thunk)
if (message) {
repository.removeEvent(thunk.event.id)
pushToast({theme: "error", message})
} else {
goto(makeSpacePath(url))
}
},
})
let loading = $state(false)
</script>
<div class="flex flex-col gap-3">
<div class="flex justify-between">
<div class="flex gap-3">
<RoomImage {url} {h} size={8} />
<div class="flex min-w-0 flex-col">
<RoomName {url} {h} class="text-2xl" />
<span class="text-primary">{displayRelayUrl(url)}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
{#if $room?.isRestricted}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Only members can send messages.">
<Icon size={4} icon={Microphone} />
</Button>
{/if}
{#if $room?.isPrivate}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Only members can view messages.">
<Icon size={4} icon={Lock} />
</Button>
{/if}
{#if $room?.isHidden}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="This room is not visible to non-members.">
<Icon size={4} icon={EyeClosed} />
</Button>
{/if}
{#if $room?.isClosed}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Requests to join this room will be ignored.">
<Icon size={4} icon={MinusCircle} />
</Button>
{/if}
</div>
</div>
{#if $room?.about}
<p>{$room.about}</p>
{/if}
{#if $members.length > 0}
<div class="card2 card2-sm bg-alt flex gap-4">
<span>Members:</span>
<Button onclick={showMembers}>
<ProfileCircles pubkeys={$members} />
</Button>
</div>
{/if}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<div class="flex gap-2">
{#if $userIsAdmin}
<Button class="btn btn-outline btn-error" onclick={startDelete}>
<Icon icon={TrashBin2} />
<span class="hidden md:inline">Delete Room</span>
</Button>
<Button class="btn btn-primary" onclick={startEdit}>
<Icon icon={Pen} />
Edit Room
</Button>
{:else if $membershipStatus === MembershipStatus.Initial}
<Button class="btn btn-neutral" disabled={loading} onclick={join}>
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon={Login3} />
{/if}
Join member list
</Button>
{:else if $membershipStatus === MembershipStatus.Pending}
<Button class="btn btn-neutral">
<Icon icon={ClockCircle} />
Membership pending
</Button>
{:else}
<Button class="btn btn-neutral" disabled={loading} onclick={leave}>
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon={Login3} />
{/if}
Leave member list
</Button>
{/if}
</div>
</ModalFooter>
</div>
+3 -32
View File
@@ -2,20 +2,15 @@
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util" import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl} 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 AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
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 Confirm from "@lib/components/Confirm.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 RoomForm from "@app/components/RoomForm.svelte" import RoomForm from "@app/components/RoomForm.svelte"
import {deriveRoom} from "@app/core/state" import {deriveRoom} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
type Props = { type Props = {
url: string url: string
@@ -29,24 +24,6 @@
const back = () => history.back() const back = () => history.back()
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, h)) 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> </script>
<RoomForm {url} {onsubmit} initialValues={$room}> <RoomForm {url} {onsubmit} initialValues={$room}>
@@ -68,15 +45,9 @@
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<div class="flex gap-2"> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Button class="btn btn-outline btn-error" onclick={startDelete}> <Spinner {loading}>Save Changes</Spinner>
<Icon icon={TrashBin2} /> </Button>
<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> </ModalFooter>
{/snippet} {/snippet}
</RoomForm> </RoomForm>
+22
View File
@@ -0,0 +1,22 @@
<script lang="ts">
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import {deriveRoom} from "@app/core/state"
interface Props {
h: string
url: string
size?: number
}
const {url, h, size = 5}: Props = $props()
const room = deriveRoom(url, h)
</script>
{#if $room.picture}
<ImageIcon src={$room.picture} {size} alt="Room icon" />
{:else}
<Icon icon={Hashtag} {size} />
{/if}
+5 -4
View File
@@ -7,7 +7,6 @@
thunks, thunks,
pubkey, pubkey,
mergeThunks, mergeThunks,
deriveProfile,
deriveProfileDisplay, deriveProfileDisplay,
displayProfileByPubkey, displayProfileByPubkey,
} from "@welshman/app" } from "@welshman/app"
@@ -16,12 +15,12 @@
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import ReplyAlt from "@assets/icons/reply.svg?dataurl" import ReplyAlt from "@assets/icons/reply.svg?dataurl"
import TapTarget from "@lib/components/TapTarget.svelte" import TapTarget from "@lib/components/TapTarget.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
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 ProfileCircle from "@app/components/ProfileCircle.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte" import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte"
import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte" import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte"
@@ -56,7 +55,6 @@
const path = getRoomItemPath(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 profileDisplay = deriveProfileDisplay(event.pubkey, [url]) const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id)) const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
@@ -83,7 +81,10 @@
<div class="flex w-full gap-3 overflow-auto"> <div class="flex w-full gap-3 overflow-auto">
{#if showPubkey} {#if showPubkey}
<Button onclick={openProfile} class="flex items-start"> <Button onclick={openProfile} class="flex items-start">
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} /> <ProfileCircle
pubkey={event.pubkey}
class="border border-solid border-base-content"
size={8} />
</Button> </Button>
{:else} {:else}
<div class="w-8 min-w-8 max-w-8"></div> <div class="w-8 min-w-8 max-w-8"></div>
+1 -1
View File
@@ -35,7 +35,7 @@
} }
</script> </script>
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl"> <ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md">
<li> <li>
<Button onclick={showInfo}> <Button onclick={showInfo}>
<Icon size={4} icon={Code2} /> <Icon size={4} icon={Code2} />
+13 -3
View File
@@ -1,7 +1,17 @@
<script lang="ts"> <script lang="ts">
import {roomsById, makeRoomId} from "@app/core/state" import {deriveRoom} from "@app/core/state"
const {url, h} = $props() type Props = {
url: string
h: string
class?: string
}
const {url, h, ...props}: Props = $props()
const room = deriveRoom(url, h)
</script> </script>
{$roomsById.get(makeRoomId(url, h))?.name || h} <span class="ellipsize {props.class}">
{$room?.name || h}
</span>
+12 -17
View File
@@ -1,26 +1,21 @@
<script lang="ts"> <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 RoomName from "@app/components/RoomName.svelte"
import {deriveRoom} from "@app/core/state" import RoomImage from "@app/components/RoomImage.svelte"
interface Props { interface Props {
url: any h: string
h: any url: string
class?: string
} }
const {url, h}: Props = $props() const {url, h, ...props}: Props = $props()
const room = deriveRoom(url, h)
</script> </script>
{#if $room.picture} <div class="flex flex-grow items-center justify-between gap-4 {props.class}">
{@const src = $room.picture} <div class="flex items-center gap-3">
<ImageIcon {src} alt="Room icon" /> <RoomImage {url} {h} />
{:else} <div class="min-w-0 overflow-hidden text-ellipsis">
<Icon icon={Hashtag} /> <RoomName {url} {h} />
{/if} </div>
<div class="min-w-0 overflow-hidden text-ellipsis"> </div>
<RoomName {url} {h} />
</div> </div>
+26 -4
View File
@@ -12,12 +12,29 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte" import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {removeSpaceMembership, publishLeaveRequest, removeTrustedRelay} from "@app/core/commands"
const {url, error} = $props() const {url, error} = $props()
const back = () => goto("/home") const back = () => goto("/home")
const requestAccess = () => pushModal(SpaceAccessRequest, {url}) const requestAccess = () => pushModal(SpaceAccessRequest, {url})
const leaveSpace = async () => {
loading = true
try {
await removeSpaceMembership(url)
await publishLeaveRequest({url})
await removeTrustedRelay(url)
} finally {
loading = false
}
goto("/home")
}
let loading = $state(false)
</script> </script>
<form class="column gap-4" onsubmit={preventDefault(requestAccess)}> <form class="column gap-4" onsubmit={preventDefault(requestAccess)}>
@@ -40,9 +57,14 @@
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go Home Go Home
</Button> </Button>
<Button type="submit" class="btn btn-primary"> <div class="flex gap-2">
Request Access <Button class="btn btn-outline btn-error" onclick={leaveSpace} disabled={loading}>
<Icon icon={AltArrowRight} /> Leave Space
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
Request Access
<Icon icon={AltArrowRight} />
</Button>
</div>
</ModalFooter> </ModalFooter>
</form> </form>
-20
View File
@@ -1,20 +0,0 @@
<script lang="ts">
import {displayRelayUrl} from "@welshman/util"
import Avatar from "@lib/components/Avatar.svelte"
import {deriveRelay} from "@welshman/app"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
interface Props {
url?: string
}
const {url = ""}: Props = $props()
const relay = deriveRelay(url)
</script>
<Avatar
icon={RemoteControllerMinimalistic}
class="!h-10 !w-10"
alt={displayRelayUrl(url)}
src={$relay?.icon} />
+11 -6
View File
@@ -10,19 +10,24 @@
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 SpaceVisitConfirm, {confirmSpaceVisit} from "@app/components/SpaceVisitConfirm.svelte" import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
import {attemptRelayAccess} from "@app/core/commands" import {attemptRelayAccess} from "@app/core/commands"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
const {url} = $props() const {url} = $props()
const back = () => history.back() const back = () => history.back()
const next = () => { const next = async () => {
if (!error && Pool.get().get(url).auth.status === AuthStatus.None) { if (error) {
pushModal(SpaceVisitConfirm, {url}, {replaceState: true}) return pushToast({theme: "error", message: error, timeout: 30_000})
}
if (Pool.get().get(url).auth.status === AuthStatus.None) {
pushModal(SpaceJoinConfirm, {url}, {replaceState: true})
} else { } else {
confirmSpaceVisit(url) await confirmSpaceJoin(url)
} }
} }
@@ -72,7 +77,7 @@
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading}> <Button type="submit" class="btn btn-primary" disabled={loading}>
Go to Space Join Space
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
</ModalFooter> </ModalFooter>
+23 -1
View File
@@ -2,16 +2,22 @@
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {deriveRelay} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
import ShieldUser from "@assets/icons/shield-user.svg?dataurl" import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
import BillList from "@assets/icons/bill-list.svg?dataurl" import BillList from "@assets/icons/bill-list.svg?dataurl"
import Ghost from "@assets/icons/ghost-smile.svg?dataurl" import Ghost from "@assets/icons/ghost-smile.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import RelayName from "@app/components/RelayName.svelte" import RelayName from "@app/components/RelayName.svelte"
import SpaceEdit from "@app/components/SpaceEdit.svelte"
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte" import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte" import RelayDescription from "@app/components/RelayDescription.svelte"
import ProfileLatest from "@app/components/ProfileLatest.svelte" import ProfileLatest from "@app/components/ProfileLatest.svelte"
import {deriveUserIsSpaceAdmin} from "@app/core/state"
import {pushModal} from "@app/util/modal"
type Props = { type Props = {
url: string url: string
@@ -20,8 +26,11 @@
const {url}: Props = $props() const {url}: Props = $props()
const relay = deriveRelay(url) const relay = deriveRelay(url)
const owner = $derived($relay?.pubkey) const owner = $derived($relay?.pubkey)
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const back = () => history.back() const back = () => history.back()
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay})
</script> </script>
<div class="column gap-4"> <div class="column gap-4">
@@ -78,5 +87,18 @@
</div> </div>
{/if} {/if}
</div> </div>
<Button class="btn btn-primary" onclick={back}>Got it</Button> {#if $userIsAdmin}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button class="btn btn-primary" onclick={startEdit}>
<Icon icon={Pen} />
Edit Space
</Button>
</ModalFooter>
{:else}
<Button class="btn btn-primary" onclick={back}>Got it</Button>
{/if}
</div> </div>
+202
View File
@@ -0,0 +1,202 @@
<script lang="ts">
import {uniqBy, prop, append, ifLet} from "@welshman/lib"
import type {RelayProfile} from "@welshman/util"
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {manageRelay, relays, fetchRelayProfileDirectly} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
import {preventDefault} from "@lib/html"
import FieldInline from "@lib/components/FieldInline.svelte"
import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
import {pushToast} from "@app/util/toast"
import {clearModals} from "@app/util/modal"
import {uploadFile} from "@app/core/commands"
type Props = {
url: string
initialValues: RelayProfile
}
const {url, initialValues}: Props = $props()
const values = $state(initialValues)
const back = () => history.back()
const submit = async () => {
if (values.name != initialValues.name) {
const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayName,
params: [values.name || ""],
})
if (res.error) {
return pushToast({theme: "error", message: res.error})
}
}
if (values.description != initialValues.description) {
const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayDescription,
params: [values.description || ""],
})
if (res.error) {
return pushToast({theme: "error", message: res.error})
}
}
if (imageFile) {
const {error, result} = await uploadFile(imageFile, {maxWidth: 128, maxHeight: 128})
console.log(imageFile, result)
if (error) {
return pushToast({theme: "error", message: error})
}
const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayIcon,
params: [result.url],
})
if (res.error) {
return pushToast({theme: "error", message: res.error})
}
}
// Force-reload the relay
ifLet(await fetchRelayProfileDirectly(url), relay => {
relays.update($relays => uniqBy(prop("url"), append(relay, $relays)))
})
pushToast({message: "Your changes have been saved!"})
clearModals()
}
const trySubmit = async () => {
loading = true
try {
await submit()
} finally {
loading = false
}
}
let loading = $state(false)
let imageFile = $state<File | undefined>()
let imagePreview = $state(initialValues.icon)
const handleImageUpload = async (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (file && file.type.startsWith("image/")) {
const reader = new FileReader()
reader.onload = e => {
imageFile = file
imagePreview = e.target?.result as string
}
reader.readAsDataURL(file)
}
}
const handleIconSelect = (iconUrl: string) => {
imagePreview = iconUrl
const parts = iconUrl.split(",")
const imageData = atob(parts[1])
const result = new Uint8Array(imageData.length)
for (let n = 0; n < imageData.length; n++) {
result[n] = imageData.charCodeAt(n)
}
imageFile = new File([result], `icon.svg`, {type: "image/svg+xml"})
}
</script>
<form class="column gap-4" onsubmit={preventDefault(trySubmit)}>
<ModalHeader>
{#snippet title()}
<div>Edit a Space</div>
{/snippet}
{#snippet info()}
<span class="text-primary">{displayRelayUrl(url)}</span>
{/snippet}
</ModalHeader>
<FieldInline>
{#snippet label()}
<p>Icon</p>
{/snippet}
{#snippet input()}
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<ImageIcon src={imagePreview} alt="Room icon preview" />
</div>
{:else}
<span class="text-sm opacity-75">No icon selected</span>
{/if}
<div class="flex gap-2">
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
<Icon icon={StickerSmileSquare} size={4} />
Select
</IconPickerButton>
<label class="btn btn-neutral btn-sm cursor-pointer">
<Icon icon={UploadMinimalistic} size={4} />
Upload
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
</label>
</div>
</div>
</div>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Name</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
{#if imagePreview}
<ImageIcon src={imagePreview} alt="Room icon preview" />
{:else}
<Icon icon={SettingsMinimalistic} />
{/if}
<input bind:value={values.name} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Description</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<input bind:value={values.description} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Save Changes</Spinner>
</Button>
</ModalFooter>
</form>
+1 -1
View File
@@ -10,8 +10,8 @@
await addSpaceMembership(url) await addSpaceMembership(url)
broadcastUserData([url]) broadcastUserData([url])
goto(makeSpacePath(url), {replaceState: true})
relaysMostlyRestricted.update(dissoc(url)) relaysMostlyRestricted.update(dissoc(url))
goto(makeSpacePath(url), {replaceState: true})
pushToast({message: "Welcome to the space!"}) pushToast({message: "Welcome to the space!"})
} }
</script> </script>
@@ -1,24 +0,0 @@
<script module lang="ts">
import {goto} from "$app/navigation"
import {makeSpacePath} from "@app/util/routes"
export const confirmSpaceVisit = (url: string) => {
goto(makeSpacePath(url), {replaceState: true})
}
</script>
<script lang="ts">
import Confirm from "@lib/components/Confirm.svelte"
type Props = {
url: string
}
const {url}: Props = $props()
const confirm = () => confirmSpaceVisit(url)
</script>
<Confirm
{confirm}
message="This space does not appear to limit who can post to it. This can result in a large amount of spam or other objectionable content. Continue?" />
+1 -1
View File
@@ -23,7 +23,7 @@
}) })
</script> </script>
<div class="card2 bg-alt col-2 shadow-2xl"> <div class="card2 bg-alt col-2 shadow-lg">
<p> <p>
Failed to publish to {displayRelayUrl(url)}: {message}. Failed to publish to {displayRelayUrl(url)}: {message}.
</p> </p>
+9 -5
View File
@@ -612,7 +612,7 @@ export const payInvoice = async (invoice: string) => {
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http")) export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
export const hasBlossomSupport = simpleCache(async ([url]: [string]) => { export const fetchHasBlossomSupport = async (url: string) => {
const server = normalizeBlossomUrl(url) const server = normalizeBlossomUrl(url)
const $signer = signer.get() || Nip01Signer.ephemeral() const $signer = signer.get() || Nip01Signer.ephemeral()
const headers: Record<string, string> = { const headers: Record<string, string> = {
@@ -633,7 +633,9 @@ export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
} }
return false return false
}) }
export const hasBlossomSupport = simpleCache(([url]: [string]) => fetchHasBlossomSupport(url))
export type GetBlossomServerOptions = { export type GetBlossomServerOptions = {
url?: string url?: string
@@ -658,6 +660,8 @@ export const getBlossomServer = async (options: GetBlossomServerOptions = {}) =>
export type UploadFileOptions = { export type UploadFileOptions = {
url?: string url?: string
encrypt?: boolean encrypt?: boolean
maxWidth?: number
maxHeight?: number
} }
export type UploadFileResult = { export type UploadFileResult = {
@@ -669,8 +673,8 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
try { try {
const {name, type} = file const {name, type} = file
if (!type.match("image/(webp|gif)")) { if (!type.match("image/(webp|gif|svg)")) {
file = await compressFile(file) file = await compressFile(file, options)
} }
const tags: string[][] = [] const tags: string[][] = []
@@ -701,7 +705,7 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
let {uploaded, url, ...task} = parseJson(text) || {} let {uploaded, url, ...task} = parseJson(text) || {}
if (!uploaded) { if (!uploaded) {
return {error: text} return {error: text || `Failed to upload file (HTTP ${res.status})`}
} }
// Always append correct file extension if we encrypted the file, or if it's missing // Always append correct file extension if we encrypted the file, or if it's missing
+2 -2
View File
@@ -8,7 +8,7 @@ import {
sortBy, sortBy,
now, now,
on, on,
isNotNil, isDefined,
filterVals, filterVals,
fromPairs, fromPairs,
} from "@welshman/lib" } from "@welshman/lib"
@@ -279,6 +279,6 @@ export const requestRelayClaim = async (url: string) => {
export const requestRelayClaims = async (urls: string[]) => export const requestRelayClaims = async (urls: string[]) =>
filterVals( filterVals(
isNotNil, isDefined,
fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))), fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))),
) )
+26 -21
View File
@@ -730,7 +730,7 @@ export const deriveSpaceMembers = (url: string) =>
return getTagValues("member", membersEvent.tags) return getTagValues("member", membersEvent.tags)
} }
const members = new Set() const members = new Set<string>()
for (const event of sortBy(e => e.created_at, $events)) { for (const event of sortBy(e => e.created_at, $events)) {
const pubkeys = getPubkeyTagValues(event.tags) const pubkeys = getPubkeyTagValues(event.tags)
@@ -765,7 +765,7 @@ export const deriveRoomMembers = (url: string, h: string) =>
return getPubkeyTagValues(membersEvent.tags) return getPubkeyTagValues(membersEvent.tags)
} }
const members = new Set() const members = new Set<string>()
for (const event of sortBy(e => -e.created_at, $events)) { for (const event of sortBy(e => -e.created_at, $events)) {
const pubkeys = getPubkeyTagValues(event.tags) const pubkeys = getPubkeyTagValues(event.tags)
@@ -806,18 +806,29 @@ export enum MembershipStatus {
Granted, Granted,
} }
export const deriveUserIsSpaceAdmin = (url: string) => {
const store = writable(false)
manageRelay(url, {method: ManagementMethod.SupportedMethods, params: []}).then(res =>
store.set(Boolean(res.result?.length)),
)
return store
}
export const deriveUserSpaceMembershipStatus = (url: string) => export const deriveUserSpaceMembershipStatus = (url: string) =>
derived( derived(
[ [
pubkey, pubkey,
deriveSpaceMembers(url), deriveSpaceMembers(url),
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]), deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
deriveUserIsSpaceAdmin(url),
], ],
([$pubkey, $members, $events]) => { ([$pubkey, $members, $events, $isAdmin]) => {
const isMember = $members.includes($pubkey) const isMember = $members.includes($pubkey!) || $isAdmin
for (const event of $events) { for (const event of $events) {
if (!getPubkeyTagValues(event.tags).includes($pubkey!)) { if (event.pubkey !== $pubkey) {
continue continue
} }
@@ -834,18 +845,25 @@ export const deriveUserSpaceMembershipStatus = (url: string) =>
}, },
) )
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
derived(
[pubkey, deriveRoomAdmins(url, h), deriveUserIsSpaceAdmin(url)],
([$pubkey, $admins, $isSpaceAdmin]) => $isSpaceAdmin || $admins.includes($pubkey!),
)
export const deriveUserRoomMembershipStatus = (url: string, h: string) => export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
derived( derived(
[ [
pubkey, pubkey,
deriveRoomMembers(url, h), deriveRoomMembers(url, h),
deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]), deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]),
deriveUserIsRoomAdmin(url, h),
], ],
([$pubkey, $members, $events]) => { ([$pubkey, $members, $events, $isAdmin]) => {
const isMember = $members.includes($pubkey) const isMember = $members.includes($pubkey!) || $isAdmin
for (const event of $events) { for (const event of $events) {
if (!getPubkeyTagValues(event.tags).includes($pubkey!)) { if (event.pubkey !== $pubkey) {
continue continue
} }
@@ -872,19 +890,6 @@ export const deriveUserCanCreateRoom = (url: string) =>
}, },
) )
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
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
export const encodeRelay = (url: string) => export const encodeRelay = (url: string) =>
+4 -66
View File
@@ -6,7 +6,6 @@ import {
getListTags, getListTags,
getRelayTagValues, getRelayTagValues,
WRAP, WRAP,
MESSAGE,
ROOM_META, ROOM_META,
ROOM_DELETE, ROOM_DELETE,
ROOM_ADMINS, ROOM_ADMINS,
@@ -37,9 +36,9 @@ import {
repository, repository,
shouldUnwrap, shouldUnwrap,
hasNegentropy, hasNegentropy,
relaysByUrl,
} from "@welshman/app" } from "@welshman/app"
import { import {
REACTION_KINDS,
MESSAGE_KINDS, MESSAGE_KINDS,
CONTENT_KINDS, CONTENT_KINDS,
INDEXER_RELAYS, INDEXER_RELAYS,
@@ -50,7 +49,6 @@ import {
bootstrapPubkeys, bootstrapPubkeys,
decodeRelay, decodeRelay,
getUrlsForEvent, getUrlsForEvent,
hasNip29,
getSpaceUrlsFromGroupSelections, getSpaceUrlsFromGroupSelections,
getSpaceRoomsFromGroupSelections, getSpaceRoomsFromGroupSelections,
makeCommentFilter, makeCommentFilter,
@@ -267,9 +265,11 @@ const syncSpace = (url: string) => {
{kinds: [RELAY_MEMBERS]}, {kinds: [RELAY_MEMBERS]},
{kinds: [ROOM_META, ROOM_DELETE]}, {kinds: [ROOM_META, ROOM_DELETE]},
{kinds: [ROOM_ADMINS, ROOM_MEMBERS]}, {kinds: [ROOM_ADMINS, ROOM_MEMBERS]},
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]},
{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}, {kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]},
...MESSAGE_KINDS.map(kind => ({kinds: [kind]})), ...MESSAGE_KINDS.map(kind => ({kinds: [kind]})),
makeCommentFilter(CONTENT_KINDS), makeCommentFilter(CONTENT_KINDS),
{kinds: REACTION_KINDS, limit: 0},
], ],
}) })
@@ -328,68 +328,6 @@ const syncSpaces = () => {
} }
} }
// 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) => {
@@ -479,7 +417,7 @@ const syncDMs = () => {
// Merge all synchronization functions // Merge all synchronization functions
export const syncApplicationData = () => { export const syncApplicationData = () => {
const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncRooms(), syncDMs()] const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncDMs()]
return () => unsubscribers.forEach(call) return () => unsubscribers.forEach(call)
} }
+2 -2
View File
@@ -1,12 +1,12 @@
import type {NodeViewProps} from "@tiptap/core" import type {NodeViewProps} from "@tiptap/core"
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {deriveProfileDisplay} from "@welshman/app" import {deriveProfileDisplay} from "@welshman/app"
export const makeMentionNodeView = export const makeMentionNodeView =
(url?: string) => (url?: string) =>
({node}: NodeViewProps) => { ({node}: NodeViewProps) => {
const dom = document.createElement("span") const dom = document.createElement("span")
const display = deriveProfileDisplay(node.attrs.pubkey, removeNil([url])) const display = deriveProfileDisplay(node.attrs.pubkey, removeUndefined([url]))
dom.classList.add("tiptap-object") dom.classList.add("tiptap-object")
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {displayPubkey} from "@welshman/util" import {displayPubkey} from "@welshman/util"
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app" import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
import WotScore from "@app/components/WotScore.svelte" import WotScore from "@app/components/WotScore.svelte"
@@ -13,7 +13,7 @@
const {value, url}: Props = $props() const {value, url}: Props = $props()
const pubkey = value const pubkey = value
const profileDisplay = deriveProfileDisplay(pubkey, removeNil([url])) const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
const handle = deriveHandleForPubkey(pubkey) const handle = deriveHandleForPubkey(pubkey)
</script> </script>
+1 -8
View File
@@ -1,13 +1,7 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import type {Nip46ResponseWithResult} from "@welshman/signer" import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker, makeSecret} from "@welshman/signer" import {Nip46Broker, makeSecret} from "@welshman/signer"
import { import {PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/core/state"
NIP46_PERMS,
PLATFORM_URL,
PLATFORM_NAME,
PLATFORM_LOGO,
SIGNER_RELAYS,
} from "@app/core/state"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
export class Nip46Controller { export class Nip46Controller {
@@ -25,7 +19,6 @@ export class Nip46Controller {
async start() { async start() {
const url = await this.broker.makeNostrconnectUrl({ const url = await this.broker.makeNostrconnectUrl({
perms: NIP46_PERMS,
url: PLATFORM_URL, url: PLATFORM_URL,
name: PLATFORM_NAME, name: PLATFORM_NAME,
image: PLATFORM_LOGO, image: PLATFORM_LOGO,
+11 -9
View File
@@ -58,14 +58,18 @@ export const trustPolicy = (socket: Socket) => {
export const mostlyRestrictedPolicy = (socket: Socket) => { export const mostlyRestrictedPolicy = (socket: Socket) => {
let total = 0 let total = 0
let restricted = 0 let restricted = 0
let error = ""
const pending = new Set<string>() const pending = new Set<string>()
const updateStatus = () => const updateStatus = (error?: string) => {
relaysMostlyRestricted.update( if (restricted > total / 2) {
restricted > total / 2 ? assoc(socket.url, error) : dissoc(socket.url), if (error) {
) return relaysMostlyRestricted.update(assoc(socket.url, error))
}
} else {
relaysMostlyRestricted.update(dissoc(socket.url))
}
}
const unsubscribers = [ const unsubscribers = [
on(socket, SocketEvent.Receive, (message: RelayMessage) => { on(socket, SocketEvent.Receive, (message: RelayMessage) => {
@@ -83,8 +87,7 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
if (details.startsWith("restricted: ")) { if (details.startsWith("restricted: ")) {
restricted++ restricted++
error = details updateStatus(details)
updateStatus()
} }
} }
} }
@@ -103,8 +106,7 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
if (details.startsWith("restricted: ")) { if (details.startsWith("restricted: ")) {
restricted++ restricted++
error = details updateStatus(details)
updateStatus()
} }
} }
} }
-30
View File
@@ -1,30 +0,0 @@
<script lang="ts">
import {onMount} from "svelte"
import Icon from "@lib/components/Icon.svelte"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
const {src = "", size = 7, icon = UserRounded, style = "", ...restProps} = $props()
let element: HTMLElement
const rem = $derived(size * 4)
onMount(() => {
if (src) {
const image = new Image()
image.addEventListener("error", () => {
element?.querySelector(".hidden")?.classList.remove("hidden")
})
image.src = src
}
})
</script>
<div
bind:this={element}
class="{restProps.class} relative !flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-cover bg-center"
style="width: {rem}px; height: {rem}px; min-width: {rem}px; background-image: url({src}); {style}">
<Icon {icon} class={src ? "hidden" : ""} size={Math.round(size * 0.8)} />
</div>
+2 -2
View File
@@ -12,8 +12,8 @@
</script> </script>
<div class="btn flex h-[unset] w-full flex-nowrap py-4 text-left {props.class}"> <div class="btn flex h-[unset] w-full flex-nowrap py-4 text-left {props.class}">
<div class="flex flex-grow flex-row items-start gap-1 sm:pl-2"> <div class="flex flex-grow flex-row items-start gap-4">
<div class="flex h-14 w-12 flex-shrink-0 items-center"> <div class="flex h-14 w-12 flex-shrink-0 items-center justify-center">
{@render props.icon?.()} {@render props.icon?.()}
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
+1 -1
View File
@@ -14,7 +14,7 @@
const extraClass = $derived( const extraClass = $derived(
!fullscreen && !fullscreen &&
cx( cx(
"bg-alt text-base-content overflow-auto text-base-content shadow-xl", "bg-alt text-base-content overflow-auto text-base-content shadow-md",
"px-4 py-6 bottom-0 left-0 right-0 top-20 rounded-t-box absolute", "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", "sm:p-6 sm:max-h-[90vh] sm:w-[520px] sm:rounded-box sm:relative sm:top-0",
), ),
+5 -3
View File
@@ -4,13 +4,15 @@
type Props = { type Props = {
src: string src: string
alt: string alt: string
size?: number
class?: string
} }
const {src, alt}: Props = $props() const {src, alt, size = 5, ...props}: Props = $props()
</script> </script>
{#if src.includes("image/svg") || src.endsWith(".svg")} {#if src.includes("image/svg") || src.endsWith(".svg")}
<Icon icon={src} /> <Icon icon={src} {size} class={props.class} />
{:else} {:else}
<img {src} {alt} class="h-5 w-5 rounded-lg object-cover" /> <img {src} {alt} class="h-{size} w-{size} aspect-square object-cover {props.class}" />
{/if} {/if}
+2 -2
View File
@@ -11,9 +11,9 @@
const {...props}: Props = $props() const {...props}: Props = $props()
</script> </script>
<div data-component="PageBar" class="cw top-sai fixed z-feature p-2 {props.class}"> <div data-component="PageBar" class="cw top-sai fixed z-nav 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-md">
<div class="ellipsize flex items-center gap-4 whitespace-nowrap"> <div class="ellipsize flex items-center gap-4 whitespace-nowrap">
{@render props.icon?.()} {@render props.icon?.()}
{@render props.title?.()} {@render props.title?.()}
+2 -2
View File
@@ -18,7 +18,7 @@
{#if href} {#if href}
<a {href} class="relative z-nav-item flex h-14 w-14 items-center justify-center"> <a {href} class="relative z-nav-item flex h-14 w-14 items-center justify-center">
<div <div
class="avatar cursor-pointer rounded-full p-1 {restProps.class} transition-colors hover:bg-base-300" class="avatar cursor-pointer rounded-full p-2 {restProps.class} transition-colors hover:bg-base-300"
class:bg-base-300={active} class:bg-base-300={active}
class:tooltip={title} class:tooltip={title}
data-tip={title}> data-tip={title}>
@@ -31,7 +31,7 @@
{:else} {:else}
<Button {onclick} class="relative z-nav-item flex h-14 w-14 items-center justify-center"> <Button {onclick} class="relative z-nav-item flex h-14 w-14 items-center justify-center">
<div <div
class="avatar cursor-pointer rounded-full p-1 {restProps.class} transition-colors hover:bg-base-300" class="avatar cursor-pointer rounded-full p-2 {restProps.class} transition-colors hover:bg-base-300"
class:bg-base-300={active} class:bg-base-300={active}
class:tooltip={title} class:tooltip={title}
data-tip={title}> data-tip={title}>
+67
View File
@@ -0,0 +1,67 @@
<style>
:global(.tippy-box[data-theme~="tooltip"]) {
background-color: var(--neutral);
color: var(--neutral-content);
border-radius: 0.5rem;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
}
:global(.tippy-box[data-theme~="tooltip"][data-placement^="top"] > .tippy-arrow::before) {
border-top-color: var(--neutral);
}
:global(.tippy-box[data-theme~="tooltip"][data-placement^="bottom"] > .tippy-arrow::before) {
border-bottom-color: var(--neutral);
}
:global(.tippy-box[data-theme~="tooltip"][data-placement^="left"] > .tippy-arrow::before) {
border-left-color: var(--neutral);
}
:global(.tippy-box[data-theme~="tooltip"][data-placement^="right"] > .tippy-arrow::before) {
border-right-color: var(--neutral);
}
</style>
<script lang="ts">
import "tippy.js/animations/shift-away.css"
import tippy from "tippy.js"
import {onMount} from "svelte"
import {isMobile} from "@lib/html"
let {
content,
arrow = true,
interactive = false,
children = undefined,
instance = $bindable(),
...restProps
} = $props()
let element: Element
onMount(() => {
instance = tippy(element, {
content,
arrow,
interactive,
animation: "shift-away",
theme: "tooltip",
appendTo: document.querySelector(".tippy-target")!,
trigger: isMobile ? "click" : "mouseenter focus",
})
return () => {
instance?.destroy()
}
})
</script>
<div bind:this={element} class={restProps.class}>
{@render children?.()}
</div>
+2 -2
View File
@@ -145,7 +145,7 @@
{#if inviteData} {#if inviteData}
{#key inviteData.url} {#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-md transition-all hover:shadow-lg hover:dark:brightness-[1.1]"
onclick={() => openSpace(inviteData.url, inviteData.claim)}> onclick={() => openSpace(inviteData.url, inviteData.claim)}>
<RelaySummary url={inviteData.url} /> <RelaySummary url={inviteData.url} />
</Button> </Button>
@@ -153,7 +153,7 @@
{/if} {/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-md transition-all hover:shadow-lg hover:dark:brightness-[1.1]"
onclick={() => openSpace(relay.url)}> onclick={() => openSpace(relay.url)}>
<RelaySummary url={relay.url} /> <RelaySummary url={relay.url} />
</Button> </Button>
+3 -3
View File
@@ -35,7 +35,7 @@
<Button onclick={addSpace}> <Button onclick={addSpace}>
<CardButton class="btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div> <Icon icon={AddCircle} size={7} />
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<div>Add a space</div> <div>Add a space</div>
@@ -48,7 +48,7 @@
<Link href="/discover"> <Link href="/discover">
<CardButton class="btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Compass} size={7} /></div> <Icon icon={Compass} size={7} />
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<div>Browse the network</div> <div>Browse the network</div>
@@ -61,7 +61,7 @@
<Button onclick={openChat}> <Button onclick={openChat}>
<CardButton class="btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={ChatRound} size={7} /></div> <Icon icon={ChatRound} size={7} />
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<div>Start a conversation</div> <div>Start a conversation</div>
+2 -2
View File
@@ -25,7 +25,7 @@
<h1 class="mb-4 text-center text-5xl font-bold uppercase">{PLATFORM_NAME}</h1> <h1 class="mb-4 text-center text-5xl font-bold uppercase">{PLATFORM_NAME}</h1>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2"> <div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
{#if Capacitor.getPlatform() !== "ios"} {#if Capacitor.getPlatform() !== "ios"}
<div class="card2 bg-alt flex flex-col gap-2 text-center shadow-2xl"> <div class="card2 bg-alt flex flex-col gap-2 text-center shadow-lg">
<h3 class="text-2xl sm:h-12">Donate</h3> <h3 class="text-2xl sm:h-12">Donate</h3>
<p class="sm:h-16">Funds will be used to support development.</p> <p class="sm:h-16">Funds will be used to support development.</p>
<Link external href="https://geyser.fund/project/flotilla" class="btn btn-primary"> <Link external href="https://geyser.fund/project/flotilla" class="btn btn-primary">
@@ -33,7 +33,7 @@
</Link> </Link>
</div> </div>
{/if} {/if}
<div class="card2 bg-alt flex flex-col gap-2 text-center shadow-2xl"> <div class="card2 bg-alt flex flex-col gap-2 text-center shadow-lg">
<h3 class="text-2xl sm:h-12">Get in touch</h3> <h3 class="text-2xl sm:h-12">Get in touch</h3>
<p class="sm:h-16">Having problems? Let us know.</p> <p class="sm:h-16">Having problems? Let us know.</p>
<Link <Link
+1 -1
View File
@@ -48,7 +48,7 @@
</script> </script>
<form class="content column gap-4" {onsubmit}> <form class="content column gap-4" {onsubmit}>
<div class="card2 bg-alt col-4 shadow-xl"> <div class="card2 bg-alt col-4 shadow-md">
<strong class="text-lg">Content Settings</strong> <strong class="text-lg">Content Settings</strong>
<FieldInline> <FieldInline>
{#snippet label()} {#snippet label()}
+6 -6
View File
@@ -16,7 +16,7 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import FieldInline from "@lib/components/FieldInline.svelte" import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
import ProfileEdit from "@app/components/ProfileEdit.svelte" import ProfileEdit from "@app/components/ProfileEdit.svelte"
import ProfileDelete from "@app/components/ProfileDelete.svelte" import ProfileDelete from "@app/components/ProfileDelete.svelte"
@@ -44,11 +44,11 @@
</script> </script>
<div class="content column gap-4"> <div class="content column gap-4">
<div class="card2 bg-alt shadow-xl"> <div class="card2 bg-alt shadow-md">
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="flex max-w-full gap-3"> <div class="flex max-w-full gap-3">
<div class="py-1"> <div class="py-1">
<Avatar src={$profile?.picture} size={10} /> <ProfileCircle pubkey={$pubkey!} size={10} />
</div> </div>
<div class="flex min-w-0 flex-col"> <div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -70,7 +70,7 @@
{/key} {/key}
</div> </div>
{#if $session?.email} {#if $session?.email}
<div class="card2 bg-alt col-4 shadow-xl"> <div class="card2 bg-alt col-4 shadow-md">
<FieldInline> <FieldInline>
{#snippet label()} {#snippet label()}
<p>Email Address</p> <p>Email Address</p>
@@ -90,7 +90,7 @@
</FieldInline> </FieldInline>
</div> </div>
{/if} {/if}
<div class="card2 bg-alt col-4 shadow-xl"> <div class="card2 bg-alt col-4 shadow-md">
<FieldInline> <FieldInline>
{#snippet label()} {#snippet label()}
<p class="flex items-center gap-3"> <p class="flex items-center gap-3">
@@ -140,7 +140,7 @@
{/if} {/if}
<SignerStatus /> <SignerStatus />
</div> </div>
<div class="card2 bg-alt shadow-xl"> <div class="card2 bg-alt shadow-md">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<strong class="flex items-center gap-3"> <strong class="flex items-center gap-3">
<Icon icon={Settings} /> <Icon icon={Settings} />
+3 -3
View File
@@ -50,7 +50,7 @@
</script> </script>
<div class="content column gap-4"> <div class="content column gap-4">
<Collapse class="card2 bg-alt column gap-4 shadow-xl"> <Collapse class="card2 bg-alt column gap-4 shadow-md">
{#snippet title()} {#snippet title()}
<h2 class="flex items-center gap-3 text-xl"> <h2 class="flex items-center gap-3 text-xl">
<Icon icon={Globus} /> <Icon icon={Globus} />
@@ -83,7 +83,7 @@
</Button> </Button>
</div> </div>
</Collapse> </Collapse>
<Collapse class="card2 bg-alt column gap-4 shadow-xl"> <Collapse class="card2 bg-alt column gap-4 shadow-md">
{#snippet title()} {#snippet title()}
<h2 class="flex items-center gap-3 text-xl"> <h2 class="flex items-center gap-3 text-xl">
<Icon icon={Inbox} /> <Icon icon={Inbox} />
@@ -115,7 +115,7 @@
</Button> </Button>
</div> </div>
</Collapse> </Collapse>
<Collapse class="card2 bg-alt column gap-4 shadow-xl"> <Collapse class="card2 bg-alt column gap-4 shadow-md">
{#snippet title()} {#snippet title()}
<h2 class="flex items-center gap-3 text-xl"> <h2 class="flex items-center gap-3 text-xl">
<Icon icon={Mailbox} /> <Icon icon={Mailbox} />
+2 -2
View File
@@ -30,7 +30,7 @@
</script> </script>
<div class="content column gap-4"> <div class="content column gap-4">
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-6 shadow-md">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<strong class="flex items-center gap-3"> <strong class="flex items-center gap-3">
<Icon icon={Wallet2} /> <Icon icon={Wallet2} />
@@ -100,7 +100,7 @@
</div> </div>
</div> </div>
<div <div
class="card2 bg-alt flex flex-col shadow-xl" class="card2 bg-alt flex flex-col shadow-md"
class:gap-6={profileLightningAddress && walletLud16 && profile?.lud16 !== walletLud16}> class:gap-6={profileLightningAddress && walletLud16 && profile?.lud16 !== walletLud16}>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<strong>Lightning Address</strong> <strong>Lightning Address</strong>
+18 -11
View File
@@ -8,7 +8,7 @@
import PageContent from "@lib/components/PageContent.svelte" import PageContent from "@lib/components/PageContent.svelte"
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte" import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte" import SpaceAdd from "@app/components/SpaceAdd.svelte"
import {userSpaceUrls, PLATFORM_RELAYS} from "@app/core/state" import {userSpaceUrls, loadUserGroupSelections, PLATFORM_RELAYS} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
const addSpace = () => pushModal(SpaceAdd) const addSpace = () => pushModal(SpaceAdd)
@@ -37,17 +37,24 @@
{#each PLATFORM_RELAYS as url (url)} {#each PLATFORM_RELAYS as url (url)}
<MenuSpacesItem {url} /> <MenuSpacesItem {url} />
{:else} {:else}
{#each $userSpaceUrls as url (url)} {#await loadUserGroupSelections()}
<MenuSpacesItem {url} /> <div class="flex justify-center items-center py-20">
{:else} <span class="loading loading-spinner mr-3"></span>
<div class="flex flex-col gap-8 items-center py-20"> Loading your spaces...
<p>You haven't added any spaces yet!</p>
<Button class="btn btn-primary" onclick={addSpace}>
<Icon icon={AddCircle} />
Add a Space
</Button>
</div> </div>
{/each} {:then}
{#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}
{/await}
{/each} {/each}
</PageContent> </PageContent>
</Page> </Page>
+16 -51
View File
@@ -16,12 +16,10 @@
} from "@welshman/util" } from "@welshman/util"
import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app" import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} 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 InfoCircle from "@assets/icons/info-circle.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 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 Bookmark from "@assets/icons/bookmark.svg?dataurl" import Bookmark from "@assets/icons/bookmark.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"
@@ -31,8 +29,9 @@
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 RoomEdit from "@app/components/RoomEdit.svelte"
import RoomName from "@app/components/RoomName.svelte" import RoomName from "@app/components/RoomName.svelte"
import RoomImage from "@app/components/RoomImage.svelte"
import RoomDetail from "@app/components/RoomDetail.svelte"
import RoomItem from "@app/components/RoomItem.svelte" import RoomItem from "@app/components/RoomItem.svelte"
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte" import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
import RoomItemRemoveMember from "@src/app/components/RoomItemRemoveMember.svelte" import RoomItemRemoveMember from "@src/app/components/RoomItemRemoveMember.svelte"
@@ -48,7 +47,6 @@
MembershipStatus, MembershipStatus,
PROTECTED, PROTECTED,
MESSAGE_KINDS, MESSAGE_KINDS,
deriveUserIsRoomAdmin,
} from "@app/core/state" } from "@app/core/state"
import {setChecked, checked} from "@app/util/notifications" import {setChecked, checked} from "@app/util/notifications"
import { import {
@@ -70,10 +68,11 @@
const room = deriveRoom(url, h) 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, h)
const isFavorite = $derived($userRooms.includes(h)) const isFavorite = $derived($userRooms.includes(h))
const membershipStatus = deriveUserRoomMembershipStatus(url, h) const membershipStatus = deriveUserRoomMembershipStatus(url, h)
const showRoomDetail = () => pushModal(RoomDetail, {url, h})
const addFavorite = () => addRoomMembership(url, h) const addFavorite = () => addRoomMembership(url, h)
const removeFavorite = () => removeRoomMembership(url, h) const removeFavorite = () => removeRoomMembership(url, h)
@@ -86,10 +85,10 @@
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
return pushToast({theme: "error", message}) return pushToast({theme: "error", message})
} else {
// Restart the feed now that we're a member
start()
} }
// Restart the feed now that we're a member
start()
} finally { } finally {
joining = false joining = false
} }
@@ -303,8 +302,6 @@
} }
} }
const startEdit = () => pushModal(RoomEdit, {url, h})
onMount(() => { onMount(() => {
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
if (dynamicPadding && chatCompose) { if (dynamicPadding && chatCompose) {
@@ -334,57 +331,25 @@
<PageBar> <PageBar>
{#snippet icon()} {#snippet icon()}
<div class="center"> <RoomImage {url} {h} />
<Icon icon={Hashtag} />
</div>
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<strong class="ellipsize"> <RoomName {url} {h} />
<RoomName {url} {h} />
</strong>
{/snippet} {/snippet}
{#snippet action()} {#snippet action()}
<div class="row-2"> <div class="row-2">
{#if $userIsAdmin}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Edit room information"
onclick={startEdit}>
<Icon size={4} icon={Pen} />
</Button>
{:else if $membershipStatus === MembershipStatus.Initial}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Request to be added to the member list"
disabled={joining}
onclick={join}>
{#if joining}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon size={4} icon={Login2} />
{/if}
</Button>
{:else if $membershipStatus === MembershipStatus.Pending}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Membership is pending">
<Icon size={4} icon={ClockCircle} />
</Button>
{:else}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Request to be removed from member list"
disabled={leaving}
onclick={leave}>
<Icon size={4} icon={Logout2} />
</Button>
{/if}
<Button <Button
class="btn btn-neutral btn-sm tooltip tooltip-left" class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip={isFavorite ? "Remove Favorite" : "Add Favorite"} data-tip={isFavorite ? "Remove Favorite" : "Add Favorite"}
onclick={isFavorite ? removeFavorite : addFavorite}> onclick={isFavorite ? removeFavorite : addFavorite}>
<Icon size={4} icon={Bookmark} class={cx({"text-primary": isFavorite})} /> <Icon size={4} icon={Bookmark} class={cx({"text-primary": isFavorite})} />
</Button> </Button>
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Room information"
onclick={showRoomDetail}>
<Icon size={4} icon={InfoCircle} />
</Button>
<MenuSpaceButton {url} /> <MenuSpaceButton {url} />
</div> </div>
{/snippet} {/snippet}
+2 -2
View File
@@ -36,7 +36,7 @@
<li>Requires some technical skills</li> <li>Requires some technical skills</li>
</ul> </ul>
</div> </div>
<Link class="btn btn-primary" href="https://github.com/coracle-social/zooid"> <Link external class="btn btn-primary" href="https://github.com/coracle-social/zooid">
Get Started Get Started
<Icon icon={ArrowRight} /> <Icon icon={ArrowRight} />
</Link> </Link>
@@ -101,7 +101,7 @@
<li>Support available</li> <li>Support available</li>
</ul> </ul>
</div> </div>
<Link class="btn btn-neutral" href="https://relay.tools/signup"> <Link external class="btn btn-neutral" href="https://relay.tools/signup">
Get Started Get Started
<Icon icon={ArrowRight} /> <Icon icon={ArrowRight} />
</Link> </Link>