forked from coracle/flotilla
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca4e5ae5ee | |||
| b673658c0c | |||
| 5c5c130700 | |||
| 2d89ca6c0e | |||
| 806a7c2609 | |||
| 501ce8067d | |||
| 6429f82829 | |||
| fe626218ea | |||
| b62b1bc063 | |||
| d980f36246 | |||
| b469addd29 | |||
| 6923c2a8b7 | |||
| 1d3f32fb99 | |||
| 42a550788a | |||
| b1c68972c9 | |||
| 3978e32d5f | |||
| ba2b5d182e | |||
| bef04fa899 | |||
| 4f8609421c | |||
| 07660c9d44 | |||
| a324dad2ba | |||
| dbaa0f5d49 | |||
| 478721d349 | |||
| a669a23dbc | |||
| cfeb6478cc | |||
| 64539c49c1 | |||
| 0399ae37ec | |||
| 173a411a36 | |||
| 62013a2ea2 | |||
| c82cf4a4c2 |
+1
-1
@@ -1,7 +1,7 @@
|
||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||
VITE_BURROW_URL=
|
||||
VITE_PLATFORM_URL=https://flotilla.social
|
||||
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||
VITE_PLATFORM_NAME=Flotilla
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
# 1.5.0
|
||||
|
||||
* Restyle mobile dialogs
|
||||
* Add room membership lists
|
||||
* Add space membership lists
|
||||
* Add edit room form
|
||||
* Support closed/private/restricted/hidden rooms
|
||||
* Add hosting services page
|
||||
* Improve performance and UI
|
||||
* Fix push notifications
|
||||
* Improve error detection and handling
|
||||
* Support invite links on discover page
|
||||
* Add link to landlubber if user is admin
|
||||
* Clear reply/share/edit on escape
|
||||
|
||||
# 1.4.1
|
||||
|
||||
* Improve data synchronization
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ Here are a few important domain objects:
|
||||
|
||||
- Spaces are relays used as community groups. Their `url`s are core to a lot of data and components, and are frequently passed around from place to place.
|
||||
- Chats are direct message conversations. There is currently some ambiguity in routing, since relays that don't support NIP 29 also have a "chat" tab, which uses vanilla NIP-C7.
|
||||
- NIP 29 groups are variously called "rooms" and "channels". Conventionally, a "room" is a group id, while a "channel" as an object representing the group's metadata.
|
||||
- NIP 29 groups are called "rooms". Conventionally, "h" is a group id, while a "room" as an object representing the group's metadata.
|
||||
- "Alerts" are records of requests the user has made to be notified, following [this NIP](https://github.com/nostr-protocol/nips/pull/1796)
|
||||
|
||||
`app/core/requests` contains utilities related to loading data from the nostr network. This might include feed manager utilities, loaders, or listeners.
|
||||
|
||||
@@ -8,8 +8,8 @@ If you would like to be interoperable with Flotilla, please check out this guide
|
||||
|
||||
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
|
||||
|
||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust.
|
||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted. This is only used for build-time population of meta tags.
|
||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
|
||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted
|
||||
- `VITE_PLATFORM_NAME` - The name of the app
|
||||
- `VITE_PLATFORM_LOGO` - A logo url for the app
|
||||
- `VITE_PLATFORM_RELAYS` - A list of comma-separated relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "social.flotilla"
|
||||
minSdk rootProject.ext.minSdkVersion
|
||||
targetSdk rootProject.ext.targetSdkVersion
|
||||
versionCode 30
|
||||
versionName "1.4.1"
|
||||
versionCode 31
|
||||
versionName "1.5.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -6,10 +6,6 @@ if [ -f .env.template ]; then
|
||||
source .env.template
|
||||
fi
|
||||
|
||||
if [ -f .env ]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Avoid overwriting env vars provided directly
|
||||
# https://stackoverflow.com/a/69127685/1467342
|
||||
eval "$temp_env"
|
||||
|
||||
@@ -358,14 +358,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
MARKETING_VERSION = 1.5.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -384,14 +384,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
MARKETING_VERSION = 1.5.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -60,16 +60,16 @@
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.8",
|
||||
"@welshman/app": "^0.6.3",
|
||||
"@welshman/content": "^0.6.3",
|
||||
"@welshman/editor": "^0.6.3",
|
||||
"@welshman/feeds": "^0.6.3",
|
||||
"@welshman/lib": "^0.6.3",
|
||||
"@welshman/net": "^0.6.3",
|
||||
"@welshman/router": "^0.6.3",
|
||||
"@welshman/signer": "^0.6.3",
|
||||
"@welshman/store": "^0.6.3",
|
||||
"@welshman/util": "^0.6.3",
|
||||
"@welshman/app": "^0.6.4",
|
||||
"@welshman/content": "^0.6.4",
|
||||
"@welshman/editor": "^0.6.4",
|
||||
"@welshman/feeds": "^0.6.4",
|
||||
"@welshman/lib": "^0.6.4",
|
||||
"@welshman/net": "^0.6.4",
|
||||
"@welshman/router": "^0.6.4",
|
||||
"@welshman/signer": "^0.6.4",
|
||||
"@welshman/store": "^0.6.4",
|
||||
"@welshman/util": "^0.6.4",
|
||||
"compressorjs": "^1.2.1",
|
||||
"daisyui": "^4.12.24",
|
||||
"date-picker-svelte": "^2.16.0",
|
||||
|
||||
Generated
+76
-76
@@ -72,35 +72,35 @@ importers:
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
|
||||
'@welshman/app':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/content':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4(typescript@5.9.3)
|
||||
'@welshman/editor':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)
|
||||
'@welshman/feeds':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/lib':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4
|
||||
'@welshman/net':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/store':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util':
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3(typescript@5.9.3)
|
||||
specifier: ^0.6.4
|
||||
version: 0.6.4(typescript@5.9.3)
|
||||
compressorjs:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
@@ -1692,38 +1692,38 @@ packages:
|
||||
'@vite-pwa/assets-generator':
|
||||
optional: true
|
||||
|
||||
'@welshman/app@0.6.3':
|
||||
resolution: {integrity: sha512-iCZj0b3D5Q7rSEUiON+nJEKIdYG3j6MdK3oSyFiiQQxa+TPArjEbWF5kkogp1J1GMGo7gzQ6owByPgDIdlNiZg==}
|
||||
'@welshman/app@0.6.4':
|
||||
resolution: {integrity: sha512-hmUdVK+mntAYLjoH8Pv4GKNgzP+4vzyFJc0ntvoiKmOZN++/0fZWYPDrwvQ2laW1NEoig8DyUuJn8KH1XTiwkQ==}
|
||||
|
||||
'@welshman/content@0.6.3':
|
||||
resolution: {integrity: sha512-VvlW2kJ/lB6sy/Upa0nbKbO6rMwTO9Bi2iGXNiR9XFA8rlMUJc7Wl1Dcd3QZD3QHBTeTICOuFB4xopwQK3W0JQ==}
|
||||
'@welshman/content@0.6.4':
|
||||
resolution: {integrity: sha512-sesFIa1DIzxpXx6drF6CFdjiWFZ9Z9DrKP34JBaPfb3Vi6ewh43VBZx2lM52S1Nx3/BMEv2rh50PNcvNs2WYFQ==}
|
||||
|
||||
'@welshman/editor@0.6.3':
|
||||
resolution: {integrity: sha512-oLOhUUYO+4vhOehEpVhmsGMhmiBsJa/jgSsVRuCpZRKi+jwrJsaC9J7AyUzbQmcA8e07oNsqfakDYFkbFAfUVQ==}
|
||||
'@welshman/editor@0.6.4':
|
||||
resolution: {integrity: sha512-G03AlY/n6ANG9zosVOd833DPXJheLbdWcE6shhYyscdOitW7KFwKYAaUOug5NOhBsonGtD8O7lKHryPo6qBxyw==}
|
||||
|
||||
'@welshman/feeds@0.6.3':
|
||||
resolution: {integrity: sha512-Q5Dupo+C/CIm7BMZ++7BhQdjgyu3tU4aK2w172cx0gTFfSTqKxFrbWX4ewUWqq8ex1/1Xd+heVheE1q54FCVNQ==}
|
||||
'@welshman/feeds@0.6.4':
|
||||
resolution: {integrity: sha512-Mjnd4/bBQvc7szcXjCy6fG3tDDJv6J+PdCOWLgE28BIGmG3yzMPvKDEYm1zcyDuZfa/zJp2o4NYg9FWVpYdW9A==}
|
||||
|
||||
'@welshman/lib@0.6.3':
|
||||
resolution: {integrity: sha512-O632yJQ/1IUmb9FuC+O0TsOUl+UubYriU95LpKA9ZFAaogXLrzHZH0HZ/4xmdPj82kqBeS98EOb3GkSAOYZaLA==}
|
||||
'@welshman/lib@0.6.4':
|
||||
resolution: {integrity: sha512-+K5OC2jctVLTOXjENNoB7rG70pkYI7dZysjE2vD2EJLrN1XaV7MtGHpONsKJQi4f1yKOhXeUMPrPu/lKADcSLg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
'@welshman/net@0.6.3':
|
||||
resolution: {integrity: sha512-RlCYFFau6p2RZq3K4XMYhD7xvNFYjA6zpzHuM8iV4Xh8nL5b8tn6e6f6a4I2E5uZMfYdWstvcC6OhU/Z4NQZoA==}
|
||||
'@welshman/net@0.6.4':
|
||||
resolution: {integrity: sha512-+4x9HnArPR6tLGt/bptOsxx/s61UpWEVO5+epLfS6t8sW7II7K8j/+d/SVwF3nP+TI+6M5zjaEOG9cOprS0EmQ==}
|
||||
|
||||
'@welshman/router@0.6.3':
|
||||
resolution: {integrity: sha512-lZaw166aJKhftakMGofzmFmtAqfQBGSur58/ANgLCwVy4b5riqempcPoH6EOgwqANye5YUlsUisxbx0r/2wXPA==}
|
||||
'@welshman/router@0.6.4':
|
||||
resolution: {integrity: sha512-o5YHd8eOCziuhq7p2dZMlQ9ocosjJdv7RtAL4ofRhfT31eGmPtexkwTEem90+Lr5gdgsRsszPwUFS7AbNBARRg==}
|
||||
|
||||
'@welshman/signer@0.6.3':
|
||||
resolution: {integrity: sha512-O0cd5nV77Lt4vWRvb50l6U1wkhQF6LW3x+N4tspqWqo9BeIHTDlUtZMdsnhwfTjeVlLocSVkUValxhoxxt20SA==}
|
||||
'@welshman/signer@0.6.4':
|
||||
resolution: {integrity: sha512-Oby/aMJvsEEdSNday4dC+/M0ZOABkYQddvfkDYvR2snBS326+bCuABf3Fd9eoHj7MB+wE2qQ96tiX4zX/RWS5Q==}
|
||||
peerDependencies:
|
||||
nostr-signer-capacitor-plugin: ~0.0.4
|
||||
|
||||
'@welshman/store@0.6.3':
|
||||
resolution: {integrity: sha512-3TMon00CF1l/LcWq+B3pq/FNs2Ie4Y9EdjiAcg4dvDeZPI9h1HoQPB/s2RcaG4FYPhkynvM5zL/eH+5GIe9kJw==}
|
||||
'@welshman/store@0.6.4':
|
||||
resolution: {integrity: sha512-uDlpLt/f/nf9voO6K/h97o+VzhfReogUB5ZMljsW49rUsw/P9ohidPYcLkvIjyWaWcRvuRxJK4cipVXXpqmukw==}
|
||||
|
||||
'@welshman/util@0.6.3':
|
||||
resolution: {integrity: sha512-ki2j3ADmxqI0rbdnkq0wS7PFXTY0dZ24yOgceyTizwF/dtcRKmH7YyvM+2YwgbgaUO6BtqKjktc3jrh6XaxOQg==}
|
||||
'@welshman/util@0.6.4':
|
||||
resolution: {integrity: sha512-T0KapD05GncO5WwaK7X7JxaYXi0AVsOyhUi7anhVCsXg/ubFd8Giqa5vV/S9HcS95C9nCcqcoKlbh9kHPvVK5g==}
|
||||
|
||||
'@xml-tools/parser@1.0.11':
|
||||
resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==}
|
||||
@@ -6651,16 +6651,16 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@vite-pwa/assets-generator': 0.2.6
|
||||
|
||||
'@welshman/app@0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/app@0.6.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@types/throttle-debounce': 5.0.2
|
||||
'@welshman/feeds': 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer': 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/store': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/feeds': 0.6.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/lib': 0.6.4
|
||||
'@welshman/net': 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router': 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer': 0.6.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/store': 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.4(typescript@5.9.3)
|
||||
fuse.js: 7.1.0
|
||||
svelte: 4.2.20
|
||||
throttle-debounce: 5.0.2
|
||||
@@ -6669,14 +6669,14 @@ snapshots:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/content@0.6.3(typescript@5.9.3)':
|
||||
'@welshman/content@0.6.4(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@braintree/sanitize-url': 7.1.1
|
||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@welshman/editor@0.6.3(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)':
|
||||
'@welshman/editor@0.6.4(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||
'@tiptap/extension-code': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
@@ -6691,8 +6691,8 @@ snapshots:
|
||||
'@tiptap/extension-text': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||
'@tiptap/pm': 2.26.3
|
||||
'@tiptap/suggestion': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.4
|
||||
'@welshman/util': 0.6.4(typescript@5.9.3)
|
||||
nostr-editor: 1.0.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))
|
||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||
tippy.js: 6.3.7
|
||||
@@ -6707,71 +6707,71 @@ snapshots:
|
||||
- tiptap-markdown
|
||||
- typescript
|
||||
|
||||
'@welshman/feeds@0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/feeds@0.6.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer': 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.4
|
||||
'@welshman/net': 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/router': 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/signer': 0.6.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.4(typescript@5.9.3)
|
||||
trava: 1.2.1
|
||||
transitivePeerDependencies:
|
||||
- nostr-signer-capacitor-plugin
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/lib@0.6.3':
|
||||
'@welshman/lib@0.6.4':
|
||||
dependencies:
|
||||
'@scure/base': 1.2.6
|
||||
'@types/events': 3.0.3
|
||||
events: 3.3.0
|
||||
|
||||
'@welshman/net@0.6.3(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/net@0.6.4(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.4
|
||||
'@welshman/util': 0.6.4(typescript@5.9.3)
|
||||
events: 3.3.0
|
||||
isomorphic-ws: 5.0.0(ws@8.18.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/router@0.6.3(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/router@0.6.4(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.4
|
||||
'@welshman/net': 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.4(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/signer@0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/signer@0.6.4(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@noble/curves': 1.9.7
|
||||
'@noble/hashes': 1.8.0
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.4
|
||||
'@welshman/net': 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.4(typescript@5.9.3)
|
||||
nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.4.3)
|
||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/store@0.6.3(typescript@5.9.3)(ws@8.18.3)':
|
||||
'@welshman/store@0.6.4(typescript@5.9.3)(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.3(typescript@5.9.3)
|
||||
'@welshman/lib': 0.6.4
|
||||
'@welshman/net': 0.6.4(typescript@5.9.3)(ws@8.18.3)
|
||||
'@welshman/util': 0.6.4(typescript@5.9.3)
|
||||
svelte: 4.2.20
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
'@welshman/util@0.6.3(typescript@5.9.3)':
|
||||
'@welshman/util@0.6.4(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@types/ws': 8.18.1
|
||||
'@welshman/lib': 0.6.3
|
||||
'@welshman/lib': 0.6.4
|
||||
js-base64: 3.7.8
|
||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||
nostr-wasm: 0.1.0
|
||||
|
||||
+1
-1
@@ -395,7 +395,7 @@ progress[value]::-webkit-progress-value {
|
||||
/* chat view */
|
||||
|
||||
.chat__compose {
|
||||
@apply cb cw fixed;
|
||||
@apply cb cw fixed z-compose;
|
||||
}
|
||||
|
||||
.chat__scroll-down {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
const path = makeCalendarPath(url, event.id)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if room && showRoom}
|
||||
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<ChannelName {room} {url} />
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, room}: Props = $props()
|
||||
const {url, h}: Props = $props()
|
||||
</script>
|
||||
|
||||
<CalendarEventForm {url} {room}>
|
||||
<CalendarEventForm {url} {h}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
header: Snippet
|
||||
initialValues?: {
|
||||
d: string
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const {url, room, header, initialValues}: Props = $props()
|
||||
const {url, h, header, initialValues}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -85,8 +85,8 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (room) {
|
||||
tags.push(["h", room])
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import {makeCalendarPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -15,16 +15,18 @@
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}>
|
||||
<Link
|
||||
class="col-3 card2 bg-alt w-full cursor-pointer shadow-xl"
|
||||
href={makeCalendarPath(url, event.id)}>
|
||||
<CalendarEventHeader {event} />
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if room}
|
||||
in <ChannelLink {url} {room} />
|
||||
{#if h}
|
||||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<CalendarEventActions showActivity {url} {event} />
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {channelsById, makeChannelId} from "@app/core/state"
|
||||
|
||||
const {url, room} = $props()
|
||||
</script>
|
||||
|
||||
{$channelsById.get(makeChannelId(url, room))?.name || room}
|
||||
@@ -13,16 +13,16 @@
|
||||
type Props = {
|
||||
url: string
|
||||
onClick: () => void
|
||||
room?: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, room, onClick}: Props = $props()
|
||||
const {url, h, onClick}: Props = $props()
|
||||
|
||||
const createGoal = () => pushModal(GoalCreate, {url, room})
|
||||
const createGoal = () => pushModal(GoalCreate, {url, h})
|
||||
|
||||
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, room})
|
||||
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, h})
|
||||
|
||||
const createThread = () => pushModal(ThreadCreate, {url, room})
|
||||
const createThread = () => pushModal(ThreadCreate, {url, h})
|
||||
|
||||
let ul: Element
|
||||
|
||||
|
||||
@@ -8,29 +8,29 @@
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import {goToEvent} from "@app/util/routes"
|
||||
import {displayChannel} from "@app/core/state"
|
||||
import {displayRoom} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
events: TrustedEvent[]
|
||||
latest: TrustedEvent
|
||||
earliest: TrustedEvent
|
||||
participants: string[]
|
||||
}
|
||||
|
||||
const {url, room, events, latest, earliest, participants}: Props = $props()
|
||||
const {url, h, events, latest, earliest, participants}: Props = $props()
|
||||
</script>
|
||||
|
||||
<Button class="card2 bg-alt" onclick={() => goToEvent(earliest)}>
|
||||
<Button class="card2 bg-alt shadow-lg" onclick={() => goToEvent(earliest)}>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<ProfileCircle pubkey={earliest.pubkey} size={10} />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||
{#if room}
|
||||
{#if h}
|
||||
<span class="truncate font-medium text-blue-400">
|
||||
#{displayChannel(url, room)}
|
||||
#{displayRoom(url, h)}
|
||||
</span>
|
||||
<span class="opacity-50">•</span>
|
||||
{/if}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import {channelsByUrl} from "@app/core/state"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {roomsByUrl} from "@app/core/state"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
|
||||
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
|
||||
@@ -22,8 +22,8 @@
|
||||
goto(makeRoomPath(url, selection), {replaceState: true})
|
||||
}
|
||||
|
||||
const toggleRoom = (room: string) => {
|
||||
selection = room === selection ? "" : room
|
||||
const toggleRoom = (h: string) => {
|
||||
selection = h === selection ? "" : h
|
||||
}
|
||||
|
||||
let selection = $state("")
|
||||
@@ -39,14 +39,14 @@
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
{#each $channelsByUrl.get(url) || [] as channel (channel.room)}
|
||||
{#each $roomsByUrl.get(url) || [] as room (room.h)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
class:btn-neutral={selection !== channel.room}
|
||||
class:btn-primary={selection === channel.room}
|
||||
onclick={() => toggleRoom(channel.room)}>
|
||||
#<ChannelName {...channel} />
|
||||
class:btn-neutral={selection !== room.h}
|
||||
class:btn-primary={selection === room.h}
|
||||
onclick={() => toggleRoom(room.h)}>
|
||||
#<RoomName {...room} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {makeGoalPath, makeSpacePath} from "@app/util/routes"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const path = makeGoalPath(url, event.id)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const deleteReaction = async (event: TrustedEvent) =>
|
||||
@@ -31,9 +31,9 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if room && showRoom}
|
||||
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<ChannelName {room} {url} />
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, room}: Props = $props()
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (room) {
|
||||
tags.push(["h", room])
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import GoalActions from "@app/components/GoalActions.svelte"
|
||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import {makeGoalPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -17,10 +17,10 @@
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const summary = getTagValue("summary", event.tags)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeGoalPath(url, event.id)}>
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl" href={makeGoalPath(url, event.id)}>
|
||||
<p class="text-2xl">{event.content}</p>
|
||||
<Content
|
||||
event={{content: summary, tags: event.tags}}
|
||||
@@ -32,8 +32,8 @@
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if room}
|
||||
in <ChannelLink {url} {room} />
|
||||
{#if h}
|
||||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<GoalActions showActivity {url} {event} />
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Login from "@assets/icons/login-3.svg?dataurl"
|
||||
import History from "@assets/icons/history.svg?dataurl"
|
||||
import Tuning2 from "@assets/icons/tuning-2.svg?dataurl"
|
||||
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
||||
@@ -47,6 +48,7 @@
|
||||
hasNip29,
|
||||
alerts,
|
||||
deriveUserCanCreateRoom,
|
||||
deriveUserIsSpaceAdmin,
|
||||
} from "@app/core/state"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
@@ -63,6 +65,7 @@
|
||||
const otherRooms = deriveOtherRooms(url)
|
||||
const members = deriveSpaceMembers(url)
|
||||
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
|
||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||
|
||||
const spaceKinds = derived(
|
||||
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
||||
@@ -149,7 +152,14 @@
|
||||
View Members ({$members.length})
|
||||
</Button>
|
||||
</li>
|
||||
{#if $relay?.pubkey}
|
||||
{#if $userIsAdmin}
|
||||
<li>
|
||||
<Link external href="https://landlubber.coracle.social">
|
||||
<Icon icon={Tuning2} />
|
||||
Manage Space
|
||||
</Link>
|
||||
</li>
|
||||
{:else if $relay?.pubkey}
|
||||
<li>
|
||||
<Link href={makeChatPath([$relay.pubkey])}>
|
||||
<Icon icon={Letter} />
|
||||
@@ -216,8 +226,8 @@
|
||||
<div class="h-2"></div>
|
||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||
{/if}
|
||||
{#each $userRooms as room, i (room)}
|
||||
<MenuSpaceRoomItem {replaceState} notify {url} {room} />
|
||||
{#each $userRooms as h, i (h)}
|
||||
<MenuSpaceRoomItem {replaceState} notify {url} {h} />
|
||||
{/each}
|
||||
{#if $otherRooms.length > 0}
|
||||
<div class="h-2"></div>
|
||||
@@ -229,8 +239,8 @@
|
||||
{/if}
|
||||
</SecondaryNavHeader>
|
||||
{/if}
|
||||
{#each $otherRooms as room, i (room)}
|
||||
<MenuSpaceRoomItem {replaceState} {url} {room} />
|
||||
{#each $otherRooms as h, i (h)}
|
||||
<MenuSpaceRoomItem {replaceState} {url} {h} />
|
||||
{/each}
|
||||
{#if $canCreateRoom}
|
||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
||||
|
||||
@@ -1,43 +1,24 @@
|
||||
<script lang="ts">
|
||||
import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
import {deriveChannel} from "@app/core/state"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
room: any
|
||||
h: any
|
||||
notify?: boolean
|
||||
replaceState?: boolean
|
||||
}
|
||||
|
||||
const {url, room, notify = false, replaceState = false}: Props = $props()
|
||||
const {url, h, notify = false, replaceState = false}: Props = $props()
|
||||
|
||||
const path = makeRoomPath(url, room)
|
||||
const channel = deriveChannel(url, room)
|
||||
const path = makeRoomPath(url, h)
|
||||
</script>
|
||||
|
||||
<SecondaryNavItem
|
||||
href={path}
|
||||
{replaceState}
|
||||
notification={notify ? $notifications.has(path) : false}>
|
||||
{#if $channel?.picture}
|
||||
{@const src = $channel.picture}
|
||||
{#if src.match("\.(png|svg)$") || src.match("image/(png|svg)")}
|
||||
<Icon icon={src} />
|
||||
{:else}
|
||||
<img alt="Room icon" {src} class="h-6 w-6 rounded-lg" />
|
||||
{/if}
|
||||
{:else if $channel?.closed || $channel?.private}
|
||||
<Icon icon={Lock} />
|
||||
{:else}
|
||||
<Icon icon={Hashtag} />
|
||||
{/if}
|
||||
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||
<ChannelName {url} {room} />
|
||||
</div>
|
||||
<RoomNameWithImage {url} {h} />
|
||||
</SecondaryNavItem>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
|
||||
import {userSpaceUrls, PLATFORM_RELAYS} from "@app/core/state"
|
||||
</script>
|
||||
|
||||
<div class="column menu gap-2">
|
||||
{#each PLATFORM_RELAYS as url (url)}
|
||||
<MenuSpacesItem {url} />
|
||||
{:else}
|
||||
{#if $userSpaceUrls.length > 0}
|
||||
{#each $userSpaceUrls as url (url)}
|
||||
<MenuSpacesItem {url} />
|
||||
{/each}
|
||||
<Divider />
|
||||
{/if}
|
||||
<Link href="/discover">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Compass} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Explore Spaces</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Join create, or browse spaces</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -4,9 +4,15 @@
|
||||
import Dialog from "@lib/components/Dialog.svelte"
|
||||
import {modal, clearModals} from "@app/util/modal"
|
||||
|
||||
const closeModals = () => {
|
||||
if ($modal && !$modal.options.noEscape) {
|
||||
clearModals()
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyDown = (e: any) => {
|
||||
if (e.code === "Escape" && e.target === document.body) {
|
||||
clearModals()
|
||||
closeModals()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +33,7 @@
|
||||
instance = mount(wrapper as any, {
|
||||
target: element,
|
||||
props: {
|
||||
onClose: clearModals,
|
||||
onClose: closeModals,
|
||||
children: createRawSnippet(() => ({
|
||||
render: () => "<div></div>",
|
||||
setup: (target: Element) => {
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||
import MenuSpaces from "@app/components/MenuSpaces.svelte"
|
||||
import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte"
|
||||
import MenuSettings from "@app/components/MenuSettings.svelte"
|
||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||
@@ -31,9 +29,6 @@
|
||||
|
||||
const {children}: Props = $props()
|
||||
|
||||
const showSpacesMenu = () =>
|
||||
$userSpaceUrls.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd)
|
||||
|
||||
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
|
||||
|
||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||
@@ -118,7 +113,7 @@
|
||||
<div
|
||||
class="border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
||||
<div class="content-padding-x content-sizing flex justify-between px-2">
|
||||
<div class="flex gap-2 sm:gap-8">
|
||||
<div class="flex gap-2 sm:gap-6">
|
||||
<PrimaryNavItem title="Home" href="/home">
|
||||
<Avatar icon={HomeSmile} class="!h-10 !w-10" />
|
||||
</PrimaryNavItem>
|
||||
@@ -129,10 +124,7 @@
|
||||
<Avatar icon={Letter} class="!h-10 !w-10" />
|
||||
</PrimaryNavItem>
|
||||
{#if PLATFORM_RELAYS.length !== 1}
|
||||
<PrimaryNavItem
|
||||
title="Spaces"
|
||||
onclick={showSpacesMenu}
|
||||
notification={anySpaceNotifications}>
|
||||
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
|
||||
<Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" />
|
||||
</PrimaryNavItem>
|
||||
{/if}
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||
import {encodeRelay} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {lastPageBySpaceUrl} from "@app/util/history"
|
||||
import {makeSpacePath, goToSpace} from "@app/util/routes"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const path = makeSpacePath(url)
|
||||
|
||||
const onClick = () => goto(lastPageBySpaceUrl.get(encodeRelay(url)) || path)
|
||||
const onClick = () => goToSpace(url)
|
||||
</script>
|
||||
|
||||
<PrimaryNavItem
|
||||
onclick={onClick}
|
||||
title={displayRelayUrl(url)}
|
||||
class="tooltip-right"
|
||||
notification={$notifications.has(path)}>
|
||||
notification={$notifications.has(makeSpacePath(url))}>
|
||||
<SpaceAvatar {url} />
|
||||
</PrimaryNavItem>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {onMount} from "svelte"
|
||||
import type {Snippet} from "svelte"
|
||||
import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
|
||||
@@ -134,10 +135,15 @@
|
||||
<button
|
||||
type="button"
|
||||
data-tip={tooltip}
|
||||
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full text-xs font-normal {reactionClass}"
|
||||
class:tooltip={!noTooltip && !isMobile}
|
||||
class:btn-neutral={!isOwn}
|
||||
class:btn-primary={isOwn}>
|
||||
class={cx(
|
||||
reactionClass,
|
||||
"flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full text-xs font-normal",
|
||||
{
|
||||
tooltip: !noTooltip && !isMobile,
|
||||
"border-neutral-content/20": !isOwn,
|
||||
"btn-primary": isOwn,
|
||||
},
|
||||
)}>
|
||||
<Reaction event={zaps[0].request} />
|
||||
<span>{amount}</span>
|
||||
</button>
|
||||
@@ -151,10 +157,15 @@
|
||||
<button
|
||||
type="button"
|
||||
data-tip={tooltip}
|
||||
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full font-normal {reactionClass}"
|
||||
class:tooltip={!noTooltip && !isMobile}
|
||||
class:btn-neutral={!isOwn}
|
||||
class:btn-primary={isOwn}
|
||||
class={cx(
|
||||
reactionClass,
|
||||
"flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal",
|
||||
{
|
||||
tooltip: !noTooltip && !isMobile,
|
||||
"border-neutral-content/20": !isOwn,
|
||||
"btn-primary": isOwn,
|
||||
},
|
||||
)}
|
||||
onclick={stopPropagation(preventDefault(onClick))}>
|
||||
<Reaction event={events[0]} />
|
||||
{#if events.length > 1}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
room?: string
|
||||
h?: string
|
||||
content?: string
|
||||
onEscape?: () => void
|
||||
onEditPrevious?: () => void
|
||||
onSubmit: (event: EventContent) => void
|
||||
}
|
||||
|
||||
const {url, room, content, onEditPrevious, onSubmit}: Props = $props()
|
||||
const {url, h, content, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
||||
|
||||
const autofocus = !isMobile
|
||||
|
||||
@@ -34,6 +35,10 @@
|
||||
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
|
||||
|
||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
onEscape?.()
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
|
||||
onEditPrevious?.()
|
||||
}
|
||||
@@ -74,7 +79,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||
<form class="relative flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||
<div class="join">
|
||||
<Button
|
||||
data-tip="Add an image"
|
||||
@@ -90,7 +95,7 @@
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={ComposeMenu}
|
||||
props={{url, room, onClick: hidePopover}}
|
||||
props={{url, h, onClick: hidePopover}}
|
||||
params={{trigger: "manual", interactive: true}}>
|
||||
<Button
|
||||
data-tip="More options"
|
||||
+2
-2
@@ -12,10 +12,10 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
||||
class="relative flex h-8 items-center justify-between border-l-2 border-solid border-primary bg-base-300 px-2 pr-7 text-xs"
|
||||
transition:slide>
|
||||
<p class="text-primary">Editing message</p>
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||
<Button onclick={clear} class="flex items-center">
|
||||
<Icon icon={CloseCircle} />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1,181 +1,47 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {uniqBy, nth} from "@welshman/lib"
|
||||
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
|
||||
import {deriveRelay, waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||
import {preventDefault, compressFile} from "@lib/html"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
|
||||
import {hasNip29, loadChannel} from "@app/core/state"
|
||||
import RoomForm from "@app/components/RoomForm.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {uploadFile} from "@app/core/commands"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const room = makeRoomMeta()
|
||||
const relay = deriveRelay(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const tryCreate = async () => {
|
||||
room.tags = uniqBy(nth(0), [...room.tags, ["name", name]])
|
||||
|
||||
if (imageFile) {
|
||||
const {error, result} = await uploadFile(imageFile)
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
room.tags.push(["picture", result.url, ...result.tags])
|
||||
} else if (selectedIcon) {
|
||||
room.tags.push(["picture", selectedIcon])
|
||||
}
|
||||
|
||||
const createMessage = await waitForThunkError(createRoom(url, room))
|
||||
|
||||
if (createMessage && !createMessage.match(/^duplicate:|already a member/)) {
|
||||
return pushToast({theme: "error", message: createMessage})
|
||||
}
|
||||
|
||||
const editMessage = await waitForThunkError(editRoom(url, room))
|
||||
|
||||
if (editMessage) {
|
||||
return pushToast({theme: "error", message: editMessage})
|
||||
}
|
||||
|
||||
const joinMessage = await waitForThunkError(joinRoom(url, room))
|
||||
|
||||
if (joinMessage && !joinMessage.includes("already")) {
|
||||
return pushToast({theme: "error", message: joinMessage})
|
||||
}
|
||||
|
||||
await loadChannel(url, room.id)
|
||||
|
||||
goto(makeSpacePath(url, room.id))
|
||||
}
|
||||
|
||||
const create = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await tryCreate()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let name = $state("")
|
||||
let loading = $state(false)
|
||||
let imageFile = $state<File | undefined>()
|
||||
let imagePreview = $state<string | undefined>()
|
||||
let selectedIcon = $state<string | undefined>()
|
||||
|
||||
const handleImageUpload = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
|
||||
if (file && file.type.startsWith("image/")) {
|
||||
selectedIcon = undefined
|
||||
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = e => {
|
||||
imagePreview = e.target?.result as string
|
||||
}
|
||||
|
||||
reader.readAsDataURL(imageFile)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIconSelect = (iconUrl: string) => {
|
||||
imageFile = undefined
|
||||
imagePreview = undefined
|
||||
selectedIcon = iconUrl
|
||||
}
|
||||
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, room.h))
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(create)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Create a Room</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>
|
||||
On <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{#if hasNip29($relay)}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Room Name</p>
|
||||
<RoomForm {url} {onsubmit}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Create a Room</div>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Hashtag} />
|
||||
<input bind:value={name} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="font-bold">Room Icon</p>
|
||||
<div class="flex items-center gap-4">
|
||||
{#if imagePreview}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-75">Selected:</span>
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Room icon preview"
|
||||
class="h-8 w-8 rounded-lg object-cover" />
|
||||
</div>
|
||||
{:else if selectedIcon}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-75">Selected:</span>
|
||||
<Icon icon={selectedIcon} class="h-8 w-8" />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-sm opacity-75">No icon selected</span>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
|
||||
<Icon icon={StickerSmileSquare} size={4} />
|
||||
Select
|
||||
</IconPickerButton>
|
||||
<label class="btn btn-neutral btn-sm cursor-pointer">
|
||||
<Icon icon={UploadMinimalistic} size={4} />
|
||||
Upload
|
||||
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
|
||||
</label>
|
||||
{#snippet info()}
|
||||
<div>
|
||||
On <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="bg-alt card2 row-2">
|
||||
<Icon icon={Danger} />
|
||||
This relay does not support creating rooms.
|
||||
</p>
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={!name || loading || !hasNip29($relay)}>
|
||||
<Spinner {loading}>Create Room</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{/snippet}
|
||||
{#snippet footer({loading})}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Create Room</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
{/snippet}
|
||||
</RoomForm>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {deleteRoom, waitForThunkError, repository} from "@welshman/app"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import RoomForm from "@app/components/RoomForm.svelte"
|
||||
import {deriveRoom} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
h: string
|
||||
}
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, h))
|
||||
|
||||
const startDelete = () =>
|
||||
pushModal(Confirm, {
|
||||
title: "Are you sure you want to delete this room?",
|
||||
message:
|
||||
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
|
||||
confirm: async () => {
|
||||
const thunk = deleteRoom(url, $room)
|
||||
const message = await waitForThunkError(thunk)
|
||||
|
||||
if (message) {
|
||||
repository.removeEvent(thunk.event.id)
|
||||
pushToast({theme: "error", message})
|
||||
} else {
|
||||
goto(makeSpacePath(url))
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<RoomForm {url} {onsubmit} initialValues={$room}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Edit a Room</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>
|
||||
On <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{/snippet}
|
||||
{#snippet footer({loading})}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<Button class="btn btn-outline btn-error" onclick={startDelete}>
|
||||
<Icon icon={TrashBin2} />
|
||||
<span class="hidden md:inline">Delete Room</span>
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Save Changes</Spinner>
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
{/snippet}
|
||||
</RoomForm>
|
||||
@@ -0,0 +1,206 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
import {makeRoomMeta} from "@welshman/util"
|
||||
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||
import {preventDefault, compressFile} from "@lib/html"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {uploadFile} from "@app/core/commands"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
header: Snippet
|
||||
footer: Snippet<[{loading: boolean}]>
|
||||
onsubmit: (room: RoomMeta) => void
|
||||
initialValues?: RoomMeta
|
||||
}
|
||||
|
||||
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
|
||||
|
||||
const values = $state(initialValues)
|
||||
|
||||
const submit = async () => {
|
||||
const room = $state.snapshot(values)
|
||||
|
||||
if (imageFile) {
|
||||
const {error, result} = await uploadFile(imageFile)
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
room.picture = result.url
|
||||
room.pictureMeta = result.tags
|
||||
} else if (selectedIcon) {
|
||||
room.picture = selectedIcon
|
||||
}
|
||||
|
||||
const createMessage = await waitForThunkError(createRoom(url, room))
|
||||
|
||||
if (createMessage && !createMessage.includes("already")) {
|
||||
return pushToast({theme: "error", message: createMessage})
|
||||
}
|
||||
|
||||
const editMessage = await waitForThunkError(editRoom(url, room))
|
||||
|
||||
if (editMessage) {
|
||||
return pushToast({theme: "error", message: editMessage})
|
||||
}
|
||||
|
||||
const joinMessage = await waitForThunkError(joinRoom(url, room))
|
||||
|
||||
if (joinMessage && !joinMessage.includes("already")) {
|
||||
return pushToast({theme: "error", message: joinMessage})
|
||||
}
|
||||
|
||||
onsubmit(room)
|
||||
}
|
||||
|
||||
const trySubmit = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await submit()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
let imageFile = $state<File | undefined>()
|
||||
let imagePreview = $state(initialValues.picture)
|
||||
let selectedIcon = $state<string | undefined>()
|
||||
|
||||
const handleImageUpload = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
|
||||
if (file && file.type.startsWith("image/")) {
|
||||
selectedIcon = undefined
|
||||
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = e => {
|
||||
imagePreview = e.target?.result as string
|
||||
}
|
||||
|
||||
reader.readAsDataURL(imageFile)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIconSelect = (iconUrl: string) => {
|
||||
imageFile = undefined
|
||||
imagePreview = undefined
|
||||
selectedIcon = iconUrl
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(trySubmit)}>
|
||||
{@render header()}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Icon</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
{#if imagePreview}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-75">Selected:</span>
|
||||
<ImageIcon src={imagePreview} alt="Room icon preview" />
|
||||
</div>
|
||||
{:else if selectedIcon}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-75">Selected:</span>
|
||||
<Icon icon={selectedIcon} class="h-8 w-8" />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-sm opacity-75">No icon selected</span>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
|
||||
<Icon icon={StickerSmileSquare} size={4} />
|
||||
Select
|
||||
</IconPickerButton>
|
||||
<label class="btn btn-neutral btn-sm cursor-pointer">
|
||||
<Icon icon={UploadMinimalistic} size={4} />
|
||||
Upload
|
||||
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Name</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
{#if imagePreview}
|
||||
<ImageIcon src={imagePreview} alt="Room icon preview" />
|
||||
{:else if selectedIcon}
|
||||
<Icon icon={selectedIcon} class="h-8 w-8" />
|
||||
{:else}
|
||||
<Icon icon={Hashtag} />
|
||||
{/if}
|
||||
<input bind:value={values.name} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Description</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<input bind:value={values.about} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<strong>Restricted</strong>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
|
||||
<span class="text-sm opacity-75">Only allow members to send messages</span>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<strong>Private</strong>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
|
||||
<span class="text-sm opacity-75">Only allow members to read messages</span>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<strong>Hidden</strong>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="checkbox" bind:checked={values.isHidden} />
|
||||
<span class="text-sm opacity-75">Hide this group from non-members</span>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<strong>Closed</strong>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
|
||||
<span class="text-sm opacity-75">Ignore requests to join</span>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{@render footer({loading})}
|
||||
</form>
|
||||
@@ -23,14 +23,14 @@
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ChannelItemZapButton from "@app/components/ChannelItemZapButton.svelte"
|
||||
import ChannelItemEmojiButton from "@app/components/ChannelItemEmojiButton.svelte"
|
||||
import ChannelItemMenuButton from "@app/components/ChannelItemMenuButton.svelte"
|
||||
import ChannelItemMenuMobile from "@app/components/ChannelItemMenuMobile.svelte"
|
||||
import ChannelItemContent from "@app/components/ChannelItemContent.svelte"
|
||||
import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte"
|
||||
import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte"
|
||||
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
|
||||
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
|
||||
import RoomItemContent from "@app/components/RoomItemContent.svelte"
|
||||
import {colors, ENABLE_ZAPS, deriveEventsForUrl} from "@app/core/state"
|
||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {getChannelItemPath} from "@app/util/routes"
|
||||
import {getRoomItemPath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
interface Props {
|
||||
@@ -53,7 +53,7 @@
|
||||
onEdit,
|
||||
}: Props = $props()
|
||||
|
||||
const path = getChannelItemPath(url, event)
|
||||
const path = getRoomItemPath(url, event)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const today = formatTimestampAsDate(now())
|
||||
const profile = deriveProfile(event.pubkey, [url])
|
||||
@@ -65,7 +65,7 @@
|
||||
const reply = () => replyTo!(event)
|
||||
const edit = canEdit(event) ? () => onEdit(event) : undefined
|
||||
|
||||
const onTap = () => pushModal(ChannelItemMenuMobile, {url, event, reply, edit})
|
||||
const onTap = () => pushModal(RoomItemMenuMobile, {url, event, reply, edit})
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
||||
<ChannelItemContent {url} {event} />
|
||||
<RoomItemContent {url} {event} />
|
||||
{#if thunk}
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||
{/if}
|
||||
@@ -142,9 +142,9 @@
|
||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}>
|
||||
{#if ENABLE_ZAPS}
|
||||
<ChannelItemZapButton {url} {event} />
|
||||
<RoomItemZapButton {url} {event} />
|
||||
{/if}
|
||||
<ChannelItemEmojiButton {url} {event} />
|
||||
<RoomItemEmojiButton {url} {event} />
|
||||
{#if replyTo}
|
||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||
<Icon icon={Reply} size={4} />
|
||||
@@ -155,7 +155,7 @@
|
||||
<Icon icon={Pen} size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
<ChannelItemMenuButton {url} {event} />
|
||||
<RoomItemMenuButton {url} {event} />
|
||||
</button>
|
||||
{/if}
|
||||
</TapTarget>
|
||||
+2
-2
@@ -5,11 +5,11 @@
|
||||
import {isMobile} from "@lib/html"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import {getChannelItemPath} from "@app/util/routes"
|
||||
import {getRoomItemPath} from "@app/util/routes"
|
||||
|
||||
const props: ComponentProps<typeof NoteContent> = $props()
|
||||
|
||||
const path = getChannelItemPath(props.url!, props.event)
|
||||
const path = getRoomItemPath(props.url!, props.event)
|
||||
</script>
|
||||
|
||||
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import ChannelItemMenu from "@app/components/ChannelItemMenu.svelte"
|
||||
import RoomItemMenu from "@app/components/RoomItemMenu.svelte"
|
||||
|
||||
const {url, event} = $props()
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</Button>
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={ChannelItemMenu}
|
||||
component={RoomItemMenu}
|
||||
props={{url, event, onClick}}
|
||||
params={{trigger: "manual", interactive: true}} />
|
||||
</div>
|
||||
+2
-2
@@ -17,7 +17,7 @@
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import {ENABLE_ZAPS} from "@app/core/state"
|
||||
import {publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {getChannelItemPath} from "@app/util/routes"
|
||||
import {getRoomItemPath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
const {url, event, reply}: Props = $props()
|
||||
|
||||
const path = getChannelItemPath(url, event)
|
||||
const path = getRoomItemPath(url, event)
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
room: string
|
||||
h: string
|
||||
url: string
|
||||
class?: string
|
||||
unstyled?: boolean
|
||||
}
|
||||
|
||||
const {room, url, unstyled, ...props}: Props = $props()
|
||||
const {h, url, unstyled, ...props}: Props = $props()
|
||||
|
||||
const path = makeSpacePath(url, room)
|
||||
const path = makeSpacePath(url, h)
|
||||
</script>
|
||||
|
||||
<Link href={path} class={cx(props.class, {"link-content bg-alt": !unstyled})}>
|
||||
#<ChannelName {room} {url} />
|
||||
#<RoomName {h} {url} />
|
||||
</Link>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {roomsById, makeRoomId} from "@app/core/state"
|
||||
|
||||
const {url, h} = $props()
|
||||
</script>
|
||||
|
||||
{$roomsById.get(makeRoomId(url, h))?.name || h}
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {deriveRoom} from "@app/core/state"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
h: any
|
||||
}
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
</script>
|
||||
|
||||
{#if $room.picture}
|
||||
{@const src = $room.picture}
|
||||
<ImageIcon {src} alt="Room icon" />
|
||||
{:else}
|
||||
<Icon icon={Hashtag} />
|
||||
{/if}
|
||||
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||
<RoomName {url} {h} />
|
||||
</div>
|
||||
@@ -14,7 +14,7 @@
|
||||
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {checkRelayAccess} from "@app/core/commands"
|
||||
import {attemptRelayAccess} from "@app/core/commands"
|
||||
import {deriveSocket} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
@@ -31,7 +31,7 @@
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const message = await checkRelayAccess(url, claim)
|
||||
const message = await attemptRelayAccess(url, claim)
|
||||
|
||||
if (message) {
|
||||
return pushToast({theme: "error", message, timeout: 30_000})
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<script lang="ts">
|
||||
import Login from "@assets/icons/login-3.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import SpaceCreateExternal from "@app/components/SpaceCreateExternal.svelte"
|
||||
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const startCreate = () => pushModal(SpaceCreateExternal)
|
||||
type Props = {
|
||||
hideDiscover?: boolean
|
||||
}
|
||||
|
||||
const {hideDiscover}: Props = $props()
|
||||
|
||||
const startJoin = () => pushModal(SpaceInviteAccept)
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<div class="column gap-2">
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Add a Space</div>
|
||||
@@ -23,8 +28,23 @@
|
||||
<div>Spaces are places where communities come together to work, play, and hang out.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{#if !hideDiscover}
|
||||
<Link href="/discover">
|
||||
<CardButton class="btn-primary">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Compass} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Explore Spaces</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Join create, or browse spaces</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
{/if}
|
||||
<Button onclick={startJoin}>
|
||||
<CardButton class="btn-primary">
|
||||
<CardButton class={hideDiscover ? "btn-primary" : "btn-neutral"}>
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Login} size={7} /></div>
|
||||
{/snippet}
|
||||
@@ -36,7 +56,7 @@
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Button>
|
||||
<Button onclick={startCreate}>
|
||||
<Link href="/spaces/create">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={AddCircle} size={7} /></div>
|
||||
@@ -48,5 +68,5 @@
|
||||
<div>Just a few questions and you'll be on your way.</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {parse, renderAsHtml} from "@welshman/content"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -14,7 +15,7 @@
|
||||
|
||||
const {url, error} = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
const back = () => goto("/home")
|
||||
|
||||
const requestAccess = () => pushModal(SpaceAccessRequest, {url})
|
||||
</script>
|
||||
@@ -37,7 +38,7 @@
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
Go Home
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary">
|
||||
Request Access
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import SpaceVisitConfirm, {confirmSpaceVisit} from "@app/components/SpaceVisitConfirm.svelte"
|
||||
@@ -49,7 +48,10 @@
|
||||
</ModalHeader>
|
||||
<div class="m-auto flex flex-col gap-4">
|
||||
{#if loading}
|
||||
<Spinner loading>Hold tight, we're checking your connection.</Spinner>
|
||||
<p class="flex items-center gap-3">
|
||||
<span class="loading loading-spinner"></span>
|
||||
Hold tight, we're checking your connection.
|
||||
</p>
|
||||
{:else if error}
|
||||
<p>Oops! We ran into some problems:</p>
|
||||
<p class="card2 bg-alt">{error}</p>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import FireMinimalistic from "@assets/icons/fire-minimalistic.svg?dataurl"
|
||||
import Server from "@assets/icons/server.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import InfoRelay from "@app/components/InfoRelay.svelte"
|
||||
import InputProfilePicture from "@app/components/InputProfilePicture.svelte"
|
||||
import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const next = () => pushModal(SpaceCreateFinish)
|
||||
|
||||
let file: File | undefined = $state()
|
||||
let name = $state("")
|
||||
let relay = $state("")
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Customize your Space</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Give people a few details to go on. You can always change this later.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<div class="flex justify-center py-2">
|
||||
<InputProfilePicture bind:file />
|
||||
</div>
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Space Name</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={FireMinimalistic} />
|
||||
<input bind:value={name} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Relay</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Server} />
|
||||
<input bind:value={relay} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
This can be any nostr relay where you'd like to host your space.
|
||||
<Button class="link" onclick={() => pushModal(InfoRelay)}>What is a relay?</Button>
|
||||
</p>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary">
|
||||
Next
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -1,56 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
|
||||
import {PLATFORM_NAME} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const next = () => {
|
||||
window.open("https://relay.tools/signup")
|
||||
|
||||
setTimeout(() => pushModal(SpaceInviteAccept), 300)
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Create a Space</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Host your own space, for your community.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<p>
|
||||
<Link class="link" external href="https://relay.tools">relay.tools</Link> is a third-party service
|
||||
that allows anyone to run their own relay for use with {PLATFORM_NAME}, or any other
|
||||
nostr-compatible app.
|
||||
</p>
|
||||
<p>
|
||||
Once you've created a relay of your own, come back here to link {PLATFORM_NAME} with your new relay.
|
||||
</p>
|
||||
<p>
|
||||
Alternatively, you can
|
||||
<Link external class="link" href="https://github.com/coracle-social/frith"
|
||||
>run your own community relay</Link
|
||||
>.
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary">
|
||||
Let's go
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -1 +0,0 @@
|
||||
hi
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {sleep, nthEq} from "@welshman/lib"
|
||||
import {sleep} from "@welshman/lib"
|
||||
import {request} from "@welshman/net"
|
||||
import {displayRelayUrl, RELAY_INVITE} from "@welshman/util"
|
||||
import {displayRelayUrl, getTagValue, RELAY_INVITE} from "@welshman/util"
|
||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -13,10 +13,12 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import QRCode from "@app/components/QRCode.svelte"
|
||||
import {clip} from "@app/util/toast"
|
||||
import {PLATFORM_URL} from "@app/core/state"
|
||||
import {PLATFORM_URL, deriveRelayAuthError} from "@app/core/state"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const authError = deriveRelayAuthError(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const copyInvite = () => clip(invite)
|
||||
@@ -38,12 +40,13 @@
|
||||
request({
|
||||
relays: [url],
|
||||
autoClose: true,
|
||||
signal: AbortSignal.timeout(3000),
|
||||
filters: [{kinds: [RELAY_INVITE]}],
|
||||
}),
|
||||
sleep(2000),
|
||||
])
|
||||
|
||||
claim = event?.tags.find(nthEq(0, "claim"))?.[1] || ""
|
||||
claim = getTagValue("claim", event?.tags || []) || ""
|
||||
loading = false
|
||||
})
|
||||
</script>
|
||||
@@ -65,6 +68,8 @@
|
||||
<p class="center">
|
||||
<Spinner {loading}>Requesting an invite link...</Spinner>
|
||||
</p>
|
||||
{:else if $authError}
|
||||
<p class="center">Oops! It looks like you're not a member of this relay.</p>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<QRCode code={invite} />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {tryCatch, fromPairs} from "@welshman/lib"
|
||||
import {isRelayUrl, normalizeRelayUrl} from "@welshman/util"
|
||||
import {Pool, AuthStatus} from "@welshman/net"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {slideAndFade} from "@lib/transition"
|
||||
@@ -19,6 +17,7 @@
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {attemptRelayAccess} from "@app/core/commands"
|
||||
import {parseInviteLink} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
invite: string
|
||||
@@ -57,24 +56,7 @@
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
const inviteData = $derived.by(
|
||||
() =>
|
||||
tryCatch(() => {
|
||||
const {r: relay = "", c: claim = ""} = fromPairs(Array.from(new URL(invite).searchParams))
|
||||
const url = normalizeRelayUrl(relay)
|
||||
|
||||
if (isRelayUrl(url)) {
|
||||
return {url, claim}
|
||||
}
|
||||
}) ||
|
||||
tryCatch(() => {
|
||||
const url = normalizeRelayUrl(invite)
|
||||
|
||||
if (isRelayUrl(url)) {
|
||||
return {url, claim: ""}
|
||||
}
|
||||
}),
|
||||
)
|
||||
const inviteData = $derived(parseInviteLink(invite))
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(join)}>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
try {
|
||||
await removeSpaceMembership(url)
|
||||
await removeTrustedRelay(url)
|
||||
goto("/")
|
||||
goto("/home")
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const h = getTagValue("h", event.tags)
|
||||
const path = makeThreadPath(url, event.id)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const deleteReaction = async (event: TrustedEvent) =>
|
||||
@@ -31,9 +31,9 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if room && showRoom}
|
||||
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<ChannelName {room} {url} />
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, room}: Props = $props()
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (room) {
|
||||
tags.push(["h", room])
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import ThreadActions from "@app/components/ThreadActions.svelte"
|
||||
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import {makeThreadPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -17,10 +17,12 @@
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const title = getTagValue("title", event.tags)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
|
||||
<Link
|
||||
class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
|
||||
href={makeThreadPath(url, event.id)}>
|
||||
{#if title}
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<p class="text-xl">{title}</p>
|
||||
@@ -38,8 +40,8 @@
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by
|
||||
<ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if room}
|
||||
in <ChannelLink {url} {room} />
|
||||
{#if h}
|
||||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<ThreadActions showActivity {url} {event} />
|
||||
|
||||
+23
-72
@@ -74,6 +74,7 @@ import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
||||
import {Router} from "@welshman/router"
|
||||
import {
|
||||
pubkey,
|
||||
sign,
|
||||
signer,
|
||||
session,
|
||||
repository,
|
||||
@@ -84,7 +85,6 @@ import {
|
||||
userRelaySelections,
|
||||
userInboxRelaySelections,
|
||||
nip44EncryptToSelf,
|
||||
loadRelay,
|
||||
dropSession,
|
||||
tagEventForComment,
|
||||
tagEventForQuote,
|
||||
@@ -104,8 +104,10 @@ import {
|
||||
DEFAULT_BLOSSOM_SERVERS,
|
||||
userSpaceUrls,
|
||||
userSettingsValues,
|
||||
getSetting,
|
||||
userInboxRelays,
|
||||
userGroupSelections,
|
||||
shouldIgnoreError,
|
||||
} from "@app/core/state"
|
||||
import {loadAlertStatuses} from "@app/core/requests"
|
||||
import {platform, platformName, getPushInfo} from "@app/util/push"
|
||||
@@ -191,11 +193,11 @@ export const removeSpaceMembership = async (url: string) => {
|
||||
return publishThunk({event, relays})
|
||||
}
|
||||
|
||||
export const addRoomMembership = async (url: string, room: string) => {
|
||||
export const addRoomMembership = async (url: string, h: string) => {
|
||||
const list = get(userGroupSelections) || makeList({kind: ROOMS})
|
||||
const newTags = [
|
||||
["r", url],
|
||||
["group", room, url],
|
||||
["group", h, url],
|
||||
]
|
||||
const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
@@ -203,9 +205,9 @@ export const addRoomMembership = async (url: string, room: string) => {
|
||||
return publishThunk({event, relays})
|
||||
}
|
||||
|
||||
export const removeRoomMembership = async (url: string, room: string) => {
|
||||
export const removeRoomMembership = async (url: string, h: string) => {
|
||||
const list = get(userGroupSelections) || makeList({kind: ROOMS})
|
||||
const pred = (t: string[]) => equals(["group", room, url], t.slice(0, 3))
|
||||
const pred = (t: string[]) => equals(["group", h, url], t.slice(0, 3))
|
||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
|
||||
@@ -250,57 +252,15 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
||||
|
||||
// Relay access
|
||||
|
||||
export const attemptAuth = async (url: string) => {
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||
}
|
||||
|
||||
export const canEnforceNip70 = async (url: string) => {
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||
await socket.auth.attemptAuth(sign)
|
||||
|
||||
return socket.auth.status !== AuthStatus.None
|
||||
}
|
||||
|
||||
export const checkRelayAccess = async (url: string, claim = "") => {
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
await attemptAuth(url)
|
||||
|
||||
const thunk = publishJoinRequest({url, claim})
|
||||
const error = await waitForThunkError(thunk)
|
||||
|
||||
if (error) {
|
||||
const message =
|
||||
socket.auth.details?.replace(/^\w+: /, "") ||
|
||||
error.replace(/^\w+: /, "") ||
|
||||
"join request rejected"
|
||||
|
||||
// If it's a strict NIP 29 relay don't worry about requesting access
|
||||
// TODO: remove this if relay29 ever gets less strict
|
||||
if (message === "missing group (`h`) tag") return
|
||||
|
||||
// Ignore messages about the relay ignoring ours
|
||||
if (error?.startsWith("mute: ")) return
|
||||
|
||||
// Ignore rejected empty claims
|
||||
if (!claim && error?.includes("invite code")) return
|
||||
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
export const checkRelayProfile = async (url: string) => {
|
||||
const relay = await loadRelay(url)
|
||||
|
||||
if (!relay) {
|
||||
return "Sorry, we weren't able to find that relay."
|
||||
}
|
||||
}
|
||||
|
||||
export const checkRelayConnection = async (url: string) => {
|
||||
export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
socket.attemptToOpen()
|
||||
@@ -313,38 +273,29 @@ export const checkRelayConnection = async (url: string) => {
|
||||
if (socket.status !== SocketStatus.Open) {
|
||||
return `Failed to connect`
|
||||
}
|
||||
}
|
||||
|
||||
export const checkRelayAuth = async (url: string) => {
|
||||
const socket = Pool.get().get(url)
|
||||
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
|
||||
|
||||
await attemptAuth(url)
|
||||
await socket.auth.attemptAuth(sign)
|
||||
|
||||
// Only raise an error if it's not a timeout.
|
||||
// If it is, odds are the problem is with our signer, not the relay
|
||||
if (!okStatuses.includes(socket.auth.status)) {
|
||||
if (![AuthStatus.None, AuthStatus.Ok].includes(socket.auth.status)) {
|
||||
if (socket.auth.details) {
|
||||
return `Failed to authenticate (${socket.auth.details})`
|
||||
} else {
|
||||
return `Failed to authenticate (${last(socket.auth.status.split(":"))})`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||
const checks = [
|
||||
() => checkRelayConnection(url),
|
||||
() => checkRelayAccess(url, claim),
|
||||
() => checkRelayAuth(url),
|
||||
]
|
||||
const thunk = publishJoinRequest({url, claim})
|
||||
const error = await waitForThunkError(thunk)
|
||||
|
||||
for (const check of checks) {
|
||||
const error = await check()
|
||||
if (shouldIgnoreError(error)) return
|
||||
|
||||
if (error) {
|
||||
return error
|
||||
}
|
||||
if (claim) {
|
||||
const ignoreClaimError =
|
||||
error.includes("invalid invite code size") || error.includes("failed to validate invite code")
|
||||
|
||||
if (!ignoreClaimError) return error?.replace(/^\w+: /, "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,7 +506,7 @@ export const createAlert = async (params: CreateAlertParams): Promise<CreateAler
|
||||
}
|
||||
|
||||
// If we don't do this we'll get an event rejection
|
||||
await attemptAuth(NOTIFIER_RELAY)
|
||||
await Pool.get().get(NOTIFIER_RELAY).auth.attemptAuth(sign)
|
||||
|
||||
const thunk = await publishAlert(params as AlertParams)
|
||||
const error = await waitForThunkError(thunk)
|
||||
@@ -599,7 +550,7 @@ export const createDmAlert = async () => {
|
||||
// Settings
|
||||
|
||||
export const makeSettings = async (params: Partial<SettingsValues>) => {
|
||||
const json = JSON.stringify({...userSettingsValues.get(), ...params})
|
||||
const json = JSON.stringify({...get(userSettingsValues), ...params})
|
||||
const content = await signer.get().nip44.encrypt(pubkey.get()!, json)
|
||||
const tags = [["d", SETTINGS]]
|
||||
|
||||
@@ -610,10 +561,10 @@ export const publishSettings = async (params: Partial<SettingsValues>) =>
|
||||
publishThunk({event: await makeSettings(params), relays: Router.get().FromUser().getUrls()})
|
||||
|
||||
export const addTrustedRelay = async (url: string) =>
|
||||
publishSettings({trusted_relays: append(url, userSettingsValues.get().trusted_relays)})
|
||||
publishSettings({trusted_relays: append(url, getSetting<string[]>("trusted_relays"))})
|
||||
|
||||
export const removeTrustedRelay = async (url: string) =>
|
||||
publishSettings({trusted_relays: remove(url, userSettingsValues.get().trusted_relays)})
|
||||
publishSettings({trusted_relays: remove(url, getSetting<string[]>("trusted_relays"))})
|
||||
|
||||
// Join request
|
||||
|
||||
|
||||
@@ -248,13 +248,15 @@ export const makeCalendarFeed = ({
|
||||
// Domain specific
|
||||
|
||||
export const loadAlerts = (pubkey: string) =>
|
||||
load({
|
||||
request({
|
||||
autoClose: true,
|
||||
relays: [NOTIFIER_RELAY],
|
||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID], authors: [pubkey]}],
|
||||
})
|
||||
|
||||
export const loadAlertStatuses = (pubkey: string) =>
|
||||
load({
|
||||
request({
|
||||
autoClose: true,
|
||||
relays: [NOTIFIER_RELAY],
|
||||
filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}],
|
||||
})
|
||||
|
||||
+172
-152
@@ -4,6 +4,8 @@ import {get, derived, writable} from "svelte/store"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {
|
||||
on,
|
||||
gt,
|
||||
max,
|
||||
spec,
|
||||
call,
|
||||
first,
|
||||
@@ -17,13 +19,13 @@ import {
|
||||
pushToMapKey,
|
||||
shuffle,
|
||||
parseJson,
|
||||
fromPairs,
|
||||
memoize,
|
||||
addToMapKey,
|
||||
identity,
|
||||
groupBy,
|
||||
always,
|
||||
tryCatch,
|
||||
fromPairs,
|
||||
} from "@welshman/lib"
|
||||
import type {Socket} from "@welshman/net"
|
||||
import {
|
||||
@@ -35,14 +37,7 @@ import {
|
||||
SocketEvent,
|
||||
netContext,
|
||||
} from "@welshman/net"
|
||||
import {
|
||||
collection,
|
||||
custom,
|
||||
throttled,
|
||||
deriveEvents,
|
||||
deriveEventsMapped,
|
||||
withGetter,
|
||||
} from "@welshman/store"
|
||||
import {collection, custom, throttled, deriveEvents, deriveEventsMapped} from "@welshman/store"
|
||||
import {isKindFeed, findFeed} from "@welshman/feeds"
|
||||
import {
|
||||
ALERT_ANDROID,
|
||||
@@ -88,7 +83,6 @@ import {
|
||||
getPubkeyTagValues,
|
||||
getRelaysFromList,
|
||||
getRelayTagValues,
|
||||
getTag,
|
||||
getTagValue,
|
||||
getTagValues,
|
||||
isRelayUrl,
|
||||
@@ -97,8 +91,18 @@ import {
|
||||
readList,
|
||||
RelayMode,
|
||||
verifyEvent,
|
||||
readRoomMeta,
|
||||
makeRoomMeta,
|
||||
ManagementMethod,
|
||||
} from "@welshman/util"
|
||||
import type {
|
||||
TrustedEvent,
|
||||
RelayProfile,
|
||||
PublishedList,
|
||||
PublishedRoomMeta,
|
||||
List,
|
||||
Filter,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, RelayProfile, PublishedList, List, Filter} from "@welshman/util"
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import {routerContext, Router} from "@welshman/router"
|
||||
import {
|
||||
@@ -112,6 +116,7 @@ import {
|
||||
userFollows,
|
||||
ensurePlaintext,
|
||||
thunks,
|
||||
sign,
|
||||
signer,
|
||||
makeOutboxLoader,
|
||||
appContext,
|
||||
@@ -122,6 +127,7 @@ import {
|
||||
deriveRelay,
|
||||
makeUserData,
|
||||
makeUserLoader,
|
||||
manageRelay,
|
||||
} from "@welshman/app"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
|
||||
@@ -232,31 +238,29 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const getUrlsForEvent = withGetter(
|
||||
derived([trackerStore, thunks], ([$tracker, $thunks]) => {
|
||||
const getThunksByEventId = memoize(() => {
|
||||
const thunksByEventId = new Map<string, Thunk[]>()
|
||||
export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thunks]) => {
|
||||
const getThunksByEventId = memoize(() => {
|
||||
const thunksByEventId = new Map<string, Thunk[]>()
|
||||
|
||||
for (const thunk of $thunks) {
|
||||
pushToMapKey(thunksByEventId, thunk.event.id, thunk)
|
||||
}
|
||||
|
||||
return thunksByEventId
|
||||
})
|
||||
|
||||
return (id: string) => {
|
||||
const urls = Array.from($tracker.getRelays(id))
|
||||
|
||||
for (const thunk of getThunksByEventId().get(id) || []) {
|
||||
for (const url of thunk.options.relays) {
|
||||
urls.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
return uniq(urls)
|
||||
for (const thunk of $thunks) {
|
||||
pushToMapKey(thunksByEventId, thunk.event.id, thunk)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return thunksByEventId
|
||||
})
|
||||
|
||||
return (id: string) => {
|
||||
const urls = Array.from($tracker.getRelays(id))
|
||||
|
||||
for (const thunk of getThunksByEventId().get(id) || []) {
|
||||
for (const url of thunk.options.relays) {
|
||||
urls.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
return uniq(urls)
|
||||
}
|
||||
})
|
||||
|
||||
export const getEventsForUrl = (url: string, filters: Filter[]) => {
|
||||
const ids = uniq([
|
||||
@@ -374,15 +378,13 @@ export const userSettings = makeUserData({
|
||||
|
||||
export const loadUserSettings = makeUserLoader(loadSettings)
|
||||
|
||||
export const userSettingsValues = withGetter(
|
||||
derived(userSettings, $s => $s?.values || defaultSettings),
|
||||
)
|
||||
export const userSettingsValues = derived(userSettings, $s => $s?.values || defaultSettings)
|
||||
|
||||
export const getSetting = <T>(key: keyof Settings["values"]) => userSettingsValues.get()[key] as T
|
||||
export const getSetting = <T>(key: keyof Settings["values"]) => get(userSettingsValues)[key] as T
|
||||
|
||||
// Relays sending events with empty signatures that the user has to choose to trust
|
||||
|
||||
export const relaysPendingTrust = withGetter(writable<string[]>([]))
|
||||
export const relaysPendingTrust = writable<string[]>([])
|
||||
|
||||
// Relays that mostly send restricted responses to requests and events
|
||||
|
||||
@@ -407,21 +409,19 @@ export type Alert = {
|
||||
tags: string[][]
|
||||
}
|
||||
|
||||
export const alerts = withGetter(
|
||||
deriveEventsMapped<Alert>(repository, {
|
||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
export const alerts = deriveEventsMapped<Alert>(repository, {
|
||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const getAlertFeed = (alert: Alert) =>
|
||||
tryCatch(() => JSON.parse(getTagValue("feed", alert.tags)!))
|
||||
@@ -441,21 +441,19 @@ export type AlertStatus = {
|
||||
tags: string[][]
|
||||
}
|
||||
|
||||
export const alertStatuses = withGetter(
|
||||
deriveEventsMapped<AlertStatus>(repository, {
|
||||
filters: [{kinds: [ALERT_STATUS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
|
||||
filters: [{kinds: [ALERT_STATUS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const deriveAlertStatus = (address: string) =>
|
||||
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
|
||||
@@ -530,62 +528,53 @@ export const chatSearch = derived(chats, $chats =>
|
||||
}),
|
||||
)
|
||||
|
||||
// Channels
|
||||
// Rooms
|
||||
|
||||
export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]})
|
||||
|
||||
export type Channel = {
|
||||
export type Room = PublishedRoomMeta & {
|
||||
id: string
|
||||
url: string
|
||||
room: string
|
||||
name: string
|
||||
event: TrustedEvent
|
||||
closed: boolean
|
||||
private: boolean
|
||||
picture?: string
|
||||
about?: string
|
||||
}
|
||||
|
||||
export const makeChannelId = (url: string, room: string) => `${url}'${room}`
|
||||
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
|
||||
|
||||
export const splitChannelId = (id: string) => id.split("'")
|
||||
export const splitRoomId = (id: string) => id.split("'")
|
||||
|
||||
export const hasNip29 = (relay?: RelayProfile) =>
|
||||
relay?.supported_nips?.map?.(String)?.includes?.("29")
|
||||
|
||||
export const channels = derived(
|
||||
[deriveEvents(repository, {filters: [{kinds: [ROOM_META, ROOM_DELETE]}]}), getUrlsForEvent],
|
||||
([$events, $getUrlsForEvent]) => {
|
||||
const result = new Map<string, Channel>()
|
||||
export const roomMetas = deriveEventsMapped<PublishedRoomMeta>(repository, {
|
||||
filters: [{kinds: [ROOM_META]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: readRoomMeta,
|
||||
})
|
||||
|
||||
for (const event of sortBy(e => e.created_at, $events)) {
|
||||
for (const url of $getUrlsForEvent(event.id)) {
|
||||
if (event.kind === ROOM_META) {
|
||||
const meta = fromPairs(event.tags)
|
||||
const room = meta.d
|
||||
export const roomDeletes = deriveEvents(repository, {
|
||||
filters: [{kinds: [ROOM_DELETE]}],
|
||||
})
|
||||
|
||||
if (room) {
|
||||
const id = makeChannelId(url, room)
|
||||
export const rooms = derived(
|
||||
[roomMetas, roomDeletes, getUrlsForEvent],
|
||||
([$roomMetas, $roomDeletes, $getUrlsForEvent]) => {
|
||||
const result = new Map<string, Room>()
|
||||
const deletedByH = new Map<string, number>()
|
||||
|
||||
result.set(id, {
|
||||
id,
|
||||
url,
|
||||
room,
|
||||
event,
|
||||
name: meta.name || room,
|
||||
closed: Boolean(getTag("closed", event.tags)),
|
||||
private: Boolean(getTag("private", event.tags)),
|
||||
picture: meta.picture,
|
||||
about: meta.about,
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const event of $roomDeletes) {
|
||||
for (const h of getTagValues("h", event.tags)) {
|
||||
deletedByH.set(h, max([deletedByH.get(h), event.created_at]))
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === ROOM_DELETE) {
|
||||
for (const room of getTagValues("h", event.tags)) {
|
||||
result.delete(makeChannelId(url, room))
|
||||
}
|
||||
}
|
||||
for (const meta of $roomMetas) {
|
||||
if (gt(deletedByH.get(meta.h), meta.event.created_at)) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const url of $getUrlsForEvent(meta.event.id)) {
|
||||
const id = makeRoomId(url, meta.h)
|
||||
|
||||
result.set(id, {...meta, url, id})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,35 +582,33 @@ export const channels = derived(
|
||||
},
|
||||
)
|
||||
|
||||
export const channelsByUrl = derived(channels, $channels => groupBy(c => c.url, $channels))
|
||||
export const roomsByUrl = derived(rooms, $rooms => groupBy(c => c.url, $rooms))
|
||||
|
||||
export const {
|
||||
indexStore: channelsById,
|
||||
deriveItem: _deriveChannel,
|
||||
loadItem: _loadChannel,
|
||||
indexStore: roomsById,
|
||||
deriveItem: _deriveRoom,
|
||||
loadItem: _loadRoom,
|
||||
} = collection({
|
||||
name: "channels",
|
||||
store: channels,
|
||||
getKey: channel => channel.id,
|
||||
name: "rooms",
|
||||
store: rooms,
|
||||
getKey: room => room.id,
|
||||
load: async (id: string) => {
|
||||
const [url, room] = splitChannelId(id)
|
||||
const [url, h] = splitRoomId(id)
|
||||
|
||||
await load({
|
||||
relays: [url],
|
||||
filters: [{kinds: [ROOM_META], "#d": [room]}],
|
||||
filters: [{kinds: [ROOM_META], "#d": [h]}],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const deriveChannel = (url: string, room: string) => _deriveChannel(makeChannelId(url, room))
|
||||
export const deriveRoom = (url: string, h: string) =>
|
||||
derived(_deriveRoom(makeRoomId(url, h)), $meta => $meta || makeRoomMeta({h}))
|
||||
|
||||
export const loadChannel = (url: string, room: string) => _loadChannel(makeChannelId(url, room))
|
||||
export const displayRoom = (url: string, h: string) =>
|
||||
roomsById.get().get(makeRoomId(url, h))?.name || h
|
||||
|
||||
export const displayChannel = (url: string, room: string) =>
|
||||
channelsById.get().get(makeChannelId(url, room))?.name || room
|
||||
|
||||
export const roomComparator = (url: string) => (room: string) =>
|
||||
displayChannel(url, room).toLowerCase()
|
||||
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
|
||||
|
||||
// User space/room selections
|
||||
|
||||
@@ -685,9 +672,9 @@ export const getSpaceRoomsFromGroupSelections = (
|
||||
) => {
|
||||
const rooms: string[] = []
|
||||
|
||||
for (const [_, room, relay] of getGroupTags(getListTags($groupSelections))) {
|
||||
for (const [_, h, relay] of getGroupTags(getListTags($groupSelections))) {
|
||||
if (url === relay) {
|
||||
rooms.push(room)
|
||||
rooms.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,12 +691,12 @@ export const loadUserGroupSelections = makeUserLoader(loadGroupSelections)
|
||||
export const userSpaceUrls = derived(userGroupSelections, getSpaceUrlsFromGroupSelections)
|
||||
|
||||
export const deriveUserRooms = (url: string) =>
|
||||
derived([userGroupSelections, channelsById], ([$userGroupSelections, $channelsById]) => {
|
||||
derived([userGroupSelections, roomsById], ([$userGroupSelections, $roomsById]) => {
|
||||
const rooms: string[] = []
|
||||
|
||||
for (const room of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
||||
if ($channelsById.has(makeChannelId(url, room))) {
|
||||
rooms.push(room)
|
||||
for (const h of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
||||
if ($roomsById.has(makeRoomId(url, h))) {
|
||||
rooms.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,12 +704,12 @@ export const deriveUserRooms = (url: string) =>
|
||||
})
|
||||
|
||||
export const deriveOtherRooms = (url: string) =>
|
||||
derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) => {
|
||||
derived([deriveUserRooms(url), roomsByUrl], ([$userRooms, $roomsByUrl]) => {
|
||||
const rooms: string[] = []
|
||||
|
||||
for (const {room} of $channelsByUrl.get(url) || []) {
|
||||
if (!$userRooms.includes(room)) {
|
||||
rooms.push(room)
|
||||
for (const {h} of $roomsByUrl.get(url) || []) {
|
||||
if (!$userRooms.includes(h)) {
|
||||
rooms.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,11 +752,11 @@ export const deriveSpaceMembers = (url: string) =>
|
||||
},
|
||||
)
|
||||
|
||||
export const deriveRoomMembers = (url: string, room: string) =>
|
||||
export const deriveRoomMembers = (url: string, h: string) =>
|
||||
derived(
|
||||
deriveEventsForUrl(url, [
|
||||
{kinds: [ROOM_MEMBERS], "#d": [room]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]},
|
||||
{kinds: [ROOM_MEMBERS], "#d": [h]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
|
||||
]),
|
||||
$events => {
|
||||
const membersEvent = $events.find(spec({kind: ROOM_MEMBERS}))
|
||||
@@ -800,8 +787,8 @@ export const deriveRoomMembers = (url: string, room: string) =>
|
||||
},
|
||||
)
|
||||
|
||||
export const deriveRoomAdmins = (url: string, room: string) =>
|
||||
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [room]}]), $events => {
|
||||
export const deriveRoomAdmins = (url: string, h: string) =>
|
||||
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [h]}]), $events => {
|
||||
const adminsEvent = first($events)
|
||||
|
||||
if (adminsEvent) {
|
||||
@@ -830,7 +817,7 @@ export const deriveUserSpaceMembershipStatus = (url: string) =>
|
||||
const isMember = $members.includes($pubkey)
|
||||
|
||||
for (const event of $events) {
|
||||
if (event.pubkey !== $pubkey) {
|
||||
if (!getPubkeyTagValues(event.tags).includes($pubkey!)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -847,18 +834,18 @@ export const deriveUserSpaceMembershipStatus = (url: string) =>
|
||||
},
|
||||
)
|
||||
|
||||
export const deriveUserRoomMembershipStatus = (url: string, room: string) =>
|
||||
export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
|
||||
derived(
|
||||
[
|
||||
pubkey,
|
||||
deriveRoomMembers(url, room),
|
||||
deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [room]}]),
|
||||
deriveRoomMembers(url, h),
|
||||
deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]),
|
||||
],
|
||||
([$pubkey, $members, $events]) => {
|
||||
const isMember = $members.includes($pubkey)
|
||||
|
||||
for (const event of $events) {
|
||||
if (event.pubkey !== $pubkey) {
|
||||
if (!getPubkeyTagValues(event.tags).includes($pubkey!)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -885,8 +872,18 @@ export const deriveUserCanCreateRoom = (url: string) =>
|
||||
},
|
||||
)
|
||||
|
||||
export const deriveUserIsRoomAdmin = (url: string, room: string) =>
|
||||
derived([pubkey, deriveRoomAdmins(url, room)], ([$pubkey, $admins]) => $admins.includes($pubkey!))
|
||||
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
|
||||
|
||||
@@ -977,13 +974,19 @@ export const deriveTimeout = (timeout: number) => {
|
||||
return derived(store, identity)
|
||||
}
|
||||
|
||||
export const shouldIgnoreError = (error: string) => {
|
||||
const isIgnored = error.startsWith("mute: ")
|
||||
const isAborted = error.includes("Signing was aborted")
|
||||
const isStrictNip29Relay = error.includes("missing group (`h`) tag")
|
||||
|
||||
return isIgnored || isAborted || isStrictNip29Relay
|
||||
}
|
||||
|
||||
export const deriveRelayAuthError = (url: string, claim = "") => {
|
||||
const $signer = signer.get()
|
||||
const socket = Pool.get().get(url)
|
||||
const stripPrefix = (m: string) => m.replace(/^\w+: /, "")
|
||||
|
||||
// Kick off the auth process
|
||||
socket.auth.attemptAuth($signer.sign)
|
||||
Pool.get().get(url).auth.attemptAuth(sign)
|
||||
|
||||
// Attempt to join the relay
|
||||
const thunk = publishThunk({
|
||||
@@ -992,8 +995,8 @@ export const deriveRelayAuthError = (url: string, claim = "") => {
|
||||
})
|
||||
|
||||
return derived(
|
||||
[relaysMostlyRestricted, deriveSocket(url)],
|
||||
([$relaysMostlyRestricted, $socket]) => {
|
||||
[thunk, relaysMostlyRestricted, deriveSocket(url)],
|
||||
([$thunk, $relaysMostlyRestricted, $socket]) => {
|
||||
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
|
||||
return stripPrefix($socket.auth.details)
|
||||
}
|
||||
@@ -1002,17 +1005,34 @@ export const deriveRelayAuthError = (url: string, claim = "") => {
|
||||
return stripPrefix($relaysMostlyRestricted[url])
|
||||
}
|
||||
|
||||
const error = getThunkError(thunk)
|
||||
const error = getThunkError($thunk)
|
||||
|
||||
if (error) {
|
||||
const isIgnored = error.startsWith("mute: ")
|
||||
const isEmptyInvite = !claim && error.includes("invite code")
|
||||
const isStrictNip29Relay = error.includes("missing group (`h`) tag")
|
||||
|
||||
if (!isStrictNip29Relay && !isIgnored && !isEmptyInvite && !isStrictNip29Relay) {
|
||||
if (!shouldIgnoreError(error) && !isEmptyInvite) {
|
||||
return stripPrefix(error) || "join request rejected"
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export type InviteData = {url: string; claim: string}
|
||||
|
||||
export const parseInviteLink = (invite: string): InviteData | undefined =>
|
||||
tryCatch(() => {
|
||||
const {r: relay = "", c: claim = ""} = fromPairs(Array.from(new URL(invite).searchParams))
|
||||
const url = normalizeRelayUrl(relay)
|
||||
|
||||
if (isRelayUrl(url)) {
|
||||
return {url, claim}
|
||||
}
|
||||
}) ||
|
||||
tryCatch(() => {
|
||||
const url = normalizeRelayUrl(invite)
|
||||
|
||||
if (isRelayUrl(url)) {
|
||||
return {url, claim: ""}
|
||||
}
|
||||
})
|
||||
|
||||
+18
-17
@@ -18,6 +18,7 @@ import {
|
||||
RELAY_ADD_MEMBER,
|
||||
RELAY_REMOVE_MEMBER,
|
||||
isSignedEvent,
|
||||
unionFilters,
|
||||
} from "@welshman/util"
|
||||
import type {Filter, TrustedEvent} from "@welshman/util"
|
||||
import {request, load, pull} from "@welshman/net"
|
||||
@@ -96,7 +97,7 @@ const pullAndListen = ({relays, filters, signal}: PullOpts) => {
|
||||
request({
|
||||
relays,
|
||||
signal,
|
||||
filters: filters.map(assoc("limit", 0)),
|
||||
filters: unionFilters(filters).map(assoc("limit", 0)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ const syncUserSpaceMembership = (url: string) => {
|
||||
return () => controller.abort()
|
||||
}
|
||||
|
||||
const syncUserRoomMembership = (url: string, room: string) => {
|
||||
const syncUserRoomMembership = (url: string, h: string) => {
|
||||
const $pubkey = pubkey.get()
|
||||
const controller = new AbortController()
|
||||
|
||||
@@ -162,7 +163,7 @@ const syncUserRoomMembership = (url: string, room: string) => {
|
||||
{
|
||||
kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
|
||||
"#p": [$pubkey],
|
||||
"#h": [room],
|
||||
"#h": [h],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -187,11 +188,11 @@ const syncUserData = () => {
|
||||
|
||||
keys.add(url)
|
||||
|
||||
for (const room of getSpaceRoomsFromGroupSelections(url, $l)) {
|
||||
const key = `${url}'${room}`
|
||||
for (const h of getSpaceRoomsFromGroupSelections(url, $l)) {
|
||||
const key = `${url}'${h}`
|
||||
|
||||
if (!unsubscribersByKey.has(key)) {
|
||||
unsubscribersByKey.set(key, syncUserRoomMembership(url, room))
|
||||
unsubscribersByKey.set(key, syncUserRoomMembership(url, h))
|
||||
}
|
||||
|
||||
keys.add(key)
|
||||
@@ -264,7 +265,8 @@ const syncSpace = (url: string) => {
|
||||
signal: controller.signal,
|
||||
filters: [
|
||||
{kinds: [RELAY_MEMBERS]},
|
||||
{kinds: [ROOM_META]},
|
||||
{kinds: [ROOM_META, ROOM_DELETE]},
|
||||
{kinds: [ROOM_ADMINS, ROOM_MEMBERS]},
|
||||
{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]},
|
||||
...MESSAGE_KINDS.map(kind => ({kinds: [kind]})),
|
||||
makeCommentFilter(CONTENT_KINDS),
|
||||
@@ -328,17 +330,16 @@ const syncSpaces = () => {
|
||||
|
||||
// Chat
|
||||
|
||||
const syncRoomChat = (url: string, room: string) => {
|
||||
const syncRoom = (url: string, h: string) => {
|
||||
const controller = new AbortController()
|
||||
|
||||
pullAndListen({
|
||||
relays: [url],
|
||||
signal: controller.signal,
|
||||
filters: [
|
||||
{kinds: [ROOM_ADMINS, ROOM_MEMBERS], "#d": [room]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]},
|
||||
{kinds: [ROOM_DELETE], "#h": [room]},
|
||||
{kinds: [MESSAGE], "#h": [room]},
|
||||
{kinds: [ROOM_ADMINS, ROOM_MEMBERS], "#d": [h]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
|
||||
{kinds: [MESSAGE], "#h": [h]},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -356,14 +357,14 @@ const syncRooms = () => {
|
||||
// Add new subscriptions, depending on whether nip 29 is supported
|
||||
for (const url of getRelayTagValues(getListTags($l))) {
|
||||
if (hasNip29($relaysByUrl.get(url))) {
|
||||
for (const room of getSpaceRoomsFromGroupSelections(url, $l)) {
|
||||
const id = `${url}'${room}`
|
||||
for (const h of getSpaceRoomsFromGroupSelections(url, $l)) {
|
||||
const key = `${url}'${h}`
|
||||
|
||||
if (!unsubscribersByKey.has(id)) {
|
||||
newUnsubscribersByKey.set(url, syncRoomChat(url, room))
|
||||
if (!unsubscribersByKey.has(key)) {
|
||||
newUnsubscribersByKey.set(key, syncRoom(url, h))
|
||||
}
|
||||
|
||||
keys.add(id)
|
||||
keys.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {page} from "$app/stores"
|
||||
|
||||
export type ModalOptions = {
|
||||
drawer?: boolean
|
||||
noEscape?: boolean
|
||||
fullscreen?: boolean
|
||||
replaceState?: boolean
|
||||
path?: string
|
||||
|
||||
@@ -171,11 +171,9 @@ export const notifications = derived(
|
||||
}
|
||||
|
||||
if (hasNip29($relaysByUrl.get(url))) {
|
||||
for (const room of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
||||
const roomPath = makeRoomPath(url, room)
|
||||
const latestEvent = allMessages.find(
|
||||
e => $getUrlsForEvent(e.id).includes(url) && e.tags.find(spec(["h", room])),
|
||||
)
|
||||
for (const h of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
||||
const roomPath = makeRoomPath(url, h)
|
||||
const latestEvent = messages.find(e => e.tags.some(spec(["h", h])))
|
||||
|
||||
if (hasNotification(roomPath, latestEvent)) {
|
||||
paths.add(spacePathMobile)
|
||||
|
||||
+30
-17
@@ -1,5 +1,4 @@
|
||||
import {on, call, dissoc, assoc, uniq} from "@welshman/lib"
|
||||
import type {StampedEvent} from "@welshman/util"
|
||||
import {on, always, call, dissoc, assoc, uniq} from "@welshman/lib"
|
||||
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
|
||||
import {
|
||||
makeSocketPolicyAuth,
|
||||
@@ -7,11 +6,14 @@ import {
|
||||
isRelayEvent,
|
||||
isRelayOk,
|
||||
isRelayClosed,
|
||||
isRelayNegErr,
|
||||
isClientReq,
|
||||
isClientEvent,
|
||||
isClientClose,
|
||||
isClientNegOpen,
|
||||
isClientNegClose,
|
||||
} from "@welshman/net"
|
||||
import {signer} from "@welshman/app"
|
||||
import {sign} from "@welshman/app"
|
||||
import {
|
||||
userSettingsValues,
|
||||
getSetting,
|
||||
@@ -19,10 +21,7 @@ import {
|
||||
relaysMostlyRestricted,
|
||||
} from "@app/core/state"
|
||||
|
||||
export const authPolicy = makeSocketPolicyAuth({
|
||||
sign: (event: StampedEvent) => signer.get()?.sign(event),
|
||||
shouldAuth: (socket: Socket) => true,
|
||||
})
|
||||
export const authPolicy = makeSocketPolicyAuth({sign, shouldAuth: always(true)})
|
||||
|
||||
export const trustPolicy = (socket: Socket) => {
|
||||
const buffer: RelayMessage[] = []
|
||||
@@ -76,20 +75,32 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id)
|
||||
|
||||
if (!ok && details.startsWith("restricted: ")) {
|
||||
restricted++
|
||||
error = details
|
||||
updateStatus()
|
||||
if (!ok) {
|
||||
if (details.startsWith("auth-required: ")) {
|
||||
total--
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
if (details.startsWith("restricted: ")) {
|
||||
restricted++
|
||||
error = details
|
||||
updateStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isRelayClosed(message)) {
|
||||
if (isRelayClosed(message) || isRelayNegErr(message)) {
|
||||
const [_, id, details = ""] = message
|
||||
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id)
|
||||
|
||||
if (details.startsWith("auth-required: ")) {
|
||||
total--
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
if (details.startsWith("restricted: ")) {
|
||||
restricted++
|
||||
error = details
|
||||
@@ -99,10 +110,12 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
|
||||
}
|
||||
}),
|
||||
on(socket, SocketEvent.Send, (message: ClientMessage) => {
|
||||
if (isClientReq(message)) {
|
||||
total++
|
||||
pending.add(message[1])
|
||||
updateStatus()
|
||||
if (isClientReq(message) || isClientNegOpen(message)) {
|
||||
if (!pending.has(message[1])) {
|
||||
total++
|
||||
pending.add(message[1])
|
||||
updateStatus()
|
||||
}
|
||||
}
|
||||
|
||||
if (isClientEvent(message)) {
|
||||
@@ -111,7 +124,7 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
if (isClientClose(message)) {
|
||||
if (isClientClose(message) || isClientNegClose(message)) {
|
||||
pending.delete(message[1])
|
||||
}
|
||||
}),
|
||||
|
||||
+19
-14
@@ -4,7 +4,7 @@ import * as nip19 from "nostr-tools/nip19"
|
||||
import {goto} from "$app/navigation"
|
||||
import {nthEq, sleep} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {tracker, relaysByUrl} from "@welshman/app"
|
||||
import {tracker, loadRelay} from "@welshman/app"
|
||||
import {scrollToEvent} from "@lib/html"
|
||||
import {identity} from "@welshman/lib"
|
||||
import {
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
hasNip29,
|
||||
ROOM,
|
||||
} from "@app/core/state"
|
||||
import {lastPageBySpaceUrl} from "@app/util/history"
|
||||
|
||||
export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) => {
|
||||
let path = `/spaces/${encodeRelay(url)}`
|
||||
@@ -37,22 +38,26 @@ export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) =>
|
||||
.filter(identity)
|
||||
.map(s => encodeURIComponent(s as string))
|
||||
.join("/")
|
||||
} else {
|
||||
const relay = relaysByUrl.get().get(url)
|
||||
|
||||
if (hasNip29(relay)) {
|
||||
path += "/recent"
|
||||
} else {
|
||||
path += "/chat"
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
export const goToSpace = async (url: string) => {
|
||||
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
|
||||
|
||||
if (prevPath) {
|
||||
goto(prevPath)
|
||||
} else if (hasNip29(await loadRelay(url))) {
|
||||
goto(makeSpacePath(url, "recent"))
|
||||
} else {
|
||||
goto(makeSpacePath("chat"))
|
||||
}
|
||||
}
|
||||
|
||||
export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}`
|
||||
|
||||
export const makeRoomPath = (url: string, room: string) => `/spaces/${encodeRelay(url)}/${room}`
|
||||
export const makeRoomPath = (url: string, h: string) => `/spaces/${encodeRelay(url)}/${h}`
|
||||
|
||||
export const makeSpaceChatPath = (url: string) => makeRoomPath(url, "chat")
|
||||
|
||||
@@ -103,7 +108,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||
return makeChatPath([event.pubkey, ...getPubkeyTagValues(event.tags)])
|
||||
}
|
||||
|
||||
const room = getTagValue(ROOM, event.tags)
|
||||
const h = getTagValue(ROOM, event.tags)
|
||||
|
||||
if (urls.length > 0) {
|
||||
const url = urls[0]
|
||||
@@ -121,7 +126,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||
}
|
||||
|
||||
if (event.kind === MESSAGE) {
|
||||
return room ? makeRoomPath(url, room) : makeSpacePath(url, "chat")
|
||||
return h ? makeRoomPath(url, h) : makeSpacePath(url, "chat")
|
||||
}
|
||||
|
||||
const kind = event.tags.find(nthEq(0, "K"))?.[1]
|
||||
@@ -141,7 +146,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||
}
|
||||
|
||||
if (parseInt(kind) === MESSAGE) {
|
||||
return room ? makeRoomPath(url, room) : makeSpacePath(url, "chat")
|
||||
return h ? makeRoomPath(url, h) : makeSpacePath(url, "chat")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,7 +154,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
|
||||
}
|
||||
|
||||
export const getChannelItemPath = (url: string, event: TrustedEvent) => {
|
||||
export const getRoomItemPath = (url: string, event: TrustedEvent) => {
|
||||
switch (event.kind) {
|
||||
case THREAD:
|
||||
return makeThreadPath(url, event.id)
|
||||
|
||||
+14
-5
@@ -25,7 +25,9 @@ import {
|
||||
ROOM_ADD_MEMBER,
|
||||
ROOM_CREATE_PERMISSION,
|
||||
ROOM_MEMBERS,
|
||||
ROOM_ADMINS,
|
||||
ROOM_META,
|
||||
ROOM_DELETE,
|
||||
ROOM_REMOVE_MEMBER,
|
||||
ROOMS,
|
||||
THREAD,
|
||||
@@ -48,6 +50,7 @@ import {
|
||||
wrapManager,
|
||||
} from "@welshman/app"
|
||||
import {Collection} from "@lib/storage"
|
||||
import {isMobile} from "@lib/html"
|
||||
|
||||
const syncEvents = async () => {
|
||||
const collection = new Collection<TrustedEvent>({table: "events", getId: prop("id")})
|
||||
@@ -75,6 +78,8 @@ const syncEvents = async () => {
|
||||
const spaceKinds = [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE]
|
||||
const roomKinds = [
|
||||
ROOM_META,
|
||||
ROOM_DELETE,
|
||||
ROOM_ADMINS,
|
||||
ROOM_MEMBERS,
|
||||
ROOM_ADD_MEMBER,
|
||||
ROOM_REMOVE_MEMBER,
|
||||
@@ -87,7 +92,7 @@ const syncEvents = async () => {
|
||||
if (alertKinds.includes(event.kind)) return 8
|
||||
if (spaceKinds.includes(event.kind)) return 7
|
||||
if (roomKinds.includes(event.kind)) return 6
|
||||
if (contentKinds.includes(event.kind)) return 5
|
||||
if (!isMobile && contentKinds.includes(event.kind)) return 5
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -236,17 +241,21 @@ const syncWrapManager = async () => {
|
||||
}
|
||||
|
||||
export const syncDataStores = async () => {
|
||||
const unsubscribers = await Promise.all([
|
||||
const promises = [
|
||||
syncEvents(),
|
||||
syncTracker(),
|
||||
syncRelays(),
|
||||
syncRelayStats(),
|
||||
syncHandles(),
|
||||
syncZappers(),
|
||||
syncFreshness(),
|
||||
syncPlaintext(),
|
||||
syncWrapManager(),
|
||||
])
|
||||
]
|
||||
|
||||
if (!isMobile) {
|
||||
promises.push(syncFreshness(), syncRelayStats())
|
||||
}
|
||||
|
||||
const unsubscribers = await Promise.all(promises)
|
||||
|
||||
return () => unsubscribers.forEach(call)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {noop} from "@welshman/lib"
|
||||
import {fade, fly} from "@lib/transition"
|
||||
|
||||
@@ -12,7 +13,11 @@
|
||||
|
||||
const extraClass = $derived(
|
||||
!fullscreen &&
|
||||
"card2 bg-alt max-h-[90vh] w-[90vw] overflow-auto text-base-content sm:w-[520px] shadow-xl",
|
||||
cx(
|
||||
"bg-alt text-base-content overflow-auto text-base-content shadow-xl",
|
||||
"px-4 py-6 bottom-0 left-0 right-0 top-20 rounded-t-box absolute",
|
||||
"sm:p-6 sm:max-h-[90vh] sm:w-[520px] sm:rounded-box sm:relative sm:top-0",
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -23,7 +28,7 @@
|
||||
transition:fade={{duration: 300}}
|
||||
onclick={onClose}>
|
||||
</button>
|
||||
<div class="scroll-container relative {extraClass}" transition:fly={{duration: 300}}>
|
||||
<div class="scroll-container {extraClass}" transition:fly={{duration: 300}}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
{@render props.input?.()}
|
||||
</div>
|
||||
<p class="flex-end text-sm md:col-span-3">
|
||||
<p class="flex-end text-sm opacity-70 md:col-span-3">
|
||||
{#if props.info}
|
||||
{@render props.info?.()}
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
type Props = {
|
||||
src: string
|
||||
alt: string
|
||||
}
|
||||
|
||||
const {src, alt}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if src.includes("image/svg") || src.endsWith(".svg")}
|
||||
<Icon icon={src} />
|
||||
{:else}
|
||||
<img {src} {alt} class="h-5 w-5 rounded-lg object-cover" />
|
||||
{/if}
|
||||
@@ -9,5 +9,5 @@
|
||||
|
||||
<div class="column m-auto max-w-xs gap-2 py-4">
|
||||
<h1 class="heading">{@render title?.()}</h1>
|
||||
<p class="text-center">{@render info?.()}</p>
|
||||
<p class="text-center text-sm opacity-75">{@render info?.()}</p>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
const {...props}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div data-component="PageBar" class="cw top-sai fixed z-feature p-2">
|
||||
<div data-component="PageBar" class="cw top-sai fixed z-feature p-2 {props.class}">
|
||||
<div
|
||||
class="flex min-h-12 items-center justify-between gap-4 rounded-xl bg-base-100 px-4 shadow-xl">
|
||||
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
{...props}
|
||||
bind:this={element}
|
||||
data-component="PageContent"
|
||||
class="scroll-container cw md:bottom-sai fixed bottom-[calc(var(--saib)+3.5rem)] top-[calc(var(--sait)+3rem)] overflow-y-auto overflow-x-hidden {props.class}">
|
||||
class="scroll-container cw md:bottom-sai fixed bottom-[calc(var(--saib)+3.5rem)] top-[calc(var(--sait)+3rem)] z-feature overflow-y-auto overflow-x-hidden {props.class}">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {goToSpace} from "@app/util/routes"
|
||||
import {PLATFORM_RELAYS} from "@app/core/state"
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
if (PLATFORM_RELAYS.length > 0) {
|
||||
goto(makeSpacePath(PLATFORM_RELAYS[0]))
|
||||
goToSpace(PLATFORM_RELAYS[0])
|
||||
} else {
|
||||
goto("/home")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {debounce} from "throttle-debounce"
|
||||
import {dec, tryCatch} from "@welshman/lib"
|
||||
import {dec} from "@welshman/lib"
|
||||
import type {RelayProfile} from "@welshman/util"
|
||||
import {ROOMS, normalizeRelayUrl, isRelayUrl} from "@welshman/util"
|
||||
import {ROOMS} from "@welshman/util"
|
||||
import {Router} from "@welshman/router"
|
||||
import {load} from "@welshman/net"
|
||||
import {relays, createSearch, loadRelay} from "@welshman/app"
|
||||
@@ -28,12 +28,11 @@
|
||||
loadGroupSelections,
|
||||
getSpaceUrlsFromGroupSelections,
|
||||
groupSelectionsPubkeysByUrl,
|
||||
parseInviteLink,
|
||||
} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const openMenu = () => pushModal(SpaceAdd)
|
||||
|
||||
const termUrl = $derived(tryCatch(() => normalizeRelayUrl(term)) || "")
|
||||
const openMenu = () => pushModal(SpaceAdd, {hideDiscover: true})
|
||||
|
||||
const toggleScanner = () => {
|
||||
showScanner = !showScanner
|
||||
@@ -60,7 +59,7 @@
|
||||
|
||||
const relaySearch = $derived(
|
||||
createSearch(
|
||||
$relays.filter(r => $groupSelectionsPubkeysByUrl.has(r.url) && r.url !== termUrl),
|
||||
$relays.filter(r => $groupSelectionsPubkeysByUrl.has(r.url) && r.url !== inviteData?.url),
|
||||
{
|
||||
getValue: (relay: RelayProfile) => relay.url,
|
||||
sortFn: ({score, item}) => {
|
||||
@@ -78,13 +77,21 @@
|
||||
),
|
||||
)
|
||||
|
||||
const openSpace = (url: string) => pushModal(SpaceCheck, {url})
|
||||
const openSpace = (url: string, claim = "") => {
|
||||
if (claim) {
|
||||
pushModal(SpaceInviteAccept, {invite: term})
|
||||
} else {
|
||||
pushModal(SpaceCheck, {url})
|
||||
}
|
||||
}
|
||||
|
||||
let term = $state("")
|
||||
let limit = $state(20)
|
||||
let showScanner = $state(false)
|
||||
let element: Element
|
||||
|
||||
const inviteData = $derived(parseInviteLink(term))
|
||||
|
||||
onMount(() => {
|
||||
const scroller = createScroller({
|
||||
element,
|
||||
@@ -114,13 +121,18 @@
|
||||
<div class="row-2 min-w-0 flex-grow items-center">
|
||||
<label class="input input-bordered flex flex-grow items-center gap-2">
|
||||
<Icon icon={Magnifier} />
|
||||
<input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." />
|
||||
<input
|
||||
bind:value={term}
|
||||
class="grow"
|
||||
type="text"
|
||||
placeholder="Search for spaces or paste invite link..." />
|
||||
<Button onclick={toggleScanner} class="center">
|
||||
<Icon icon={QrCode} />
|
||||
</Button>
|
||||
</label>
|
||||
<Button class="btn btn-primary" onclick={openMenu}>
|
||||
<Icon icon={AddCircle} />
|
||||
<span class="hidden sm:inline">Add Space</span>
|
||||
</Button>
|
||||
</div>
|
||||
{#if showScanner}
|
||||
@@ -130,15 +142,15 @@
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="col-2 scroll-container" bind:this={element}>
|
||||
{#key termUrl}
|
||||
{#if isRelayUrl(termUrl)}
|
||||
{#if inviteData}
|
||||
{#key inviteData.url}
|
||||
<Button
|
||||
class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]"
|
||||
onclick={() => openSpace(termUrl)}>
|
||||
<RelaySummary url={termUrl} />
|
||||
onclick={() => openSpace(inviteData.url, inviteData.claim)}>
|
||||
<RelaySummary url={inviteData.url} />
|
||||
</Button>
|
||||
{/if}
|
||||
{/key}
|
||||
{/key}
|
||||
{/if}
|
||||
{#each relaySearch.searchOptions(term).slice(0, limit) as relay (relay.url)}
|
||||
<Button
|
||||
class="card2 bg-alt shadow-xl transition-all hover:shadow-2xl hover:dark:brightness-[1.1]"
|
||||
|
||||
@@ -12,16 +12,16 @@
|
||||
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {goToSpace} from "@app/util/routes"
|
||||
import {PLATFORM_NAME, PLATFORM_RELAYS} from "@app/core/state"
|
||||
|
||||
const addSpace = () => pushModal(SpaceAdd)
|
||||
|
||||
const openChat = () => ($shouldUnwrap ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
if (PLATFORM_RELAYS.length > 0) {
|
||||
goto(makeSpacePath(PLATFORM_RELAYS[0]))
|
||||
goToSpace(PLATFORM_RELAYS[0])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
import AltArrowUp from "@assets/icons/alt-arrow-up.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
|
||||
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||
import {userSpaceUrls, PLATFORM_RELAYS} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const addSpace = () => pushModal(SpaceAdd)
|
||||
</script>
|
||||
|
||||
<Page class="cw-full">
|
||||
<PageBar class="cw-full">
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon={SettingsMinimalistic} />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong>Your Spaces</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
{#if $userSpaceUrls.length > 0 && PLATFORM_RELAYS.length === 0}
|
||||
<Button class="btn btn-primary btn-sm" onclick={addSpace}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Space
|
||||
</Button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
<PageContent class="cw-full flex flex-col gap-2 p-2 pt-4">
|
||||
{#each PLATFORM_RELAYS as url (url)}
|
||||
<MenuSpacesItem {url} />
|
||||
{:else}
|
||||
{#each $userSpaceUrls as url (url)}
|
||||
<MenuSpacesItem {url} />
|
||||
{:else}
|
||||
<div class="flex flex-col gap-8 items-center py-20">
|
||||
<p>You haven't added any spaces yet!</p>
|
||||
<Button class="btn btn-primary" onclick={addSpace}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add a Space
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</PageContent>
|
||||
</Page>
|
||||
@@ -3,7 +3,6 @@
|
||||
import {page} from "$app/stores"
|
||||
import {once} from "@welshman/lib"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import Dialog from "@lib/components/Dialog.svelte"
|
||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||
import SpaceAuthError from "@app/components/SpaceAuthError.svelte"
|
||||
@@ -23,7 +22,11 @@
|
||||
|
||||
const authError = deriveRelayAuthError(url)
|
||||
|
||||
const showAuthError = once(() => pushModal(SpaceAuthError, {url, error: $authError}))
|
||||
const showAuthError = once(() =>
|
||||
pushModal(SpaceAuthError, {url, error: $authError}, {noEscape: true}),
|
||||
)
|
||||
|
||||
const showPendingTrust = once(() => pushModal(SpaceTrustRelay, {url}, {noEscape: true}))
|
||||
|
||||
// We have to watch this one, since on mobile the badge will be visible when active
|
||||
$effect(() => {
|
||||
@@ -36,6 +39,8 @@
|
||||
$effect(() => {
|
||||
if ($authError) {
|
||||
showAuthError()
|
||||
} else if ($relaysPendingTrust.includes(url)) {
|
||||
showPendingTrust()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -48,9 +53,3 @@
|
||||
{@render children?.()}
|
||||
{/key}
|
||||
</Page>
|
||||
|
||||
{#if $relaysPendingTrust.includes(url)}
|
||||
<Dialog>
|
||||
<SpaceTrustRelay {url} />
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
+81
-96
@@ -3,7 +3,6 @@
|
||||
import {readable} from "svelte/store"
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
import type {Readable} from "svelte/store"
|
||||
import type {MakeNonOptional} from "@welshman/lib"
|
||||
import {now, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
||||
@@ -15,20 +14,12 @@
|
||||
ROOM_ADD_MEMBER,
|
||||
ROOM_REMOVE_MEMBER,
|
||||
} from "@welshman/util"
|
||||
import {
|
||||
pubkey,
|
||||
publishThunk,
|
||||
waitForThunkError,
|
||||
deleteRoom,
|
||||
joinRoom,
|
||||
leaveRoom,
|
||||
repository,
|
||||
} from "@welshman/app"
|
||||
import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
|
||||
import {slide, fade, fly} from "@lib/transition"
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||
import Login2 from "@assets/icons/login-3.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
import Logout2 from "@assets/icons/logout-3.svg?dataurl"
|
||||
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||
@@ -38,22 +29,22 @@
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import ChannelItem from "@app/components/ChannelItem.svelte"
|
||||
import ChannelItemAddMember from "@src/app/components/ChannelItemAddMember.svelte"
|
||||
import ChannelItemRemoveMember from "@src/app/components/ChannelItemRemoveMember.svelte"
|
||||
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
||||
import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte"
|
||||
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
|
||||
import RoomEdit from "@app/components/RoomEdit.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import RoomItem from "@app/components/RoomItem.svelte"
|
||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||
import RoomItemRemoveMember from "@src/app/components/RoomItemRemoveMember.svelte"
|
||||
import RoomCompose from "@app/components/RoomCompose.svelte"
|
||||
import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
|
||||
import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
|
||||
import {
|
||||
deriveUserRooms,
|
||||
userSettingsValues,
|
||||
decodeRelay,
|
||||
deriveUserRoomMembershipStatus,
|
||||
deriveChannel,
|
||||
deriveRoom,
|
||||
MembershipStatus,
|
||||
PROTECTED,
|
||||
MESSAGE_KINDS,
|
||||
@@ -71,28 +62,27 @@
|
||||
import {popKey} from "@lib/implicit"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
|
||||
const {room, relay} = $page.params as MakeNonOptional<typeof $page.params>
|
||||
const {h, relay} = $page.params as MakeNonOptional<typeof $page.params>
|
||||
const mounted = now()
|
||||
const lastChecked = $checked[$page.url.pathname]
|
||||
const url = decodeRelay(relay)
|
||||
const channel = deriveChannel(url, room)
|
||||
const room = deriveRoom(url, h)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const userRooms = deriveUserRooms(url)
|
||||
const userIsAdmin = deriveUserIsRoomAdmin(url, room)
|
||||
const isFavorite = $derived($userRooms.includes(room))
|
||||
const membershipStatus = deriveUserRoomMembershipStatus(url, room)
|
||||
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
||||
const isFavorite = $derived($userRooms.includes(h))
|
||||
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
||||
|
||||
const addFavorite = () => addRoomMembership(url, room)
|
||||
const addFavorite = () => addRoomMembership(url, h)
|
||||
|
||||
const removeFavorite = () => removeRoomMembership(url, room)
|
||||
const removeFavorite = () => removeRoomMembership(url, h)
|
||||
|
||||
const join = async () => {
|
||||
joining = true
|
||||
|
||||
try {
|
||||
const message = await waitForThunkError(joinRoom(url, makeRoomMeta({id: room})))
|
||||
const message = await waitForThunkError(joinRoom(url, makeRoomMeta({h})))
|
||||
|
||||
if (message && !message.startsWith("duplicate:")) {
|
||||
return pushToast({theme: "error", message})
|
||||
@@ -108,7 +98,7 @@
|
||||
const leave = async () => {
|
||||
leaving = true
|
||||
try {
|
||||
const message = await waitForThunkError(leaveRoom(url, makeRoomMeta({id: room})))
|
||||
const message = await waitForThunkError(leaveRoom(url, makeRoomMeta({h})))
|
||||
|
||||
if (message && !message.startsWith("duplicate:")) {
|
||||
pushToast({theme: "error", message})
|
||||
@@ -136,7 +126,7 @@
|
||||
}
|
||||
|
||||
const onSubmit = async ({content, tags}: EventContent) => {
|
||||
tags.push(["h", room])
|
||||
tags.push(["h", h])
|
||||
|
||||
if (await shouldProtect) {
|
||||
tags.push(PROTECTED)
|
||||
@@ -214,7 +204,7 @@
|
||||
let showScrollButton = $state(false)
|
||||
let cleanup: () => void
|
||||
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
||||
let compose: ChannelCompose | undefined = $state()
|
||||
let compose: RoomCompose | undefined = $state()
|
||||
let eventToEdit: TrustedEvent | undefined = $state()
|
||||
|
||||
const elements = $derived.by(() => {
|
||||
@@ -280,7 +270,7 @@
|
||||
const feed = makeFeed({
|
||||
url,
|
||||
element: element!,
|
||||
filters: [{kinds: [...MESSAGE_KINDS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]}],
|
||||
filters: [{kinds: [...MESSAGE_KINDS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]}],
|
||||
onExhausted: () => {
|
||||
loadingEvents = false
|
||||
},
|
||||
@@ -290,6 +280,12 @@
|
||||
cleanup = feed.cleanup
|
||||
}
|
||||
|
||||
const onEscape = () => {
|
||||
clearParent()
|
||||
clearShare()
|
||||
eventToEdit = undefined
|
||||
}
|
||||
|
||||
const canEditEvent = (event: TrustedEvent) =>
|
||||
event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE)
|
||||
|
||||
@@ -307,23 +303,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
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, makeRoomMeta({id: room}))
|
||||
const message = await waitForThunkError(thunk)
|
||||
|
||||
if (message) {
|
||||
repository.removeEvent(thunk.event.id)
|
||||
pushToast({theme: "error", message})
|
||||
} else {
|
||||
goto(makeSpacePath(url))
|
||||
}
|
||||
},
|
||||
})
|
||||
const startEdit = () => pushModal(RoomEdit, {url, h})
|
||||
|
||||
onMount(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
@@ -360,17 +340,17 @@
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong class="ellipsize">
|
||||
<ChannelName {url} {room} />
|
||||
<RoomName {url} {h} />
|
||||
</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div class="row-2">
|
||||
{#if $userIsAdmin || true}
|
||||
{#if $userIsAdmin}
|
||||
<Button
|
||||
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||
data-tip="Delete this room"
|
||||
onclick={startDelete}>
|
||||
<Icon size={4} icon={TrashBin2} />
|
||||
data-tip="Edit room information"
|
||||
onclick={startEdit}>
|
||||
<Icon size={4} icon={Pen} />
|
||||
</Button>
|
||||
{:else if $membershipStatus === MembershipStatus.Initial}
|
||||
<Button
|
||||
@@ -412,24 +392,26 @@
|
||||
|
||||
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4">
|
||||
<div bind:this={dynamicPadding}></div>
|
||||
{#if $channel?.private && $membershipStatus !== MembershipStatus.Granted}
|
||||
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
||||
<div class="py-20">
|
||||
<div class="card2 col-8 m-auto max-w-md items-center text-center">
|
||||
<p class="row-2">You aren't currently a member of this room.</p>
|
||||
{#if $membershipStatus === MembershipStatus.Pending}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
||||
<Icon icon={ClockCircle} />
|
||||
Access Pending
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
||||
{#if joining}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={Login2} />
|
||||
{/if}
|
||||
Join Room
|
||||
</Button>
|
||||
<p class="opacity-75">You aren't currently a member of this room.</p>
|
||||
{#if !$room.isClosed}
|
||||
{#if $membershipStatus === MembershipStatus.Pending}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
||||
<Icon icon={ClockCircle} />
|
||||
Access Pending
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
||||
{#if joining}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={Login2} />
|
||||
{/if}
|
||||
Join Room
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -449,12 +431,12 @@
|
||||
{:else}
|
||||
{@const event = $state.snapshot(value as TrustedEvent)}
|
||||
{#if event.kind === ROOM_ADD_MEMBER}
|
||||
<ChannelItemAddMember {url} {event} />
|
||||
<RoomItemAddMember {url} {event} />
|
||||
{:else if event.kind === ROOM_REMOVE_MEMBER}
|
||||
<ChannelItemRemoveMember {url} {event} />
|
||||
<RoomItemRemoveMember {url} {event} />
|
||||
{:else}
|
||||
<div in:slide class:-mt-1={!showPubkey}>
|
||||
<ChannelItem
|
||||
<RoomItem
|
||||
{url}
|
||||
{event}
|
||||
{replyTo}
|
||||
@@ -476,44 +458,47 @@
|
||||
</PageContent>
|
||||
|
||||
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
||||
{#if $channel?.private && $membershipStatus !== MembershipStatus.Granted}
|
||||
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
||||
<!-- pass -->
|
||||
{:else if $channel?.closed && $membershipStatus !== MembershipStatus.Granted}
|
||||
{:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
|
||||
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
|
||||
<p>Only members are allowed to post to this room.</p>
|
||||
{#if $membershipStatus === MembershipStatus.Pending}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
||||
<Icon icon={ClockCircle} />
|
||||
Access Pending
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
||||
{#if joining}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={Login2} />
|
||||
{/if}
|
||||
Ask to Join
|
||||
</Button>
|
||||
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
||||
{#if !$room.isClosed}
|
||||
{#if $membershipStatus === MembershipStatus.Pending}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
||||
<Icon icon={ClockCircle} />
|
||||
Access Pending
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
||||
{#if joining}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={Login2} />
|
||||
{/if}
|
||||
Ask to Join
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
{#if parent}
|
||||
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||
<RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||
{/if}
|
||||
{#if share}
|
||||
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
|
||||
<RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
|
||||
{/if}
|
||||
{#if eventToEdit}
|
||||
<ChannelComposeEdit clear={clearEventToEdit} />
|
||||
<RoomComposeEdit clear={clearEventToEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
{#key eventToEdit}
|
||||
<ChannelCompose
|
||||
<RoomCompose
|
||||
{url}
|
||||
{room}
|
||||
{h}
|
||||
{onSubmit}
|
||||
{onEscape}
|
||||
{onEditPrevious}
|
||||
content={eventToEdit?.content}
|
||||
bind:this={compose} />
|
||||
@@ -18,12 +18,12 @@
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||
import ChannelItem from "@app/components/ChannelItem.svelte"
|
||||
import ChannelItemAddMember from "@src/app/components/ChannelItemAddMember.svelte"
|
||||
import ChannelItemRemoveMember from "@src/app/components/ChannelItemRemoveMember.svelte"
|
||||
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
||||
import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte"
|
||||
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
|
||||
import RoomItem from "@app/components/RoomItem.svelte"
|
||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||
import RoomItemRemoveMember from "@src/app/components/RoomItemRemoveMember.svelte"
|
||||
import RoomCompose from "@app/components/RoomCompose.svelte"
|
||||
import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
|
||||
import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
|
||||
import {userSettingsValues, decodeRelay, PROTECTED, MESSAGE_KINDS} from "@app/core/state"
|
||||
import {prependParent, canEnforceNip70, publishDelete} from "@app/core/commands"
|
||||
import {setChecked, checked} from "@app/util/notifications"
|
||||
@@ -128,7 +128,7 @@
|
||||
let showScrollButton = $state(false)
|
||||
let cleanup: () => void
|
||||
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
||||
let compose: ChannelCompose | undefined = $state()
|
||||
let compose: RoomCompose | undefined = $state()
|
||||
let eventToEdit: TrustedEvent | undefined = $state()
|
||||
|
||||
const elements = $derived.by(() => {
|
||||
@@ -188,6 +188,12 @@
|
||||
return elements
|
||||
})
|
||||
|
||||
const onEscape = () => {
|
||||
clearParent()
|
||||
clearShare()
|
||||
eventToEdit = undefined
|
||||
}
|
||||
|
||||
const canEditEvent = (event: TrustedEvent) =>
|
||||
event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE)
|
||||
|
||||
@@ -274,12 +280,12 @@
|
||||
{:else}
|
||||
{@const event = $state.snapshot(value as TrustedEvent)}
|
||||
{#if event.kind === RELAY_ADD_MEMBER}
|
||||
<ChannelItemAddMember {url} {event} />
|
||||
<RoomItemAddMember {url} {event} />
|
||||
{:else if event.kind === RELAY_REMOVE_MEMBER}
|
||||
<ChannelItemRemoveMember {url} {event} />
|
||||
<RoomItemRemoveMember {url} {event} />
|
||||
{:else}
|
||||
<div class:-mt-1={!showPubkey}>
|
||||
<ChannelItem
|
||||
<RoomItem
|
||||
{url}
|
||||
{event}
|
||||
{replyTo}
|
||||
@@ -302,19 +308,20 @@
|
||||
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
||||
<div>
|
||||
{#if parent}
|
||||
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||
<RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||
{/if}
|
||||
{#if share}
|
||||
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
|
||||
<RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
|
||||
{/if}
|
||||
{#if eventToEdit}
|
||||
<ChannelComposeEdit clear={clearEventToEdit} />
|
||||
<RoomComposeEdit clear={clearEventToEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
{#key eventToEdit}
|
||||
<ChannelCompose
|
||||
<RoomCompose
|
||||
{url}
|
||||
{onSubmit}
|
||||
{onEscape}
|
||||
{onEditPrevious}
|
||||
content={eventToEdit?.content}
|
||||
bind:this={compose} />
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
const conversations = derived(messages, $messages => {
|
||||
const convs = []
|
||||
|
||||
for (const [room, messages] of groupBy(e => getTagValue("h", e.tags), $messages).entries()) {
|
||||
for (const [h, messages] of groupBy(e => getTagValue("h", e.tags), $messages).entries()) {
|
||||
const avgTime = avg(overlappingPairs(messages).map(([a, b]) => a.created_at - b.created_at))
|
||||
const groups: TrustedEvent[][] = []
|
||||
const group: TrustedEvent[] = []
|
||||
@@ -52,7 +52,7 @@
|
||||
const earliest = last(events)!
|
||||
const participants = uniq(events.map(msg => msg.pubkey))
|
||||
|
||||
convs.push({room, events, latest, earliest, participants})
|
||||
convs.push({h, events, latest, earliest, participants})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +96,10 @@
|
||||
{#if $messages.length > 0}
|
||||
{@const events = $messages.slice(0, 1)}
|
||||
{@const event = events[0]}
|
||||
{@const room = getTagValue("h", event.tags)}
|
||||
{@const h = getTagValue("h", event.tags)}
|
||||
<ConversationCard
|
||||
{h}
|
||||
{url}
|
||||
{room}
|
||||
{events}
|
||||
latest={event}
|
||||
earliest={event}
|
||||
@@ -110,8 +110,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each $conversations.slice(0, limit) as { room, events, latest, earliest, participants } (latest.id)}
|
||||
<ConversationCard {url} {room} {events} {latest} {earliest} {participants} />
|
||||
{#each $conversations.slice(0, limit) as { h, events, latest, earliest, participants } (latest.id)}
|
||||
<ConversationCard {h} {url} {events} {latest} {earliest} {participants} />
|
||||
{/each}
|
||||
{/if}
|
||||
</PageContent>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import Server from "@assets/icons/server.svg?dataurl"
|
||||
import ArrowRight from "@assets/icons/arrow-right.svg?dataurl"
|
||||
import HandShake from "@assets/icons/hand-shake.svg?dataurl"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import PageHeader from "@lib/components/PageHeader.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
</script>
|
||||
|
||||
<Page class="cw-full">
|
||||
<PageContent class="cw-full flex flex-col items-center gap-2 p-2 pt-4">
|
||||
<PageHeader>
|
||||
{#snippet title()}
|
||||
<div>Create your own Space</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>Get started with one of our trusted partners, or learn how to host your own space.</p>
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
<div class="grid w-full max-w-lg grid-cols-1 gap-2 lg:max-w-4xl lg:grid-cols-2">
|
||||
<div class="card2 bg-alt flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Icon icon={Server} />
|
||||
<h3 class="text-lg font-bold">Self-Host your Space</h3>
|
||||
</div>
|
||||
<div class="badge badge-neutral">Recommended</div>
|
||||
</div>
|
||||
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
|
||||
<li>Unlimited customization and control</li>
|
||||
<li>Free and open source software</li>
|
||||
<li>Full-featured admin dashboards available</li>
|
||||
<li>Requires some technical skills</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Link class="btn btn-primary" href="https://github.com/coracle-social/zooid">
|
||||
Get Started
|
||||
<Icon icon={ArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
<div class="card2 bg-alt flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<img alt="Coracle Logo" src="/coracle.png" class="h-7 w-7" />
|
||||
<h3 class="text-lg font-bold">Coracle Hosting</h3>
|
||||
</div>
|
||||
<div class="badge badge-neutral">Recommended</div>
|
||||
</div>
|
||||
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
|
||||
<li>Simple setup, support included</li>
|
||||
<li>Free and open source software — no vendor lock-in</li>
|
||||
<li>Advanced access controls and relay policies</li>
|
||||
<li>Full-featured admin dashboard</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Link external class="btn btn-neutral" href="https://hosting.coracle.social">
|
||||
Get Started
|
||||
<Icon icon={ArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
<div class="card2 bg-alt flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={HandShake} />
|
||||
<h3 class="text-lg font-bold">Holis Communities</h3>
|
||||
</div>
|
||||
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
|
||||
<li>Simple self-serve space creation</li>
|
||||
<li>Built-in moderation tools</li>
|
||||
<li>Room-level access controls</li>
|
||||
<li>Membship lists and invite codes</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Link external class="btn btn-neutral" href="https://hol.is">
|
||||
Get Started
|
||||
<Icon icon={ArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
<div class="card2 bg-alt flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="self-start">
|
||||
<img
|
||||
alt="Relay Tools"
|
||||
src="https://relay.tools/17.svg"
|
||||
class="-my-20 -ml-2 hidden h-48 dark:block"
|
||||
style="filter: contrast(50%)" />
|
||||
<img
|
||||
alt="Relay Tools"
|
||||
src="https://relay.tools/19.svg"
|
||||
class="-my-20 -ml-2 h-48 dark:hidden"
|
||||
style="filter: contrast(50%)" />
|
||||
</div>
|
||||
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
|
||||
<li>Independently run</li>
|
||||
<li>Customizable relay policies</li>
|
||||
<li>Simple management dashboard</li>
|
||||
<li>Support available</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Link class="btn btn-neutral" href="https://relay.tools/signup">
|
||||
Get Started
|
||||
<Icon icon={ArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</PageContent>
|
||||
</Page>
|
||||
+10
-12
@@ -8,11 +8,8 @@ config({path: ".env.template"})
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
darkMode: ['selector', '[data-theme="dark"]'],
|
||||
safelist: [
|
||||
'bg-success',
|
||||
'bg-warning',
|
||||
],
|
||||
darkMode: ["selector", '[data-theme="dark"]'],
|
||||
safelist: ["bg-success", "bg-warning"],
|
||||
theme: {
|
||||
extend: {},
|
||||
zIndex: {
|
||||
@@ -20,11 +17,12 @@ export default {
|
||||
"nav-active": 1,
|
||||
"nav-item": 2,
|
||||
feature: 3,
|
||||
nav: 4,
|
||||
popover: 5,
|
||||
modal: 6,
|
||||
"modal-feature": 7,
|
||||
toast: 8,
|
||||
compose: 4,
|
||||
nav: 5,
|
||||
popover: 6,
|
||||
modal: 7,
|
||||
"modal-feature": 8,
|
||||
toast: 9,
|
||||
},
|
||||
},
|
||||
plugins: [daisyui],
|
||||
@@ -40,8 +38,8 @@ export default {
|
||||
},
|
||||
light: {
|
||||
...themes["winter"],
|
||||
neutral: '#F2F7FF',
|
||||
warning: '#FD8D0B',
|
||||
neutral: "#F2F7FF",
|
||||
warning: "#FD8D0B",
|
||||
primary: process.env.VITE_PLATFORM_ACCENT,
|
||||
"primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF",
|
||||
secondary: process.env.VITE_PLATFORM_SECONDARY,
|
||||
|
||||
Reference in New Issue
Block a user