Compare commits

..

92 Commits

Author SHA1 Message Date
Jon Staab 7bae956ffa Release 1.6.2 2025-12-08 09:22:49 -08:00
Jon Staab a2f59a5b1b Fix some modal bugs 2025-12-08 09:19:41 -08:00
Jon Staab df56af9b0e Bump version 2025-12-05 09:51:15 -08:00
Jon Staab 83f7f9584f Fix duplicate rooms 2025-12-04 17:06:50 -08:00
Jon Staab a2d440e54f Fix dialog z index 2025-12-04 16:01:39 -08:00
Jon Staab 4132e8449b Fix recent missing events in feeds 2025-12-04 15:56:05 -08:00
Jon Staab ee444416e4 Fall back to file name as hash for images 2025-12-04 14:37:59 -08:00
Jon Staab 10c12c3c48 Improve time based chat partitioning 2025-12-04 14:29:12 -08:00
Jon Staab db3775ae99 Fix timezone parsing in AlertAdd 2025-12-04 11:20:54 -08:00
Jon Staab 393acce884 Fix removing non-normalized urls 2025-12-02 17:27:14 -08:00
Jon Staab 68fe663730 Fix chat content bottom offset when keyboard is open 2025-12-02 17:20:10 -08:00
Jon Staab f65a4b0db0 Handle relay urls in content and link within the app 2025-12-02 17:09:56 -08:00
Jon Staab cdfb502e6e Fix skinny profile circles 2025-12-02 13:49:04 -08:00
Jon Staab 1a2c83e49b Bump version 2025-12-02 13:38:03 -08:00
Jon Staab e6c7a675a9 Bump welshman 2025-12-02 13:24:43 -08:00
Jon Staab 69c04f29f4 Tweak zap button 2025-12-02 09:31:38 -08:00
Jon Staab 04c6f9b4fe Add date to chats 2025-12-01 11:09:26 -08:00
Jon Staab 86ec12a9db Tweak some mobile menu components 2025-12-01 11:04:11 -08:00
Jon Staab 72b3111c64 Refine sync 2025-12-01 10:56:37 -08:00
Jon Staab 6709c91779 Fix discover social proof 2025-12-01 10:26:43 -08:00
Jon Staab bb6e7495f5 Add editor props from nostr-editor 2025-12-01 08:45:35 -08:00
Jon Staab df17929681 Fix new messages indicator 2025-11-25 17:06:21 -08:00
Jon Staab e083719ceb Hide nav when keyboard is open 2025-11-25 15:43:21 -08:00
Jon Staab bfdc69f18c Fix chats 2025-11-25 15:05:45 -08:00
Jon Staab e7ae20afb7 Fix content type nav items 2025-11-25 14:13:28 -08:00
Jon Staab 229d92055f Debounce search 2025-11-25 11:55:32 -08:00
Jon Staab 64c77cfd13 Migrate to new welshman stores 2025-11-21 12:40:59 -08:00
Jon Staab 3a63894562 Switch wording to messaging from inbox 2025-11-20 15:12:16 -08:00
Jon Staab 1d272f8b37 Tweak nav icon size 2025-11-14 15:02:23 -08:00
Jon Staab bac433b640 Re-work storage adapter a bit 2025-11-14 14:59:27 -08:00
Jon Staab 62f573eac0 Merge report detail components 2025-11-14 11:36:51 -08:00
Jon Staab b3ea62c53c Remove landlubber link 2025-11-13 17:01:23 -08:00
Jon Staab b0731503a8 Fix indexeddb deletes 2025-11-13 16:39:44 -08:00
Jon Staab 2421c02c24 Add room membership management 2025-11-13 15:25:18 -08:00
Jon Staab 25e868118d Slight optimization 2025-11-13 14:40:02 -08:00
Jon Staab 2880044e0e Add event admin deletion 2025-11-13 14:25:59 -08:00
Jon Staab 5300404b46 Add option to ban users from profile detail dialog 2025-11-13 13:44:52 -08:00
Jon Staab d949d58076 Add space membership management 2025-11-13 13:25:34 -08:00
Jon Staab 997b223e95 Rename space menu components 2025-11-13 10:36:28 -08:00
Jon Staab ba52a97e26 Tweak relay icon size in nav 2025-11-13 10:34:02 -08:00
Jon Staab cc4c7b5fe9 Fix image modal, only show + room if the user is allowed 2025-11-13 10:32:37 -08:00
Jon Staab 8e2ebd11fc remove some alts 2025-11-13 08:59:32 -08:00
Jon Staab 9cae4da9f4 Add lightning invoice payments 2025-11-12 16:24:58 -08:00
Jon Staab c05d7e99e2 remove old icon picker 2025-11-12 14:56:14 -08:00
Jon Staab 2390599e8f Fix relay updating and relay icons 2025-11-11 17:48:24 -08:00
Jon Staab 1a4d45fa9c Upload svgs for room icon 2025-11-11 17:34:15 -08:00
Jon Staab 57447e5bf4 Bump version 2025-11-11 14:09:05 -08:00
Jon Staab 8e411daaef Refactor avatar components, add space edit form 2025-11-11 13:50:45 -08:00
Jon Staab 183aebf841 Improve room syncing 2025-11-10 16:19:50 -08:00
Jon Staab e3e500ccc2 Return better blossom errors 2025-11-10 16:02:02 -08:00
Jon Staab e7a2535ece Fix access restricted after successful invite code 2025-11-10 15:24:11 -08:00
Jon Staab 761e369313 Add room detail, assume admins are members 2025-11-10 14:59:15 -08:00
Jon Staab 5248275d73 Fix nav index 2025-11-10 13:20:42 -08:00
Jon Staab cb033279dd Fix link 2025-11-06 11:25:07 -08:00
Jon Staab 41d50d8c28 Add room policy indicator 2025-11-05 16:59:17 -08:00
Jon Staab a52c2b4c3c Lighten up shadows 2025-11-05 15:32:55 -08:00
Jon Staab b5917cb184 Show loading on spaces menu 2025-11-05 15:24:46 -08:00
Jon Staab 57348472f8 Always join spaces when visiting them 2025-11-05 15:09:23 -08:00
Jon Staab 4b6223dc00 Update changelog 2025-11-05 09:46:05 -08:00
Jon Staab 5525e45a15 Bump version, upgrade welshman 2025-11-05 09:42:27 -08:00
Jon Staab 80a2ae60b0 Bump version 2025-11-04 17:28:27 -08:00
Jon Staab d7e95f5d2f Fix chat url 2025-11-04 17:25:50 -08:00
Jon Staab ca4e5ae5ee Add shadow to thread items etc, bump welshman, update changelog, update version 2025-11-04 17:14:33 -08:00
Jon Staab b673658c0c Handle escape in chat 2025-11-04 16:59:17 -08:00
Jon Staab 5c5c130700 Add landlubber link if user is admin 2025-11-04 16:55:26 -08:00
Jon Staab 2d89ca6c0e Support invite links on discover page 2025-11-04 16:39:34 -08:00
Jon Staab 806a7c2609 Persist alert kinds again 2025-11-04 16:25:21 -08:00
Jon Staab 501ce8067d Detect nip29 properly before choosing smart path, more robust auth error checking 2025-11-04 16:14:32 -08:00
Jon Staab 6429f82829 Improve claim/access detection 2025-11-04 15:36:20 -08:00
Jon Staab fe626218ea Ignore aborted signatures when checking auth 2025-11-04 09:34:07 -08:00
Jon Staab b62b1bc063 Don't source local .env file on build 2025-11-04 09:18:26 -08:00
Jon Staab d980f36246 Use request instead of load to avoid timeouts 2025-11-04 09:05:17 -08:00
Jon Staab b469addd29 Remove withGetter 2025-11-03 14:52:12 -08:00
Jon Staab 6923c2a8b7 Tweak modal, reduce storage on mobile 2025-11-03 14:43:27 -08:00
Jon Staab 1d3f32fb99 Only return error from attemptRelayAccess if there is a claim sent 2025-11-03 12:08:50 -08:00
Jon Staab 42a550788a Fix some alerts stuff 2025-11-03 11:10:16 -08:00
Jon Staab b1c68972c9 Streamline deriveRoom 2025-10-31 16:19:22 -07:00
Jon Staab 3978e32d5f Tweak access terminology, relay access attempts 2025-10-31 16:00:14 -07:00
Jon Staab ba2b5d182e Fix alerts 2025-10-31 14:51:59 -07:00
Jon Staab bef04fa899 Add holis to hosting suggestions 2025-10-31 14:02:52 -07:00
Jon Staab 4f8609421c Fix membership status 2025-10-31 12:10:16 -07:00
Jon Staab 07660c9d44 Re-work rooms derivation 2025-10-30 15:52:24 -07:00
Jon Staab a324dad2ba Rename channel to room 2025-10-30 15:36:14 -07:00
Jon Staab dbaa0f5d49 Rename room variables to h 2025-10-30 15:33:34 -07:00
Jon Staab 478721d349 Add room editing 2025-10-30 15:22:31 -07:00
Jon Staab a669a23dbc Tweak reaction buttons 2025-10-30 12:53:21 -07:00
Jon Staab cfeb6478cc Fix flapping subscription 2025-10-30 12:06:53 -07:00
Jon Staab 64539c49c1 Fix link, spinner animation 2025-10-30 07:20:09 -07:00
Jon Staab 0399ae37ec Move space create to its own page 2025-10-29 12:52:26 -07:00
Jon Staab 173a411a36 Update space create dialog 2025-10-29 11:18:27 -07:00
Jon Staab 62013a2ea2 Tweak mobile space menu 2025-10-28 16:50:15 -07:00
Jon Staab c82cf4a4c2 Update platform url 2025-10-28 16:08:48 -07:00
177 changed files with 4603 additions and 3975 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3 VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/ VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
VITE_BURROW_URL= VITE_BURROW_URL=
VITE_PLATFORM_URL=https://flotilla.social VITE_PLATFORM_URL=https://app.flotilla.social
VITE_PLATFORM_TERMS=https://flotilla.social/terms VITE_PLATFORM_TERMS=https://flotilla.social/terms
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
VITE_PLATFORM_NAME=Flotilla VITE_PLATFORM_NAME=Flotilla
+64
View File
@@ -1,5 +1,69 @@
# Changelog # Changelog
# 1.6.2
* Fix modal scrolling and style
# 1.6.1
* Fix skinny profile images
* Custom handler for relay urls
* Improve time based chat partitioning
* Improve authenticated image access interop
* Fix image detail dialog
* Fix zapper loading
* Fix recent events missing in feeds
# 1.6.0
* Switch back to indexeddb to fix memory and performance
* Add pay invoice functionality
* Add space membership management and bans
* Add event info to profile dialog
* Add better room membership management
* Refactor stores for performance
* Hide nav when keyboard is open
* Handle flotilla links in-app
* Fix new messages indicator z-index
* Fix some display bugs
* Add date to chat items
* Refine data synchronization
* Hide nav when keyboard is open on mobile
# 1.5.3
* Add space edit form
* Improve room syncing
* Return better blossom errors
* Fix access restricted bugs
* Add room detail dialog
* Fix broken link to self hosting
* Tweak shadows
* Always join spaces when visiting them
# 1.5.2
* Fix negentropy room syncing
# 1.5.1
* Fix chat path link
# 1.5.0
* Restyle mobile dialogs
* Add room membership lists
* Add space membership lists
* Add edit room form
* Support closed/private/restricted/hidden rooms
* Add hosting services page
* Improve performance and UI
* Fix push notifications
* Improve error detection and handling
* Support invite links on discover page
* Add link to landlubber if user is admin
* Clear reply/share/edit on escape
# 1.4.1 # 1.4.1
* Improve data synchronization * Improve data synchronization
+34
View File
@@ -0,0 +1,34 @@
# Flotilla - AI Assistant Context
## Project Overview
Flotilla is a Discord-like Nostr client based on the concept of "relays as groups". It's built with SvelteKit, TypeScript, and Capacitor for cross-platform support (web, Android, iOS).
On boot, please run `tree -I assets src` to get an idea of the project structure.
## Key Dependencies
`@welshman/*` libraries contain the majority of nostr-related functionality.
`@app/core/*` contains additional app-specific data stores and commands.
When creating an import statement, first identify what functionality you need. Search the codebase for components with similar functionality, and imitate their imports.
## Dependency Graph (Acyclic)
The project follows a strict dependency hierarchy:
1. **External libraries** (bottom layer)
2. **`lib/`** - Only depends on external libraries
3. **`app/core/`** and **`app/util/`** - Can depend on `lib` only
4. **`app/components/`** - Can depend on anything in `app` or `lib`
5. **`routes/`** - Can depend on anything (top layer)
**Import Ordering Convention:** Always sort imports by dependency level:
1. Third-party libraries first
2. Then `lib` imports
3. Then `app` imports
## Development Conventions
When creating components related to a given space or room, parameterize them only with the entity's identifier (i.e., `url` and `h`). Only pass additional props if they can't be derived from the identifiers. For example, a room's `members` should be derived inside the child component, not passed in by the parent.
Do not use null, only undefined.
+1 -1
View File
@@ -69,7 +69,7 @@ Here are a few important domain objects:
- Spaces are relays used as community groups. Their `url`s are core to a lot of data and components, and are frequently passed around from place to place. - Spaces are relays used as community groups. Their `url`s are core to a lot of data and components, and are frequently passed around from place to place.
- Chats are direct message conversations. There is currently some ambiguity in routing, since relays that don't support NIP 29 also have a "chat" tab, which uses vanilla NIP-C7. - Chats are direct message conversations. There is currently some ambiguity in routing, since relays that don't support NIP 29 also have a "chat" tab, which uses vanilla NIP-C7.
- NIP 29 groups are variously called "rooms" and "channels". Conventionally, a "room" is a group id, while a "channel" as an object representing the group's metadata. - NIP 29 groups are called "rooms". Conventionally, "h" is a group id, while a "room" as an object representing the group's metadata.
- "Alerts" are records of requests the user has made to be notified, following [this NIP](https://github.com/nostr-protocol/nips/pull/1796) - "Alerts" are records of requests the user has made to be notified, following [this NIP](https://github.com/nostr-protocol/nips/pull/1796)
`app/core/requests` contains utilities related to loading data from the nostr network. This might include feed manager utilities, loaders, or listeners. `app/core/requests` contains utilities related to loading data from the nostr network. This might include feed manager utilities, loaders, or listeners.
+2 -2
View File
@@ -8,8 +8,8 @@ If you would like to be interoperable with Flotilla, please check out this guide
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples): You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust. - `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
- `VITE_PLATFORM_URL` - The url where the app will be hosted. This is only used for build-time population of meta tags. - `VITE_PLATFORM_URL` - The url where the app will be hosted
- `VITE_PLATFORM_NAME` - The name of the app - `VITE_PLATFORM_NAME` - The name of the app
- `VITE_PLATFORM_LOGO` - A logo url for the app - `VITE_PLATFORM_LOGO` - A logo url for the app
- `VITE_PLATFORM_RELAYS` - A list of comma-separated relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page. - `VITE_PLATFORM_RELAYS` - A list of comma-separated relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla" applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion targetSdk rootProject.ext.targetSdkVersion
versionCode 30 versionCode 38
versionName "1.4.1" versionName "1.6.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
-4
View File
@@ -6,10 +6,6 @@ if [ -f .env.template ]; then
source .env.template source .env.template
fi fi
if [ -f .env ]; then
source .env
fi
# Avoid overwriting env vars provided directly # Avoid overwriting env vars provided directly
# https://stackoverflow.com/a/69127685/1467342 # https://stackoverflow.com/a/69127685/1467342
eval "$temp_env" eval "$temp_env"
+4 -4
View File
@@ -358,14 +358,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements"; CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 21; CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.4.1; MARKETING_VERSION = 1.6.2;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -384,14 +384,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements"; CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 21; CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.4.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
+12 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "1.4.1", "version": "1.6.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -51,6 +51,7 @@
"@capacitor/push-notifications": "^7.0.3", "@capacitor/push-notifications": "^7.0.3",
"@capawesome/capacitor-android-dark-mode-support": "^7.0.0", "@capawesome/capacitor-android-dark-mode-support": "^7.0.0",
"@capawesome/capacitor-badge": "^7.0.1", "@capawesome/capacitor-badge": "^7.0.1",
"@getalby/lightning-tools": "^6.0.0",
"@getalby/sdk": "^5.1.2", "@getalby/sdk": "^5.1.2",
"@poppanator/sveltekit-svg": "^4.2.1", "@poppanator/sveltekit-svg": "^4.2.1",
"@sentry/browser": "^8.55.0", "@sentry/browser": "^8.55.0",
@@ -60,16 +61,16 @@
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8", "@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.6.3", "@welshman/app": "^0.7.1",
"@welshman/content": "^0.6.3", "@welshman/content": "^0.7.1",
"@welshman/editor": "^0.6.3", "@welshman/editor": "^0.7.1",
"@welshman/feeds": "^0.6.3", "@welshman/feeds": "^0.7.1",
"@welshman/lib": "^0.6.3", "@welshman/lib": "^0.7.1",
"@welshman/net": "^0.6.3", "@welshman/net": "^0.7.1",
"@welshman/router": "^0.6.3", "@welshman/router": "^0.7.1",
"@welshman/signer": "^0.6.3", "@welshman/signer": "^0.7.1",
"@welshman/store": "^0.6.3", "@welshman/store": "^0.7.1",
"@welshman/util": "^0.6.3", "@welshman/util": "^0.7.1",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"daisyui": "^4.12.24", "daisyui": "^4.12.24",
"date-picker-svelte": "^2.16.0", "date-picker-svelte": "^2.16.0",
+198 -134
View File
@@ -44,6 +44,9 @@ importers:
'@capawesome/capacitor-badge': '@capawesome/capacitor-badge':
specifier: ^7.0.1 specifier: ^7.0.1
version: 7.0.1(@capacitor/core@7.4.3) version: 7.0.1(@capacitor/core@7.4.3)
'@getalby/lightning-tools':
specifier: ^6.0.0
version: 6.0.0
'@getalby/sdk': '@getalby/sdk':
specifier: ^5.1.2 specifier: ^5.1.2
version: 5.1.2(typescript@5.9.3) version: 5.1.2(typescript@5.9.3)
@@ -72,35 +75,35 @@ importers:
specifier: ^0.6.8 specifier: ^0.6.8
version: 0.6.8(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0)) version: 0.6.8(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
'@welshman/app': '@welshman/app':
specifier: ^0.6.3 specifier: ^0.7.1
version: 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) version: 0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/content': '@welshman/content':
specifier: ^0.6.3 specifier: ^0.7.1
version: 0.6.3(typescript@5.9.3) version: 0.7.1(typescript@5.9.3)
'@welshman/editor': '@welshman/editor':
specifier: ^0.6.3 specifier: ^0.7.1
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) version: 0.7.1(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)
'@welshman/feeds': '@welshman/feeds':
specifier: ^0.6.3 specifier: ^0.7.1
version: 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) version: 0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/lib': '@welshman/lib':
specifier: ^0.6.3 specifier: ^0.7.1
version: 0.6.3 version: 0.7.1
'@welshman/net': '@welshman/net':
specifier: ^0.6.3 specifier: ^0.7.1
version: 0.6.3(typescript@5.9.3)(ws@8.18.3) version: 0.7.1(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': '@welshman/router':
specifier: ^0.6.3 specifier: ^0.7.1
version: 0.6.3(typescript@5.9.3)(ws@8.18.3) version: 0.7.1(typescript@5.9.3)(ws@8.18.3)
'@welshman/signer': '@welshman/signer':
specifier: ^0.6.3 specifier: ^0.7.1
version: 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) version: 0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
'@welshman/store': '@welshman/store':
specifier: ^0.6.3 specifier: ^0.7.1
version: 0.6.3(typescript@5.9.3)(ws@8.18.3) version: 0.7.1(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': '@welshman/util':
specifier: ^0.6.3 specifier: ^0.7.1
version: 0.6.3(typescript@5.9.3) version: 0.7.1(typescript@5.9.3)
compressorjs: compressorjs:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
@@ -986,6 +989,10 @@ packages:
resolution: {integrity: sha512-dxOmJLJAh6qJ8rsbA5/Bwj7MSI9X3RkxxqmCedl5rfP+yKwNSdfu8i4EiCZN/tk2hNBJb8GHSCcPRNZfwfmEHg==} resolution: {integrity: sha512-dxOmJLJAh6qJ8rsbA5/Bwj7MSI9X3RkxxqmCedl5rfP+yKwNSdfu8i4EiCZN/tk2hNBJb8GHSCcPRNZfwfmEHg==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@getalby/lightning-tools@6.0.0':
resolution: {integrity: sha512-jpTO+7o1N1KhV5qT6qetPK+et6ZQshCzUMCRV8+Ek1NVlVU4ITIqOWRQ3kOrb0PhSxkbGN5G3d60HCi535hbDw==}
engines: {node: '>=14'}
'@getalby/sdk@5.1.2': '@getalby/sdk@5.1.2':
resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==} resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -1092,6 +1099,15 @@ packages:
'@jridgewell/trace-mapping@0.3.9': '@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@jsr/fiatjaf__promenade-trusted-dealer@0.4.1':
resolution: {integrity: sha512-K9WjpDkQGyLl5gUZBLr3Gb+b5b1r8miZmDOo4+ZlzGQgoXD2TaqT+dkBjL/yLj/pYwBcd1Bschv0xuNpguL2ZQ==, tarball: https://npm.jsr.io/~/11/@jsr/fiatjaf__promenade-trusted-dealer/0.4.1.tgz}
'@jsr/henrygd__semaphore@0.0.2':
resolution: {integrity: sha512-nrwZ8HaqU1Agb2ij8omIxaOCAsKkDHWcwS9hTRumPhZuptwh6/0BJExBd8+eClTYM7jBnZxK+cP4WJRTcHBvCA==, tarball: https://npm.jsr.io/~/11/@jsr/henrygd__semaphore/0.0.2.tgz}
'@jsr/nostr__tools@2.16.2':
resolution: {integrity: sha512-QK1XwHvAnqEwbimD+ywbLQ3T2iI+/qE/zrRgOhmtjoEGlCWgtbPTNJ6Y/MEunXr6H/MnuHV+s400i/Yk4suvGQ==, tarball: https://npm.jsr.io/~/11/@jsr/nostr__tools/2.16.2.tgz}
'@noble/ciphers@0.5.3': '@noble/ciphers@0.5.3':
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==} resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
@@ -1117,6 +1133,10 @@ packages:
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16} engines: {node: ^14.21.3 || >=16}
'@noble/hashes@2.0.1':
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
engines: {node: '>= 20.19.0'}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1451,77 +1471,77 @@ packages:
peerDependencies: peerDependencies:
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-code-block@2.26.3': '@tiptap/extension-code-block@2.27.1':
resolution: {integrity: sha512-3DbzKRfMqw9EGS7mGkpyopbRWTO+qpV52Mby4Ll2+OfhvGnHzSN4Q7xOsp+VeZr14GMEmua5Oq2e/gRypqXatQ==} resolution: {integrity: sha512-wCI5VIOfSAdkenCWFvh4m8FFCJ51EOK+CUmOC/PWUjyo2Dgn8QC8HMi015q8XF7886T0KvYVVoqxmxJSUDAYNg==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-code@2.26.3': '@tiptap/extension-code@2.27.1':
resolution: {integrity: sha512-bAkUNzV+tA1J1RYbtbAGTFqkRw9+yRpAd+d3S9jy/dAD+uOe1ZD1EIngyEf2GTonnoy4bpDYtytbCjUt9PozoA==} resolution: {integrity: sha512-i65wUGJevzBTIIUBHBc1ggVa27bgemvGl/tY1/89fEuS/0Xmre+OQjw8rCtSLevoHSiYYLgLRlvjtUSUhE4kgg==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-document@2.26.3': '@tiptap/extension-document@2.27.1':
resolution: {integrity: sha512-gcJg4Otchilr4eSUwhPNwbhPUkEYvXhkUZ/1MAhVGD40Ovq2P8ZWkJipA3tKOCJinL5MJK59ccZBstnKSTw+JA==} resolution: {integrity: sha512-NtJzJY7Q/6XWjpOm5OXKrnEaofrcc1XOTYlo/SaTwl8k2bZo918Vl0IDBWhPVDsUN7kx767uHwbtuQZ+9I82hA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-dropcursor@2.26.3': '@tiptap/extension-dropcursor@2.27.1':
resolution: {integrity: sha512-54rgDTmRStVmXZR7KdCvSOCAbumh5luXgticUkRM8OM8PBe1c0T9X8jfV7+XEFGugRVl8mtCZZpgUt5vhuxHog==} resolution: {integrity: sha512-3MBQRGHHZ0by3OT0CWbLKS7J3PH9PpobrXjmIR7kr0nde7+bHqxXiVNuuIf501oKU9rnEUSedipSHkLYGkmfsA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-gapcursor@2.26.3': '@tiptap/extension-gapcursor@2.27.1':
resolution: {integrity: sha512-ZDNSkpz7ik2PJOjrys27rwko5Ufe6GtLjaAxjvkWmyzcgAOTadDeth9NaRdBVMDGgSLBKbXihYZZXLkiAP9RLA==} resolution: {integrity: sha512-A9e1jr+jGhDWzNSXtIO6PYVYhf5j/udjbZwMja+wCE/3KvZU9V3IrnGKz1xNW+2Q2BDOe1QO7j5uVL9ElR6nTA==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-hard-break@2.26.3': '@tiptap/extension-hard-break@2.27.1':
resolution: {integrity: sha512-KJWUi+2KOZejVRb2KI0NM3LgCpNimxcunbOCKsZKygV/UByzhUl7UaCAIa+ySMM+kbu/Ec3hkTzafGfaU9ZkLg==} resolution: {integrity: sha512-W4hHa4Io6QCTwpyTlN6UAvqMIQ7t56kIUByZhyY9EWrg/+JpbfpxE1kXFLPB4ZGgwBknFOw+e4bJ1j3oAbTJFw==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-history@2.26.3': '@tiptap/extension-history@2.27.1':
resolution: {integrity: sha512-Qg4+WWf/hDgiBspxLbrhrIFUy7lzi2eBKPSoF/haEYFw/t/FeN60NXYYYtpLimUNpUzyJSOSIwsngFcVJO5X+g==} resolution: {integrity: sha512-K8PHC9gegSAt0wzSlsd4aUpoEyIJYOmVVeyniHr1P1mIblW1KYEDbRGbDlrLALTyUEfMcBhdIm8zrB9X2Nihvg==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-image@2.26.3': '@tiptap/extension-image@2.27.1':
resolution: {integrity: sha512-juAAY1QuzCgfl66Q8AHITLVKbwXpv+BmLNCi8Cl4j6a+IkySzcS8gENJee0hMMyRvc9K1U75o4vokvy580u4kA==} resolution: {integrity: sha512-wu3vMKDYWJwKS6Hrw5PPCKBO2RxyHNeFLiA/uDErEV7axzNpievK/U9DyaDXmtK3K/h1XzJAJz19X+2d/pY68w==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-link@2.26.3': '@tiptap/extension-link@2.27.1':
resolution: {integrity: sha512-cNYqAeiaG/65ctVEUOHt1MQnTF1JcdZqBkN9pLf3grzcmkmdr3w1/JbKOphZc84vOB2rxuhGZx9NFV2lrC5Qwg==} resolution: {integrity: sha512-cCwWPZsnVh9MXnGOqSIRXPPuUixRDK8eMN2TvqwbxUBb1TU7b/HtNvfMU4tAOqAuMRJ0aJkFuf3eB0Gi8LVb1g==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-paragraph@2.26.3': '@tiptap/extension-paragraph@2.27.1':
resolution: {integrity: sha512-eBC5UsaTJRUMhePtK1dcCAfes0CpqqFiewpIM0lWk4XMtpG2aoczVVVkImybbFKfqsvEEo3vgHJ2YiE5YZFCSg==} resolution: {integrity: sha512-R3QdrHcUdFAsdsn2UAIvhY0yWyHjqGyP/Rv8RRdN0OyFiTKtwTPqreKMHKJOflgX4sMJl/OpHTpNG1Kaf7Lo2A==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-placeholder@2.26.3': '@tiptap/extension-placeholder@2.27.1':
resolution: {integrity: sha512-HDF4FZj8CmQQvbSyXb/G+Ujqoue7TMQPMAe1h1OMJAXq856Y0AsVLXYKiBojUTfI11I7zVwYe08D8atIXHLZZw==} resolution: {integrity: sha512-UbXaibHHFE+lOTlw/vs3jPzBoj1sAfbXuTAhXChjgYIcTTY5Cr6yxwcymLcimbQ79gf04Xkua2FCN3YsJxIFmw==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
'@tiptap/extension-text@2.26.3': '@tiptap/extension-text@2.27.1':
resolution: {integrity: sha512-sGRbX96ss4jQeKw9d0iphuAWja8Dv4w4ryTDKfxD7Lizx3UaIxQB/y+Wna89tM3kfbi/qJcrD3AF7NJgfc/tEA==} resolution: {integrity: sha512-a4GCT+GZ9tUwl82F4CEum9/+WsuW0/De9Be/NqrMmi7eNfAwbUTbLCTFU0gEvv25WMHCoUzaeNk/qGmzeVPJ1Q==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm@2.26.3': '@tiptap/pm@2.26.3':
resolution: {integrity: sha512-8gUmdxWlUevmgq2mNvGxvf2CpDW097tVKECMWKEn8sf846kXv3CoqaGRhI3db4kfR+09uWZeRM7rtrjRBmUThg==} resolution: {integrity: sha512-8gUmdxWlUevmgq2mNvGxvf2CpDW097tVKECMWKEn8sf846kXv3CoqaGRhI3db4kfR+09uWZeRM7rtrjRBmUThg==}
'@tiptap/suggestion@2.26.3': '@tiptap/suggestion@2.27.1':
resolution: {integrity: sha512-kcyiyKEEDnqFImGQQEEuRa6N/N+/vU/OrI99wRfJnDnN8c3dP6UHJ4wr2qX6bUpx3Z0QTu6GGCpMpaqwtHTtJg==} resolution: {integrity: sha512-yTy75ZMYgVWM18cl7YxLqMJ7TorQTGysSd1aKmBA9qd8uzYlvLMmHKE9qBDxM9HXODBz1DA/BLLm9esv2enmFw==}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0 '@tiptap/pm': ^2.7.0
@@ -1692,38 +1712,38 @@ packages:
'@vite-pwa/assets-generator': '@vite-pwa/assets-generator':
optional: true optional: true
'@welshman/app@0.6.3': '@welshman/app@0.7.1':
resolution: {integrity: sha512-iCZj0b3D5Q7rSEUiON+nJEKIdYG3j6MdK3oSyFiiQQxa+TPArjEbWF5kkogp1J1GMGo7gzQ6owByPgDIdlNiZg==} resolution: {integrity: sha512-gHXuUVplKEtV2J7BDXxz9r6Gv9PwIfhXFEhjOraPW9/BEYS1zK0KneCe87jwZe5B/zmMk3dwMhkaUx4H3WphIA==}
'@welshman/content@0.6.3': '@welshman/content@0.7.1':
resolution: {integrity: sha512-VvlW2kJ/lB6sy/Upa0nbKbO6rMwTO9Bi2iGXNiR9XFA8rlMUJc7Wl1Dcd3QZD3QHBTeTICOuFB4xopwQK3W0JQ==} resolution: {integrity: sha512-AHSwpodzQ9zjgbKy7CRIoQg7Irni8PUNyqlvcj4RYbY19bgaGcSoozwjbDat0wY4ULBnVsX1y2DE3+rm5R0T2A==}
'@welshman/editor@0.6.3': '@welshman/editor@0.7.1':
resolution: {integrity: sha512-oLOhUUYO+4vhOehEpVhmsGMhmiBsJa/jgSsVRuCpZRKi+jwrJsaC9J7AyUzbQmcA8e07oNsqfakDYFkbFAfUVQ==} resolution: {integrity: sha512-fsCm+W8AQbygoN2+fm1LS6xkxdanB7v5FfhQKFsa8L1B9eYEYCAhwvrxy+nZsBEK/dt8zelk7qKQwq/CJ9sppQ==}
'@welshman/feeds@0.6.3': '@welshman/feeds@0.7.1':
resolution: {integrity: sha512-Q5Dupo+C/CIm7BMZ++7BhQdjgyu3tU4aK2w172cx0gTFfSTqKxFrbWX4ewUWqq8ex1/1Xd+heVheE1q54FCVNQ==} resolution: {integrity: sha512-i9SCE1jlVIBjM1pfPVW+5axQ0BSNBmOYeo9lKdFOjeTx1sHityb/Q3kK9lgie6IDgXhK/SshEH6bKdYSnOkVSg==}
'@welshman/lib@0.6.3': '@welshman/lib@0.7.1':
resolution: {integrity: sha512-O632yJQ/1IUmb9FuC+O0TsOUl+UubYriU95LpKA9ZFAaogXLrzHZH0HZ/4xmdPj82kqBeS98EOb3GkSAOYZaLA==} resolution: {integrity: sha512-NQkxPwnAoUY4uSroQcfvR4YPG63j7Ke0R9YrLNXF9SQn2t2p6iAQ6A3GEOVu/koUQiVBseYn514lS7X1XkCP3A==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
'@welshman/net@0.6.3': '@welshman/net@0.7.1':
resolution: {integrity: sha512-RlCYFFau6p2RZq3K4XMYhD7xvNFYjA6zpzHuM8iV4Xh8nL5b8tn6e6f6a4I2E5uZMfYdWstvcC6OhU/Z4NQZoA==} resolution: {integrity: sha512-S3dFH73Cy4phLy5I2KKEeefkRmNBYWB2qONK8txUVDhx1u7ezpALzZEMSPVqVIZk/vCQU3KJ0CyagvbuGF+F9Q==}
'@welshman/router@0.6.3': '@welshman/router@0.7.1':
resolution: {integrity: sha512-lZaw166aJKhftakMGofzmFmtAqfQBGSur58/ANgLCwVy4b5riqempcPoH6EOgwqANye5YUlsUisxbx0r/2wXPA==} resolution: {integrity: sha512-PZnbGHtbnVbsY+b+FqQHNlyY2+MrEAJ3arFiO3fouayb/sWHdBfSd9EL5UM1FQd1q0fjoZIncTmffRcvQfeBqQ==}
'@welshman/signer@0.6.3': '@welshman/signer@0.7.1':
resolution: {integrity: sha512-O0cd5nV77Lt4vWRvb50l6U1wkhQF6LW3x+N4tspqWqo9BeIHTDlUtZMdsnhwfTjeVlLocSVkUValxhoxxt20SA==} resolution: {integrity: sha512-/WNEgXZemQ36A07lmrEy78Yn7kEngBjySmXW+xYmHc3OLhQ9XEq3FBCTR+vxsmp1w/t+7IEScPTKn/wvAQ/cSw==}
peerDependencies: peerDependencies:
nostr-signer-capacitor-plugin: ~0.0.4 nostr-signer-capacitor-plugin: ~0.0.4
'@welshman/store@0.6.3': '@welshman/store@0.7.1':
resolution: {integrity: sha512-3TMon00CF1l/LcWq+B3pq/FNs2Ie4Y9EdjiAcg4dvDeZPI9h1HoQPB/s2RcaG4FYPhkynvM5zL/eH+5GIe9kJw==} resolution: {integrity: sha512-EE+vlMdUeVgQhzJqzhAkbLnnOL22gXW8afJzR377n+CvHABLV7/zY9aW0Hmgm1RnyI7fSfWF2YEa6l6VP8x4pw==}
'@welshman/util@0.6.3': '@welshman/util@0.7.1':
resolution: {integrity: sha512-ki2j3ADmxqI0rbdnkq0wS7PFXTY0dZ24yOgceyTizwF/dtcRKmH7YyvM+2YwgbgaUO6BtqKjktc3jrh6XaxOQg==} resolution: {integrity: sha512-UGryq1jfwRHFS7mjGa4fmuqN851iwKeR+616LmUpTJQHAfhGU7ifer2+JLdDLYBU/neI5iKHdRDO5hg92U6k8Q==}
'@xml-tools/parser@1.0.11': '@xml-tools/parser@1.0.11':
resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==} resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==}
@@ -3489,8 +3509,8 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
nostr-editor@1.0.1: nostr-editor@1.0.2:
resolution: {integrity: sha512-HXqXjxtIN0CcC7sLV5xYjEsQF0bFYLmNKxS75ya2yZGQ/z16U+uK6bb2Hd72QyqXlHXyWN0m24E5Gcws8/NhRQ==} resolution: {integrity: sha512-z1XfVH0cDsDBvIfsNfIjjD1MI+ugChMbJToNIlKXi6aMkm8KgZOkHl9nkKdkAfZXU5yk+DPTEvv433NPZp2yKA==}
engines: {node: '>=18.16.1'} engines: {node: '>=18.16.1'}
peerDependencies: peerDependencies:
'@tiptap/core': ^2.6.6 '@tiptap/core': ^2.6.6
@@ -3526,6 +3546,14 @@ packages:
typescript: typescript:
optional: true optional: true
nostr-tools@2.19.1:
resolution: {integrity: sha512-iEHSzRxD1gCMohtna5Jx6Cm90gGK4mrJD2+2VYMu346/EucSlz9gsUFubQ3B7f3SMsnQnh1Srm5nCcPfy2NsNw==}
peerDependencies:
typescript: '>=5.0.0'
peerDependenciesMeta:
typescript:
optional: true
nostr-wasm@0.1.0: nostr-wasm@0.1.0:
resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==}
@@ -5839,6 +5867,8 @@ snapshots:
'@getalby/lightning-tools@5.2.1': {} '@getalby/lightning-tools@5.2.1': {}
'@getalby/lightning-tools@6.0.0': {}
'@getalby/sdk@5.1.2(typescript@5.9.3)': '@getalby/sdk@5.1.2(typescript@5.9.3)':
dependencies: dependencies:
'@getalby/lightning-tools': 5.2.1 '@getalby/lightning-tools': 5.2.1
@@ -6024,6 +6054,24 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@jsr/fiatjaf__promenade-trusted-dealer@0.4.1':
dependencies:
'@jsr/henrygd__semaphore': 0.0.2
'@jsr/nostr__tools': 2.16.2
'@noble/curves': 1.9.7
'@jsr/henrygd__semaphore@0.0.2': {}
'@jsr/nostr__tools@2.16.2':
dependencies:
'@noble/ciphers': 0.5.3
'@noble/curves': 1.2.0
'@noble/hashes': 1.3.1
'@scure/base': 1.1.1
'@scure/bip32': 1.3.1
'@scure/bip39': 1.2.1
nostr-wasm: 0.1.0
'@noble/ciphers@0.5.3': {} '@noble/ciphers@0.5.3': {}
'@noble/curves@1.1.0': '@noble/curves@1.1.0':
@@ -6044,6 +6092,8 @@ snapshots:
'@noble/hashes@1.8.0': {} '@noble/hashes@1.8.0': {}
'@noble/hashes@2.0.1': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -6346,58 +6396,58 @@ snapshots:
dependencies: dependencies:
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
'@tiptap/extension-code-block@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': '@tiptap/extension-code-block@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
'@tiptap/extension-code@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': '@tiptap/extension-code@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/extension-document@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': '@tiptap/extension-document@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/extension-dropcursor@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': '@tiptap/extension-dropcursor@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
'@tiptap/extension-gapcursor@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': '@tiptap/extension-gapcursor@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
'@tiptap/extension-hard-break@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': '@tiptap/extension-hard-break@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/extension-history@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': '@tiptap/extension-history@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
'@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': '@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': '@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
linkifyjs: 4.3.2 linkifyjs: 4.3.2
'@tiptap/extension-paragraph@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': '@tiptap/extension-paragraph@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/extension-placeholder@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': '@tiptap/extension-placeholder@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
'@tiptap/extension-text@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': '@tiptap/extension-text@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
@@ -6422,7 +6472,7 @@ snapshots:
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.4
prosemirror-view: 1.41.3 prosemirror-view: 1.41.3
'@tiptap/suggestion@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': '@tiptap/suggestion@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
@@ -6651,16 +6701,16 @@ snapshots:
optionalDependencies: optionalDependencies:
'@vite-pwa/assets-generator': 0.2.6 '@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.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@types/throttle-debounce': 5.0.2 '@types/throttle-debounce': 5.0.2
'@welshman/feeds': 0.6.3(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3) '@welshman/feeds': 0.7.1(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/lib': 0.7.1
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': 0.6.3(typescript@5.9.3)(ws@8.18.3) '@welshman/router': 0.7.1(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/signer': 0.7.1(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/store': 0.7.1(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.3(typescript@5.9.3) '@welshman/util': 0.7.1(typescript@5.9.3)
fuse.js: 7.1.0 fuse.js: 7.1.0
svelte: 4.2.20 svelte: 4.2.20
throttle-debounce: 5.0.2 throttle-debounce: 5.0.2
@@ -6669,31 +6719,31 @@ snapshots:
- typescript - typescript
- ws - ws
'@welshman/content@0.6.3(typescript@5.9.3)': '@welshman/content@0.7.1(typescript@5.9.3)':
dependencies: dependencies:
'@braintree/sanitize-url': 7.1.1 '@braintree/sanitize-url': 7.1.1
nostr-tools: 2.17.0(typescript@5.9.3) nostr-tools: 2.17.0(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@welshman/editor@0.6.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.7.1(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)':
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/extension-code': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) '@tiptap/extension-code': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
'@tiptap/extension-code-block': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) '@tiptap/extension-code-block': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
'@tiptap/extension-document': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) '@tiptap/extension-document': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
'@tiptap/extension-dropcursor': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) '@tiptap/extension-dropcursor': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
'@tiptap/extension-gapcursor': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) '@tiptap/extension-gapcursor': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
'@tiptap/extension-hard-break': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) '@tiptap/extension-hard-break': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
'@tiptap/extension-history': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) '@tiptap/extension-history': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
'@tiptap/extension-paragraph': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) '@tiptap/extension-paragraph': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
'@tiptap/extension-placeholder': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) '@tiptap/extension-placeholder': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
'@tiptap/extension-text': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) '@tiptap/extension-text': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
'@tiptap/suggestion': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) '@tiptap/suggestion': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
'@welshman/lib': 0.6.3 '@welshman/lib': 0.7.1
'@welshman/util': 0.6.3(typescript@5.9.3) '@welshman/util': 0.7.1(typescript@5.9.3)
nostr-editor: 1.0.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))) nostr-editor: 1.0.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))
nostr-tools: 2.17.0(typescript@5.9.3) nostr-tools: 2.17.0(typescript@5.9.3)
tippy.js: 6.3.7 tippy.js: 6.3.7
transitivePeerDependencies: transitivePeerDependencies:
@@ -6707,71 +6757,73 @@ snapshots:
- tiptap-markdown - tiptap-markdown
- typescript - 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.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.3 '@welshman/lib': 0.7.1
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': 0.6.3(typescript@5.9.3)(ws@8.18.3) '@welshman/router': 0.7.1(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/signer': 0.7.1(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/util': 0.7.1(typescript@5.9.3)
trava: 1.2.1 trava: 1.2.1
transitivePeerDependencies: transitivePeerDependencies:
- nostr-signer-capacitor-plugin - nostr-signer-capacitor-plugin
- typescript - typescript
- ws - ws
'@welshman/lib@0.6.3': '@welshman/lib@0.7.1':
dependencies: dependencies:
'@scure/base': 1.2.6 '@scure/base': 1.2.6
'@types/events': 3.0.3 '@types/events': 3.0.3
events: 3.3.0 events: 3.3.0
'@welshman/net@0.6.3(typescript@5.9.3)(ws@8.18.3)': '@welshman/net@0.7.1(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.3 '@welshman/lib': 0.7.1
'@welshman/util': 0.6.3(typescript@5.9.3) '@welshman/util': 0.7.1(typescript@5.9.3)
events: 3.3.0 events: 3.3.0
isomorphic-ws: 5.0.0(ws@8.18.3) isomorphic-ws: 5.0.0(ws@8.18.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/router@0.6.3(typescript@5.9.3)(ws@8.18.3)': '@welshman/router@0.7.1(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.3 '@welshman/lib': 0.7.1
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.3(typescript@5.9.3) '@welshman/util': 0.7.1(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - 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.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@jsr/fiatjaf__promenade-trusted-dealer': 0.4.1
'@noble/curves': 1.9.7 '@noble/curves': 1.9.7
'@noble/hashes': 1.8.0 '@noble/hashes': 2.0.1
'@welshman/lib': 0.6.3 '@welshman/lib': 0.7.1
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.3(typescript@5.9.3) '@welshman/util': 0.7.1(typescript@5.9.3)
nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.4.3) nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.4.3)
nostr-tools: 2.17.0(typescript@5.9.3) nostr-tools: 2.19.1(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/store@0.6.3(typescript@5.9.3)(ws@8.18.3)': '@welshman/store@0.7.1(typescript@5.9.3)(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.6.3 '@welshman/lib': 0.7.1
'@welshman/net': 0.6.3(typescript@5.9.3)(ws@8.18.3) '@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.3(typescript@5.9.3) '@welshman/util': 0.7.1(typescript@5.9.3)
svelte: 4.2.20 svelte: 4.2.20
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
- ws - ws
'@welshman/util@0.6.3(typescript@5.9.3)': '@welshman/util@0.7.1(typescript@5.9.3)':
dependencies: dependencies:
'@noble/curves': 1.9.7
'@types/ws': 8.18.1 '@types/ws': 8.18.1
'@welshman/lib': 0.6.3 '@welshman/lib': 0.7.1
js-base64: 3.7.8 js-base64: 3.7.8
nostr-tools: 2.17.0(typescript@5.9.3) nostr-tools: 2.17.0(typescript@5.9.3)
nostr-wasm: 0.1.0 nostr-wasm: 0.1.0
@@ -8625,11 +8677,11 @@ snapshots:
normalize-range@0.1.2: {} normalize-range@0.1.2: {}
nostr-editor@1.0.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))): nostr-editor@1.0.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))):
dependencies: dependencies:
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
'@tiptap/extension-image': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) '@tiptap/extension-image': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
'@tiptap/extension-link': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) '@tiptap/extension-link': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
'@tiptap/pm': 2.26.3 '@tiptap/pm': 2.26.3
js-base64: 3.7.8 js-base64: 3.7.8
light-bolt11-decoder: 3.2.0 light-bolt11-decoder: 3.2.0
@@ -8669,6 +8721,18 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.9.3 typescript: 5.9.3
nostr-tools@2.19.1(typescript@5.9.3):
dependencies:
'@noble/ciphers': 0.5.3
'@noble/curves': 1.2.0
'@noble/hashes': 1.3.1
'@scure/base': 1.1.1
'@scure/bip32': 1.3.1
'@scure/bip39': 1.2.1
nostr-wasm: 0.1.0
optionalDependencies:
typescript: 5.9.3
nostr-wasm@0.1.0: {} nostr-wasm@0.1.0: {}
nth-check@2.1.1: nth-check@2.1.1:
+11 -1
View File
@@ -392,10 +392,20 @@ progress[value]::-webkit-progress-value {
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)]; @apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
} }
/* Keyboard open state adjustments */
body.keyboard-open .cb {
@apply bottom-sai;
}
body.keyboard-open .hide-on-keyboard {
display: none;
}
/* chat view */ /* chat view */
.chat__compose { .chat__compose {
@apply cb cw fixed; @apply cb cw fixed z-compose;
} }
.chat__scroll-down { .chat__scroll-down {
+6 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib" import {randomInt, map, displayList, identity, TIMEZONE} from "@welshman/lib"
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util" import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
import type {Filter} from "@welshman/util" import type {Filter} from "@welshman/util"
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds" import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
@@ -13,7 +13,7 @@
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {alerts, userSpaceUrls} from "@app/core/state" import {alertsById, userSpaceUrls} from "@app/core/state"
import {requestRelayClaim} from "@app/core/requests" import {requestRelayClaim} from "@app/core/requests"
import {createAlert} from "@app/core/commands" import {createAlert} from "@app/core/commands"
import {canSendPushNotifications} from "@app/util/push" import {canSendPushNotifications} from "@app/util/push"
@@ -37,7 +37,7 @@
hideSpaceField = false, hideSpaceField = false,
}: Props = $props() }: Props = $props()
const timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100 const timezoneOffset = parseInt(TIMEZONE.split(":")?.[0] || "00")
const minute = randomInt(0, 59) const minute = randomInt(0, 59)
const hour = (17 - timezoneOffset) % 24 const hour = (17 - timezoneOffset) % 24
const WEEKLY = `0 ${minute} ${hour} * * 1` const WEEKLY = `0 ${minute} ${hour} * * 1`
@@ -45,7 +45,9 @@
let loading = $state(false) let loading = $state(false)
let cron = $state(WEEKLY) let cron = $state(WEEKLY)
let email = $state($alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "") let email = $state(
map(a => getTagValue("email", a.tags), $alertsById.values()).filter(identity)[0] || "",
)
const back = () => history.back() const back = () => history.back()
+9 -9
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {sleep} from "@welshman/lib" import {sleep, filter} from "@welshman/lib"
import {getTagValue, getAddress} from "@welshman/util" import {getTagValue, getAddress, RelayMode} from "@welshman/util"
import {isRelayFeed, findFeed} from "@welshman/feeds" import {isRelayFeed, findFeed} from "@welshman/feeds"
import {getPubkeyRelays, pubkey} from "@welshman/app"
import Inbox from "@assets/icons/inbox.svg?dataurl" import Inbox from "@assets/icons/inbox.svg?dataurl"
import Bell from "@assets/icons/bell.svg?dataurl" import Bell from "@assets/icons/bell.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl" import AddCircle from "@assets/icons/add-circle.svg?dataurl"
@@ -12,10 +13,9 @@
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import { import {
alerts,
dmAlert, dmAlert,
alertsById,
deriveAlertStatus, deriveAlertStatus,
userInboxRelays,
getAlertFeed, getAlertFeed,
userSettingsValues, userSettingsValues,
} from "@app/core/state" } from "@app/core/state"
@@ -33,7 +33,7 @@
const dmStatus = $derived($dmAlert ? deriveAlertStatus(getAddress($dmAlert.event)) : undefined) const dmStatus = $derived($dmAlert ? deriveAlertStatus(getAddress($dmAlert.event)) : undefined)
const filteredAlerts = $derived( const filteredAlerts = $derived(
$alerts.filter(alert => { filter(alert => {
const feed = getAlertFeed(alert) const feed = getAlertFeed(alert)
// Skip non-feeds and DM alerts // Skip non-feeds and DM alerts
@@ -43,7 +43,7 @@
if (url) return findFeed(feed, f => isRelayFeed(f) && f.includes(url)) if (url) return findFeed(feed, f => isRelayFeed(f) && f.includes(url))
return true return true
}), }, $alertsById.values()),
) )
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField}) const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
@@ -59,7 +59,7 @@
if ($dmAlert) { if ($dmAlert) {
deleteAlert($dmAlert) deleteAlert($dmAlert)
} else { } else {
if ($userInboxRelays.length === 0) { if (getPubkeyRelays($pubkey!, RelayMode.Messaging).length === 0) {
return uncheckDmAlert("Please set up your messaging relays before enabling alerts.") return uncheckDmAlert("Please set up your messaging relays before enabling alerts.")
} }
@@ -89,7 +89,7 @@
</script> </script>
<div class="col-4"> <div class="col-4">
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-6 shadow-md">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<strong class="flex items-center gap-3"> <strong class="flex items-center gap-3">
<Icon icon={Inbox} /> <Icon icon={Inbox} />
@@ -108,7 +108,7 @@
{/each} {/each}
</div> </div>
</div> </div>
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-4 shadow-md">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<strong class="flex items-center gap-3"> <strong class="flex items-center gap-3">
<Icon icon={Bell} /> <Icon icon={Bell} />
@@ -5,7 +5,7 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import RoomName from "@app/components/RoomName.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte" import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte" import EventActivity from "@app/components/EventActivity.svelte"
@@ -25,7 +25,7 @@
const {url, event, showRoom, showActivity}: Props = $props() const {url, event, showRoom, showActivity}: Props = $props()
const room = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
const path = makeCalendarPath(url, event.id) const path = makeCalendarPath(url, event.id)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -39,9 +39,9 @@
</script> </script>
<div class="flex flex-grow flex-wrap justify-end gap-2"> <div class="flex flex-grow flex-wrap justify-end gap-2">
{#if room && showRoom} {#if h && showRoom}
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full"> <Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<ChannelName {room} {url} /> Posted in #<RoomName {h} {url} />
</Link> </Link>
{/if} {/if}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" /> <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
@@ -4,13 +4,13 @@
type Props = { type Props = {
url: string url: string
room?: string h?: string
} }
const {url, room}: Props = $props() const {url, h}: Props = $props()
</script> </script>
<CalendarEventForm {url} {room}> <CalendarEventForm {url} {h}>
{#snippet header()} {#snippet header()}
<ModalHeader> <ModalHeader>
{#snippet title()} {#snippet title()}
+4 -4
View File
@@ -23,7 +23,7 @@
type Props = { type Props = {
url: string url: string
room?: string h?: string
header: Snippet header: Snippet
initialValues?: { initialValues?: {
d: string d: string
@@ -35,7 +35,7 @@
} }
} }
const {url, room, header, initialValues}: Props = $props() const {url, h, header, initialValues}: Props = $props()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -85,8 +85,8 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
if (room) { if (h) {
tags.push(["h", room]) tags.push(["h", h])
} }
const event = makeEvent(EVENT_TIME, {content, tags}) const event = makeEvent(EVENT_TIME, {content, tags})
+7 -5
View File
@@ -5,7 +5,7 @@
import CalendarEventActions from "@app/components/CalendarEventActions.svelte" import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte" import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
import ChannelLink from "@app/components/ChannelLink.svelte" import RoomLink from "@app/components/RoomLink.svelte"
import {makeCalendarPath} from "@app/util/routes" import {makeCalendarPath} from "@app/util/routes"
type Props = { type Props = {
@@ -15,16 +15,18 @@
const {url, event}: Props = $props() const {url, event}: Props = $props()
const room = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
</script> </script>
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}> <Link
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
href={makeCalendarPath(url, event.id)}>
<CalendarEventHeader {event} /> <CalendarEventHeader {event} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
<span class="whitespace-nowrap py-1 text-sm opacity-75"> <span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by <ProfileLink pubkey={event.pubkey} {url} /> Posted by <ProfileLink pubkey={event.pubkey} {url} />
{#if room} {#if h}
in <ChannelLink {url} {room} /> in <RoomLink {url} {h} />
{/if} {/if}
</span> </span>
<CalendarEventActions showActivity {url} {event} /> <CalendarEventActions showActivity {url} {event} />
-7
View File
@@ -1,7 +0,0 @@
<script lang="ts">
import {channelsById, makeChannelId} from "@app/core/state"
const {url, room} = $props()
</script>
{$channelsById.get(makeChannelId(url, room))?.name || room}
+35 -48
View File
@@ -28,8 +28,8 @@
tagPubkey, tagPubkey,
sendWrapped, sendWrapped,
mergeThunks, mergeThunks,
loadInboxRelaySelections, loadMessagingRelayList,
inboxRelaySelectionsByPubkey, messagingRelayListsByPubkey,
} from "@welshman/app" } from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -43,36 +43,31 @@
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte" import ProfileCircles from "@app/components/ProfileCircles.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ProfileList from "@app/components/ProfileList.svelte" import ChatMembers from "@app/components/ChatMembers.svelte"
import ChatMessage from "@app/components/ChatMessage.svelte" import ChatMessage from "@app/components/ChatMessage.svelte"
import ChatCompose from "@app/components/ChatCompose.svelte" import ChatCompose from "@app/components/ChatCompose.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.svelte" import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte" import ThunkToast from "@app/components/ThunkToast.svelte"
import { import {INDEXER_RELAYS, userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
INDEXER_RELAYS,
userSettingsValues,
deriveChat,
splitChatId,
PLATFORM_NAME,
} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {prependParent} from "@app/core/commands" import {prependParent} from "@app/core/commands"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
type Props = { type Props = {
id: string pubkeys: string[]
info?: Snippet info?: Snippet
} }
const {id, info}: Props = $props() const {pubkeys, info}: Props = $props()
const chat = deriveChat(id) const chat = deriveChat(pubkeys)
const pubkeys = splitChatId(id)
const others = remove($pubkey!, pubkeys) const others = remove($pubkey!, pubkeys)
const missingInboxes = $derived(pubkeys.filter(pk => !$inboxRelaySelectionsByPubkey.has(pk))) const missingRelayLists = $derived(pubkeys.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
const showMembers = () => const showMembers = () =>
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`}) others.length === 1
? pushModal(ProfileDetail, {pubkey: others[0]})
: pushModal(ChatMembers, {pubkeys: others})
const replyTo = (event: TrustedEvent) => { const replyTo = (event: TrustedEvent) => {
parent = event parent = event
@@ -183,7 +178,7 @@
onMount(() => { onMount(() => {
for (const pubkey of others) { for (const pubkey of others) {
loadInboxRelaySelections(pubkey, INDEXER_RELAYS, true) loadMessagingRelayList(pubkey, INDEXER_RELAYS, true)
} }
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
@@ -208,19 +203,17 @@
<PageBar> <PageBar>
{#snippet title()} {#snippet title()}
<div class="flex flex-col gap-1 sm:flex-row sm:gap-2"> <Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
{#if others.length === 0} {#if others.length === 0}
<div class="row-2"> <div class="row-2">
<ProfileCircle pubkey={$pubkey!} size={5} /> <ProfileCircle pubkey={$pubkey!} size={5} />
<ProfileName pubkey={$pubkey!} /> <ProfileName pubkey={$pubkey!} />
</div> </div>
{:else if others.length === 1} {:else if others.length === 1}
{@const pubkey = others[0]} <div class="row-2">
{@const onClick = () => pushModal(ProfileDetail, {pubkey})} <ProfileCircle pubkey={others[0]} size={5} />
<Button onclick={onClick} class="row-2"> <ProfileName pubkey={others[0]} />
<ProfileCircle {pubkey} size={5} /> </div>
<ProfileName {pubkey} />
</Button>
{:else} {:else}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<ProfileCircles pubkeys={others} size={5} /> <ProfileCircles pubkeys={others} size={5} />
@@ -235,55 +228,49 @@
{/if} {/if}
</p> </p>
</div> </div>
{#if others.length > 2}
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
>Show all members</Button>
{/if}
{/if} {/if}
</div> </Button>
{/snippet} {/snippet}
{#snippet action()} {#snippet action()}
<div> {#if remove($pubkey, missingRelayLists).length > 0}
{#if remove($pubkey, missingInboxes).length > 0} {@const count = remove($pubkey, missingRelayLists).length}
{@const count = remove($pubkey, missingInboxes).length} {@const label = count > 1 ? "lists are" : "list is"}
{@const label = count > 1 ? "inboxes are" : "inbox is"} <div
<div class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer" data-tip="{count} messaging {label} not configured.">
data-tip="{count} {label} not configured."> <Icon icon={Danger} />
<Icon icon={Danger} /> {count}
{count} </div>
</div> {/if}
{/if}
</div>
{/snippet} {/snippet}
</PageBar> </PageBar>
<PageContent class="flex flex-col-reverse gap-2 pt-4"> <PageContent class="flex flex-col-reverse gap-2 pt-4">
<div bind:this={dynamicPadding}></div> <div bind:this={dynamicPadding}></div>
{#if missingInboxes.includes($pubkey!)} {#if missingRelayLists.includes($pubkey!)}
<div class="py-12"> <div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center"> <div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error"> <p class="row-2 text-lg text-error">
<Icon icon={Danger} /> <Icon icon={Danger} />
Your inbox is not configured. Your messaging relays are not configured.
</p> </p>
<p> <p>
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your inbox. your <Link class="link" href="/settings/relays">relay settings page</Link> to receive messages.
</p> </p>
</div> </div>
</div> </div>
{:else if missingInboxes.length > 0} {:else if missingRelayLists.length > 0}
<div class="py-12"> <div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center"> <div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error"> <p class="row-2 text-lg text-error">
<Icon icon={Danger} /> <Icon icon={Danger} />
{missingInboxes.length} {missingRelayLists.length} messaging
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured. {missingRelayLists.length > 1 ? "lists are" : "list is"} not configured.
</p> </p>
<p> <p>
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
sure everyone in this conversation has set up their inbox relays. sure everyone in this conversation has set up their messaging relays.
</p> </p>
</div> </div>
</div> </div>
+7 -4
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {remove} from "@welshman/lib" import {remove, formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {pubkey, loadInboxRelaySelections} from "@welshman/app" import {pubkey, loadMessagingRelayList} from "@welshman/app"
import {fade} from "@lib/transition" import {fade} from "@lib/transition"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
@@ -27,7 +27,7 @@
onMount(() => { onMount(() => {
for (const pk of others) { for (const pk of others) {
loadInboxRelaySelections(pk) loadMessagingRelayList(pk)
} }
}) })
</script> </script>
@@ -59,13 +59,16 @@
{/if} {/if}
</div> </div>
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm"> <p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
<span class="opacity-50"> <span class="opacity-70">
{#if props.messages[0].pubkey === $pubkey} {#if props.messages[0].pubkey === $pubkey}
You: You:
{/if} {/if}
</span> </span>
{props.messages[0].content} {props.messages[0].content}
</p> </p>
<p class="text-xs opacity-70">
{formatTimestamp(props.messages[0].created_at)}
</p>
</div> </div>
</div> </div>
</Link> </Link>
+25
View File
@@ -0,0 +1,25 @@
<script lang="ts">
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import Profile from "@app/components/Profile.svelte"
interface Props {
pubkeys: string[]
}
const {pubkeys}: Props = $props()
</script>
<div class="column gap-4">
<ModalHeader>
{#snippet title()}
<div>People in this conversation</div>
{/snippet}
</ModalHeader>
{#each pubkeys as pubkey (pubkey)}
<div class="card2 bg-alt">
<Profile {pubkey} />
</div>
{/each}
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
</div>
+4 -3
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {waitForThunkCompletion} from "@welshman/app" import {RelayMode} from "@welshman/util"
import {waitForThunkCompletion, getPubkeyRelays, pubkey} from "@welshman/app"
import ChatSquare from "@assets/icons/chat-square.svg?dataurl" import ChatSquare from "@assets/icons/chat-square.svg?dataurl"
import Check from "@assets/icons/check.svg?dataurl" import Check from "@assets/icons/check.svg?dataurl"
import Bell from "@assets/icons/bell.svg?dataurl" import Bell from "@assets/icons/bell.svg?dataurl"
@@ -11,7 +12,7 @@
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {dmAlert, userInboxRelays} from "@app/core/state" import {dmAlert} from "@app/core/state"
import {deleteAlert, createDmAlert} from "@app/core/commands" import {deleteAlert, createDmAlert} from "@app/core/commands"
const startChat = () => pushModal(ChatStart, {}, {replaceState: true}) const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
@@ -22,7 +23,7 @@
} }
const enableAlerts = async () => { const enableAlerts = async () => {
if ($userInboxRelays.length === 0) { if (getPubkeyRelays($pubkey!, RelayMode.Messaging).length === 0) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Please set up your messaging relays before enabling alerts.", message: "Please set up your messaging relays before enabling alerts.",
+4 -12
View File
@@ -2,21 +2,14 @@
import {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {hash, formatTimestampAsTime} from "@welshman/lib" import {hash, formatTimestampAsTime} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import { import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
thunks,
mergeThunks,
pubkey,
deriveProfile,
deriveProfileDisplay,
sendWrapped,
} from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Tippy from "@lib/components/Tippy.svelte" import Tippy from "@lib/components/Tippy.svelte"
import TapTarget from "@lib/components/TapTarget.svelte" import TapTarget from "@lib/components/TapTarget.svelte"
import Avatar from "@lib/components/Avatar.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkFailure from "@app/components/ThunkFailure.svelte" import ThunkFailure from "@app/components/ThunkFailure.svelte"
@@ -37,7 +30,6 @@
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props() const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
const isOwn = event.pubkey === $pubkey const isOwn = event.pubkey === $pubkey
const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id)) const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
@@ -107,8 +99,8 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{#if !isOwn} {#if !isOwn}
<Button onclick={openProfile} class="flex items-center gap-1"> <Button onclick={openProfile} class="flex items-center gap-1">
<Avatar <ProfileCircle
src={$profile?.picture} pubkey={event.pubkey}
class="border border-solid border-base-content" class="border border-solid border-base-content"
size={4} /> size={4} />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
+10 -10
View File
@@ -46,20 +46,20 @@
</script> </script>
<div class="col-2"> <div class="col-2">
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}> <Button class="btn btn-neutral" onclick={showInfo}>
<Icon size={4} icon={SmileCircle} /> <Icon size={4} icon={Code2} />
Send Reaction Message Info
</Button>
<Button class="btn btn-neutral w-full" onclick={sendReply}>
<Icon size={4} icon={Reply} />
Send Reply
</Button> </Button>
<Button class="btn btn-neutral w-full" onclick={copyText}> <Button class="btn btn-neutral w-full" onclick={copyText}>
<Icon size={4} icon={Copy} /> <Icon size={4} icon={Copy} />
Copy Text Copy Text
</Button> </Button>
<Button class="btn btn-neutral" onclick={showInfo}> <Button class="btn btn-neutral w-full" onclick={sendReply}>
<Icon size={4} icon={Code2} /> <Icon size={4} icon={Reply} />
Message Details Send Reply
</Button>
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
<Icon size={4} icon={SmileCircle} />
Send Reaction
</Button> </Button>
</div> </div>
+6 -6
View File
@@ -13,16 +13,16 @@
type Props = { type Props = {
url: string url: string
onClick: () => void onClick: () => void
room?: string h?: string
} }
const {url, room, onClick}: Props = $props() const {url, h, onClick}: Props = $props()
const createGoal = () => pushModal(GoalCreate, {url, room}) const createGoal = () => pushModal(GoalCreate, {url, h})
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, room}) const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, h})
const createThread = () => pushModal(ThreadCreate, {url, room}) const createThread = () => pushModal(ThreadCreate, {url, h})
let ul: Element let ul: Element
@@ -31,7 +31,7 @@
}) })
</script> </script>
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}> <ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md" bind:this={ul}>
<li> <li>
<Button onclick={createGoal}> <Button onclick={createGoal}>
<Icon size={4} icon={StarFallMinimalistic} /> <Icon size={4} icon={StarFallMinimalistic} />
+12 -4
View File
@@ -1,17 +1,25 @@
<script lang="ts"> <script lang="ts">
import {ellipsize, displayUrl, postJson} from "@welshman/lib" import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
import {dufflepud} from "@app/core/state" import {isRelayUrl} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html" import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte" import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte" import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {dufflepud, PLATFORM_URL} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props() const {value, event} = $props()
let hideImage = $state(false) let hideImage = $state(false)
const url = value.url.toString() const url = value.url.toString()
const [href, external] = call(() => {
if (isRelayUrl(url)) return [makeSpacePath(url), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
return [url, true]
})
const loadPreview = async () => { const loadPreview = async () => {
const json = await postJson(dufflepud("link/preview"), {url}) const json = await postJson(dufflepud("link/preview"), {url})
@@ -30,7 +38,7 @@
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true}) const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
</script> </script>
<Link external href={url} class="my-2 block"> <Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box"> <div class="overflow-hidden rounded-box">
{#if url.match(/\.(mov|webm|mp4)$/)} {#if url.match(/\.(mov|webm|mp4)$/)}
<video controls src={url} class="max-h-96 object-contain object-center"> <video controls src={url} class="max-h-96 object-contain object-center">
@@ -49,7 +57,7 @@
<div class="bg-alt flex max-w-xl flex-col leading-normal"> <div class="bg-alt flex max-w-xl flex-col leading-normal">
{#if preview.image && !hideImage} {#if preview.image && !hideImage}
<img <img
alt="Link preview" alt=""
onerror={onError} onerror={onError}
src={preview.image} src={preview.image}
class="bg-alt max-h-72 rounded-t-box object-contain object-center" /> class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
@@ -21,7 +21,8 @@
.map(tagsFromIMeta) .map(tagsFromIMeta)
.find(meta => getTagValue("url", meta) === url) || event.tags .find(meta => getTagValue("url", meta) === url) || event.tags
const hash = getTagValue("x", meta) // Fallback to filename if hash was omitted from the message for interoperability
const hash = getTagValue("x", meta) || url.split(/[\/\.]/).slice(-2)[0]
const key = getTagValue("decryption-key", meta) const key = getTagValue("decryption-key", meta)
const nonce = getTagValue("decryption-nonce", meta) const nonce = getTagValue("decryption-nonce", meta)
const algorithm = getTagValue("encryption-algorithm", meta) const algorithm = getTagValue("encryption-algorithm", meta)
+11 -2
View File
@@ -1,15 +1,24 @@
<script lang="ts"> <script lang="ts">
import {displayUrl} from "@welshman/lib" import {call, displayUrl} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte" import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {PLATFORM_URL} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value} = $props() const {value} = $props()
const url = value.url.toString() const url = value.url.toString()
const [href, external] = call(() => {
if (isRelayUrl(url)) return [makeSpacePath(url), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
return [url, true]
})
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true}) const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
</script> </script>
@@ -21,7 +30,7 @@
{displayUrl(url)} {displayUrl(url)}
</a> </a>
{:else} {:else}
<Link external href={url} class="link-content whitespace-nowrap"> <Link {external} {href} class="link-content whitespace-nowrap">
<Icon icon={LinkRound} size={3} class="inline-block" /> <Icon icon={LinkRound} size={3} class="inline-block" />
{displayUrl(url)} {displayUrl(url)}
</Link> </Link>
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import type {ProfilePointer} from "@welshman/content" import type {ProfilePointer} from "@welshman/content"
import {deriveProfileDisplay} from "@welshman/app" import {deriveProfileDisplay} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -13,7 +13,7 @@
const {value, url}: Props = $props() const {value, url}: Props = $props()
const display = deriveProfileDisplay(value.pubkey, removeNil([url])) const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
</script> </script>
+6 -6
View File
@@ -8,29 +8,29 @@
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte" import ProfileCircles from "@app/components/ProfileCircles.svelte"
import {goToEvent} from "@app/util/routes" import {goToEvent} from "@app/util/routes"
import {displayChannel} from "@app/core/state" import {displayRoom} from "@app/core/state"
type Props = { type Props = {
url: string url: string
room?: string h?: string
events: TrustedEvent[] events: TrustedEvent[]
latest: TrustedEvent latest: TrustedEvent
earliest: TrustedEvent earliest: TrustedEvent
participants: string[] participants: string[]
} }
const {url, room, events, latest, earliest, participants}: Props = $props() const {url, h, events, latest, earliest, participants}: Props = $props()
</script> </script>
<Button class="card2 bg-alt" onclick={() => goToEvent(earliest)}> <Button class="card2 bg-alt shadow-lg" onclick={() => goToEvent(earliest)}>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<ProfileCircle pubkey={earliest.pubkey} size={10} /> <ProfileCircle pubkey={earliest.pubkey} size={10} />
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-2 text-sm opacity-70"> <div class="flex items-center gap-2 text-sm opacity-70">
{#if room} {#if h}
<span class="truncate font-medium text-blue-400"> <span class="truncate font-medium text-blue-400">
#{displayChannel(url, room)} #{displayRoom(url, h)}
</span> </span>
<span class="opacity-50"></span> <span class="opacity-50"></span>
{/if} {/if}
+2 -2
View File
@@ -3,7 +3,7 @@
import {max, formatTimestampRelative} from "@welshman/lib" import {max, formatTimestampRelative} from "@welshman/lib"
import {COMMENT} from "@welshman/util" import {COMMENT} from "@welshman/util"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {deriveEvents} from "@welshman/store" import {deriveArray, deriveEventsById} from "@welshman/store"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {repository} from "@welshman/app" import {repository} from "@welshman/app"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
@@ -13,7 +13,7 @@
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props() const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
const filters = [{kinds: [COMMENT], "#E": [event.id]}] const filters = [{kinds: [COMMENT], "#E": [event.id]}]
const replies = deriveEvents(repository, {filters}) const replies = deriveArray(deriveEventsById({repository, filters}))
const lastActive = $derived(max([...$replies, event].map(e => e.created_at))) const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
onMount(() => { onMount(() => {
+2 -2
View File
@@ -4,6 +4,7 @@
import {LOCALE, secondsToDate} from "@welshman/lib" import {LOCALE, secondsToDate} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {tracker} from "@welshman/app"
import FileText from "@assets/icons/file-text.svg?dataurl" import FileText from "@assets/icons/file-text.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl" import Copy from "@assets/icons/copy.svg?dataurl"
import UserCircle from "@assets/icons/user-circle.svg?dataurl" import UserCircle from "@assets/icons/user-circle.svg?dataurl"
@@ -11,7 +12,6 @@
import FieldInline from "@lib/components/FieldInline.svelte" import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import {trackerStore} from "@app/core/state"
import {clip} from "@app/util/toast" import {clip} from "@app/util/toast"
type Props = { type Props = {
@@ -23,7 +23,7 @@
const relays = url ? [url] : Router.get().Event(event).getUrls() const relays = url ? [url] : Router.get().Event(event).getUrls()
const nevent1 = nip19.neventEncode({...event, relays}) const nevent1 = nip19.neventEncode({...event, relays})
const seenOn = $trackerStore.getRelays(event.id) const seenOn = tracker.getRelays(event.id)
const npub1 = nip19.npubEncode(event.pubkey) const npub1 = nip19.npubEncode(event.pubkey)
const json = JSON.stringify(event, null, 2) const json = JSON.stringify(event, null, 2)
const copyLink = () => clip(nevent1) const copyLink = () => clip(nevent1)
+37 -6
View File
@@ -3,21 +3,23 @@
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT} from "@welshman/util" import {COMMENT, ManagementMethod} from "@welshman/util"
import {pubkey, relaysByUrl} from "@welshman/app" import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl" import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl" import Danger from "@assets/icons/danger.svg?dataurl"
import {setKey} from "@lib/implicit" import {setKey} from "@lib/implicit"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import EventInfo from "@app/components/EventInfo.svelte" import EventInfo from "@app/components/EventInfo.svelte"
import EventReport from "@app/components/EventReport.svelte" import Report from "@app/components/Report.svelte"
import EventShare from "@app/components/EventShare.svelte" import EventShare from "@app/components/EventShare.svelte"
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte" import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import {hasNip29} from "@app/core/state" import {hasNip29, deriveUserIsSpaceAdmin} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {makeSpaceChatPath} from "@app/util/routes" import {makeSpaceChatPath} from "@app/util/routes"
type Props = { type Props = {
@@ -31,8 +33,9 @@
const {url, noun, event, onClick, customActions}: Props = $props() const {url, noun, event, onClick, customActions}: Props = $props()
const isRoot = event.kind !== COMMENT const isRoot = event.kind !== COMMENT
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const report = () => pushModal(EventReport, {url, event}) const report = () => pushModal(Report, {url, event})
const showInfo = () => pushModal(EventInfo, {url, event}) const showInfo = () => pushModal(EventInfo, {url, event})
@@ -47,6 +50,26 @@
const showDelete = () => pushModal(EventDeleteConfirm, {url, event}) const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
const showAdminDelete = () =>
pushModal(Confirm, {
title: `Delete ${noun}`,
message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent,
params: [event.id],
})
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "Event has successfully been deleted!"})
repository.removeEvent(event.id)
history.back()
}
},
})
let ul: Element let ul: Element
onMount(() => { onMount(() => {
@@ -54,7 +77,7 @@
}) })
</script> </script>
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}> <ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md" bind:this={ul}>
{#if isRoot} {#if isRoot}
<li> <li>
<Button onclick={share}> <Button onclick={share}>
@@ -84,5 +107,13 @@
Report Content Report Content
</Button> </Button>
</li> </li>
{#if $userIsAdmin}
<li>
<Button class="text-error" onclick={showAdminDelete}>
<Icon size={4} icon={TrashBin2} />
Delete {noun}
</Button>
</li>
{/if}
{/if} {/if}
</ul> </ul>
@@ -1,60 +0,0 @@
<script lang="ts">
import {getTag, REPORT} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {pubkey, repository} from "@welshman/app"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte"
import {publishDelete, canEnforceNip70} from "@app/core/commands"
const {url, event} = $props()
const shouldProtect = canEnforceNip70(url)
const reports = deriveEvents(repository, {
filters: [{kinds: [REPORT], "#e": [event.id]}],
})
const back = () => history.back()
const deleteReport = async (report: TrustedEvent) => {
publishDelete({event: report, relays: [url], protect: await shouldProtect})
if ($reports.length === 0) {
history.back()
}
}
const getReason = (tags: string[][]) => getTag("e", tags)?.[2] || "other"
</script>
<div class="column gap-4">
<ModalHeader>
{#snippet title()}
<div>Report Details</div>
{/snippet}
{#snippet info()}
<div>All reports for this event are shown below.</div>
{/snippet}
</ModalHeader>
{#each $reports as report (report.id)}
{@const reason = getReason(report.tags)}
{@const remove = () => deleteReport(report)}
<div class="column gap-2">
<div class="flex justify-between">
<div>
<Profile pubkey={report.pubkey} {url} />
<span>Reported this event as "{reason}"</span>
</div>
{#if report.pubkey === $pubkey}
<Button class="btn-default btn" onclick={remove}>Delete Report</Button>
{/if}
</div>
{#if report.content}
<p>"{report.content}"</p>
{/if}
</div>
{/each}
<Button class="btn btn-primary" onclick={back}>Got it</Button>
</div>
+9 -9
View File
@@ -9,8 +9,8 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import RoomName from "@app/components/RoomName.svelte"
import {channelsByUrl} from "@app/core/state" import {roomsByUrl} from "@app/core/state"
import {makeRoomPath} from "@app/util/routes" import {makeRoomPath} from "@app/util/routes"
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props() const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
@@ -22,8 +22,8 @@
goto(makeRoomPath(url, selection), {replaceState: true}) goto(makeRoomPath(url, selection), {replaceState: true})
} }
const toggleRoom = (room: string) => { const toggleRoom = (h: string) => {
selection = room === selection ? "" : room selection = h === selection ? "" : h
} }
let selection = $state("") let selection = $state("")
@@ -39,14 +39,14 @@
{/snippet} {/snippet}
</ModalHeader> </ModalHeader>
<div class="grid grid-cols-3 gap-2"> <div class="grid grid-cols-3 gap-2">
{#each $channelsByUrl.get(url) || [] as channel (channel.room)} {#each $roomsByUrl.get(url) || [] as room (room.h)}
<button <button
type="button" type="button"
class="btn" class="btn"
class:btn-neutral={selection !== channel.room} class:btn-neutral={selection !== room.h}
class:btn-primary={selection === channel.room} class:btn-primary={selection === room.h}
onclick={() => toggleRoom(channel.room)}> onclick={() => toggleRoom(room.h)}>
#<ChannelName {...channel} /> #<RoomName {...room} />
</button> </button>
{/each} {/each}
</div> </div>
+5 -5
View File
@@ -6,7 +6,7 @@
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte" import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte" import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte" import EventActions from "@app/components/EventActions.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import RoomName from "@app/components/RoomName.svelte"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands" import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
import {makeGoalPath, makeSpacePath} from "@app/util/routes" import {makeGoalPath, makeSpacePath} from "@app/util/routes"
@@ -20,7 +20,7 @@
const {url, event, showRoom, showActivity}: Props = $props() const {url, event, showRoom, showActivity}: Props = $props()
const path = makeGoalPath(url, event.id) const path = makeGoalPath(url, event.id)
const room = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
@@ -31,9 +31,9 @@
</script> </script>
<div class="flex flex-grow flex-wrap justify-end gap-2"> <div class="flex flex-grow flex-wrap justify-end gap-2">
{#if room && showRoom} {#if h && showRoom}
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full"> <Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<ChannelName {room} {url} /> Posted in #<RoomName {h} {url} />
</Link> </Link>
{/if} {/if}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" /> <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
+4 -4
View File
@@ -20,10 +20,10 @@
type Props = { type Props = {
url: string url: string
room?: string h?: string
} }
const {url, room}: Props = $props() const {url, h}: Props = $props()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -64,8 +64,8 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
if (room) { if (h) {
tags.push(["h", room]) tags.push(["h", h])
} }
publishThunk({ publishThunk({
+5 -5
View File
@@ -6,7 +6,7 @@
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
import GoalActions from "@app/components/GoalActions.svelte" import GoalActions from "@app/components/GoalActions.svelte"
import GoalSummary from "@app/components/GoalSummary.svelte" import GoalSummary from "@app/components/GoalSummary.svelte"
import ChannelLink from "@app/components/ChannelLink.svelte" import RoomLink from "@app/components/RoomLink.svelte"
import {makeGoalPath} from "@app/util/routes" import {makeGoalPath} from "@app/util/routes"
type Props = { type Props = {
@@ -17,10 +17,10 @@
const {url, event}: Props = $props() const {url, event}: Props = $props()
const summary = getTagValue("summary", event.tags) const summary = getTagValue("summary", event.tags)
const room = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
</script> </script>
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeGoalPath(url, event.id)}> <Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-md" href={makeGoalPath(url, event.id)}>
<p class="text-2xl">{event.content}</p> <p class="text-2xl">{event.content}</p>
<Content <Content
event={{content: summary, tags: event.tags}} event={{content: summary, tags: event.tags}}
@@ -32,8 +32,8 @@
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
<span class="whitespace-nowrap py-1 text-sm opacity-75"> <span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by <ProfileLink pubkey={event.pubkey} {url} /> Posted by <ProfileLink pubkey={event.pubkey} {url} />
{#if room} {#if h}
in <ChannelLink {url} {room} /> in <RoomLink {url} {h} />
{/if} {/if}
</span> </span>
<GoalActions showActivity {url} {event} /> <GoalActions showActivity {url} {event} />
+9 -6
View File
@@ -2,7 +2,7 @@
import {now, DAY, uniq, sum} from "@welshman/lib" import {now, DAY, uniq, sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util" import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util" import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {deriveEventsMapped} from "@welshman/store" import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {repository, getValidZap} from "@welshman/app" import {repository, getValidZap} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -16,11 +16,14 @@
const {url, event, ...props}: Props = $props() const {url, event, ...props}: Props = $props()
const zaps = deriveEventsMapped<Zap>(repository, { const zaps = deriveArray(
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}], deriveItemsByKey<Zap>({
itemToEvent: item => item.response, repository,
eventToItem: (response: TrustedEvent) => getValidZap(response, event), getKey: zap => zap.response.id,
}) filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
}),
)
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0") const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount)))) const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
+1 -1
View File
@@ -43,7 +43,7 @@
} }
</script> </script>
<div class="w-96 rounded-box bg-base-100 p-4 shadow-lg"> <div class="w-96 rounded-box bg-base-100 p-4 shadow-2xl">
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Magnifier} /> <Icon icon={Magnifier} />
<input bind:value={searchTerm} class="grow" type="text" placeholder="Search icons..." /> <input bind:value={searchTerm} class="grow" type="text" placeholder="Search icons..." />
+2 -1
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {onMount, onDestroy} from "svelte" import {onMount, onDestroy} from "svelte"
import type {Nip46ResponseWithResult} from "@welshman/signer" import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker, makeSecret} from "@welshman/signer" import {Nip46Broker} from "@welshman/signer"
import {makeSecret} from "@welshman/util"
import {loginWithNip01, loginWithNip46} from "@welshman/app" import {loginWithNip01, loginWithNip46} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
+2 -2
View File
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import {onMount, onDestroy} from "svelte" import {onMount, onDestroy} from "svelte"
import {postJson, stripProtocol} from "@welshman/lib" import {postJson, stripProtocol} from "@welshman/lib"
import {Nip46Broker, makeSecret} from "@welshman/signer" import {Nip46Broker} from "@welshman/signer"
import {normalizeRelayUrl} from "@welshman/util" import {normalizeRelayUrl, makeSecret} from "@welshman/util"
import {addSession, makeNip46Session} from "@welshman/app" import {addSession, makeNip46Session} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
+5 -24
View File
@@ -1,43 +1,24 @@
<script lang="ts"> <script lang="ts">
import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte" import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
import {makeRoomPath} from "@app/util/routes" import {makeRoomPath} from "@app/util/routes"
import {deriveChannel} from "@app/core/state"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
interface Props { interface Props {
url: any url: any
room: any h: any
notify?: boolean notify?: boolean
replaceState?: boolean replaceState?: boolean
} }
const {url, room, notify = false, replaceState = false}: Props = $props() const {url, h, notify = false, replaceState = false}: Props = $props()
const path = makeRoomPath(url, room) const path = makeRoomPath(url, h)
const channel = deriveChannel(url, room)
</script> </script>
<SecondaryNavItem <SecondaryNavItem
href={path} href={path}
{replaceState} {replaceState}
notification={notify ? $notifications.has(path) : false}> notification={notify ? $notifications.has(path) : false}>
{#if $channel?.picture} <RoomNameWithImage {url} {h} />
{@const src = $channel.picture}
{#if src.match("\.(png|svg)$") || src.match("image/(png|svg)")}
<Icon icon={src} />
{:else}
<img alt="Room icon" {src} class="h-6 w-6 rounded-lg" />
{/if}
{:else if $channel?.closed || $channel?.private}
<Icon icon={Lock} />
{:else}
<Icon icon={Hashtag} />
{/if}
<div class="min-w-0 overflow-hidden text-ellipsis">
<ChannelName {url} {room} />
</div>
</SecondaryNavItem> </SecondaryNavItem>
-35
View File
@@ -1,35 +0,0 @@
<script lang="ts">
import Compass from "@assets/icons/compass.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Divider from "@lib/components/Divider.svelte"
import CardButton from "@lib/components/CardButton.svelte"
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
import {userSpaceUrls, PLATFORM_RELAYS} from "@app/core/state"
</script>
<div class="column menu gap-2">
{#each PLATFORM_RELAYS as url (url)}
<MenuSpacesItem {url} />
{:else}
{#if $userSpaceUrls.length > 0}
{#each $userSpaceUrls as url (url)}
<MenuSpacesItem {url} />
{/each}
<Divider />
{/if}
<Link href="/discover">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Compass} size={7} /></div>
{/snippet}
{#snippet title()}
<div>Explore Spaces</div>
{/snippet}
{#snippet info()}
<div>Join create, or browse spaces</div>
{/snippet}
</CardButton>
</Link>
{/each}
</div>
+3 -3
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte" import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte" import RelayName from "@app/components/RelayName.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte" import RelayDescription from "@app/components/RelayDescription.svelte"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
@@ -13,9 +13,9 @@
</script> </script>
<Link replaceState href={path}> <Link replaceState href={path}>
<CardButton class="btn-neutral"> <CardButton class="btn-neutral shadow-md">
{#snippet icon()} {#snippet icon()}
<div><SpaceAvatar {url} /></div> <RelayIcon {url} size={12} />
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<div class="flex gap-1"> <div class="flex gap-1">
+9 -2
View File
@@ -4,9 +4,15 @@
import Dialog from "@lib/components/Dialog.svelte" import Dialog from "@lib/components/Dialog.svelte"
import {modal, clearModals} from "@app/util/modal" import {modal, clearModals} from "@app/util/modal"
const closeModals = () => {
if ($modal && !$modal.options.noEscape) {
clearModals()
}
}
const onKeyDown = (e: any) => { const onKeyDown = (e: any) => {
if (e.code === "Escape" && e.target === document.body) { if (e.code === "Escape" && e.target === document.body) {
clearModals() closeModals()
} }
} }
@@ -27,7 +33,8 @@
instance = mount(wrapper as any, { instance = mount(wrapper as any, {
target: element, target: element,
props: { props: {
onClose: clearModals, onClose: closeModals,
fullscreen: options.fullscreen,
children: createRawSnippet(() => ({ children: createRawSnippet(() => ({
render: () => "<div></div>", render: () => "<div></div>",
setup: (target: Element) => { setup: (target: Element) => {
+7 -14
View File
@@ -1,19 +1,16 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import * as nip19 from "nostr-tools/nip19"
import {formatTimestamp} from "@welshman/lib" import {formatTimestamp} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util" import {getListTags, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {Router} from "@welshman/router" import {userMuteList} from "@welshman/app"
import {userMutes} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte" import Profile from "@app/components/Profile.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
import {entityLink} from "@app/core/state" import {goToEvent} from "@app/util/routes"
const { const {
event, event,
@@ -31,14 +28,11 @@
class?: string class?: string
} = $props() } = $props()
const relays = Router.get().Event(event).getUrls()
const nevent = nip19.neventEncode({id: event.id, relays})
const ignoreMute = () => { const ignoreMute = () => {
muted = false muted = false
} }
let muted = $state(getPubkeyTagValues(getListTags($userMutes)).includes(event.pubkey)) let muted = $state(getPubkeyTagValues(getListTags($userMuteList)).includes(event.pubkey))
</script> </script>
<div class="flex flex-col gap-2 {restProps.class}"> <div class="flex flex-col gap-2 {restProps.class}">
@@ -59,12 +53,11 @@
<Profile pubkey={event.pubkey} {url} /> <Profile pubkey={event.pubkey} {url} />
{/if} {/if}
{/if} {/if}
<Link <Button
external class={cx("text-sm opacity-75", {"text-xs": minimal})}
href={entityLink(nevent)} onclick={() => goToEvent(event)}>
class={cx("text-sm opacity-75", {"text-xs": minimal})}>
{formatTimestamp(event.created_at)} {formatTimestamp(event.created_at)}
</Link> </Button>
</div> </div>
{@render children()} {@render children()}
{/if} {/if}
@@ -3,7 +3,7 @@
import {sum} from "@welshman/lib" import {sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util" import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util" import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {deriveEventsMapped} from "@welshman/store" import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {repository, getValidZap} from "@welshman/app" import {repository, getValidZap} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -14,11 +14,14 @@
const content = getTagValue("summary", props.event.tags) const content = getTagValue("summary", props.event.tags)
const fakeEvent = {content, tags: props.event.tags} const fakeEvent = {content, tags: props.event.tags}
const zaps = deriveEventsMapped<Zap>(repository, { const zaps = deriveArray(
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}], deriveItemsByKey<Zap>({
itemToEvent: item => item.response, repository,
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event), getKey: zap => zap.response.id,
}) filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
}),
)
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0") const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount)))) const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
+1 -1
View File
@@ -18,7 +18,7 @@
const openProfile = () => pushModal(ProfileDetail, {pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
</script> </script>
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-4 shadow-md">
<div class="flex justify-between"> <div class="flex justify-between">
<Profile {pubkey} {url} /> <Profile {pubkey} {url} />
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex"> <Button onclick={openProfile} class="btn btn-primary hidden sm:flex">
+29 -30
View File
@@ -4,12 +4,18 @@
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {splitAt} from "@welshman/lib" import {splitAt} from "@welshman/lib"
import {userProfile, shouldUnwrap} from "@welshman/app" import {userProfile, shouldUnwrap} from "@welshman/app"
import Avatar from "@lib/components/Avatar.svelte" import Widget from "@assets/icons/widget.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import Settings from "@assets/icons/settings.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte" import ChatEnable from "@app/components/ChatEnable.svelte"
import MenuSpaces from "@app/components/MenuSpaces.svelte"
import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte" import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte"
import MenuSettings from "@app/components/MenuSettings.svelte" import MenuSettings from "@app/components/MenuSettings.svelte"
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte" import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
@@ -17,13 +23,6 @@
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
import Widget from "@assets/icons/widget.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import Settings from "@assets/icons/settings.svg?dataurl"
type Props = { type Props = {
children?: Snippet children?: Snippet
@@ -31,9 +30,6 @@
const {children}: Props = $props() const {children}: Props = $props()
const showSpacesMenu = () =>
$userSpaceUrls.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd)
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls}) const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
const showSettingsMenu = () => pushModal(MenuSettings) const showSettingsMenu = () => pushModal(MenuSettings)
@@ -66,7 +62,7 @@
<PrimaryNavItemSpace {url} /> <PrimaryNavItemSpace {url} />
{:else} {:else}
<PrimaryNavItem title="Home" href="/home" class="tooltip-right"> <PrimaryNavItem title="Home" href="/home" class="tooltip-right">
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" /> <ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" />
</PrimaryNavItem> </PrimaryNavItem>
<Divider /> <Divider />
{#each primarySpaceUrls as url (url)} {#each primarySpaceUrls as url (url)}
@@ -78,11 +74,11 @@
class="tooltip-right" class="tooltip-right"
onclick={showOtherSpacesMenu} onclick={showOtherSpacesMenu}
notification={otherSpaceNotifications}> notification={otherSpaceNotifications}>
<Avatar icon={Widget} class="!h-10 !w-10" /> <ImageIcon alt="Other Spaces" src={Widget} />
</PrimaryNavItem> </PrimaryNavItem>
{/if} {/if}
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right"> <PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
<Avatar icon={Compass} class="!h-10 !w-10" /> <ImageIcon alt="Add a Space" src={Compass} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
{/each} {/each}
</div> </div>
@@ -95,17 +91,17 @@
href="/settings/profile" href="/settings/profile"
prefix="/settings" prefix="/settings"
class="tooltip-right"> class="tooltip-right">
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" /> <ImageIcon alt="Settings" src={$userProfile?.picture || UserRounded} class="rounded-full" />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem <PrimaryNavItem
title="Messages" title="Messages"
onclick={openChat} onclick={openChat}
class="tooltip-right" class="tooltip-right"
notification={$notifications.has("/chat")}> notification={$notifications.has("/chat")}>
<Avatar icon={Letter} class="!h-10 !w-10" /> <ImageIcon alt="Messages" src={Letter} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem title="Search" href="/people" class="tooltip-right"> <PrimaryNavItem title="Search" href="/people" class="tooltip-right">
<Avatar icon={Magnifier} class="!h-10 !w-10" /> <ImageIcon alt="Search" src={Magnifier} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
</div> </div>
</div> </div>
@@ -114,31 +110,34 @@
{@render children?.()} {@render children?.()}
<!-- a little extra something for ios --> <!-- a little extra something for ios -->
<div class="fixed bottom-0 left-0 right-0 z-nav h-[var(--saib)] bg-base-100 md:hidden"></div>
<div <div
class="border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden"> class="bottom-nav hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-[var(--saib)] bg-base-100 md:hidden">
</div>
<div
class="bottom-nav hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
<div class="content-padding-x content-sizing flex justify-between px-2"> <div class="content-padding-x content-sizing flex justify-between px-2">
<div class="flex gap-2 sm:gap-8"> <div class="flex gap-2 sm:gap-6">
<PrimaryNavItem title="Home" href="/home"> <PrimaryNavItem title="Home" href="/home">
<Avatar icon={HomeSmile} class="!h-10 !w-10" /> <ImageIcon alt="Home" src={HomeSmile} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem <PrimaryNavItem
title="Messages" title="Messages"
onclick={openChat} onclick={openChat}
notification={$notifications.has("/chat")}> notification={$notifications.has("/chat")}>
<Avatar icon={Letter} class="!h-10 !w-10" /> <ImageIcon alt="Messages" src={Letter} size={7} />
</PrimaryNavItem> </PrimaryNavItem>
{#if PLATFORM_RELAYS.length !== 1} {#if PLATFORM_RELAYS.length !== 1}
<PrimaryNavItem <PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
title="Spaces" <ImageIcon alt="Spaces" src={SettingsMinimalistic} size={7} />
onclick={showSpacesMenu}
notification={anySpaceNotifications}>
<Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
{/if} {/if}
</div> </div>
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}> <PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
<Avatar icon={Settings} src={$userProfile?.picture} class="!h-10 !w-10" /> <ImageIcon
alt="Settings"
src={$userProfile?.picture || Settings}
size={7}
class="rounded-full" />
</PrimaryNavItem> </PrimaryNavItem>
</div> </div>
</div> </div>
+9 -10
View File
@@ -1,24 +1,23 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte" import RelayIcon from "@app/components/RelayIcon.svelte"
import {encodeRelay} from "@app/core/state" import {makeSpacePath, goToSpace} from "@app/util/routes"
import {makeSpacePath} from "@app/util/routes"
import {lastPageBySpaceUrl} from "@app/util/history"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
const {url} = $props() type Props = {
url: string
}
const path = makeSpacePath(url) const {url}: Props = $props()
const onClick = () => goto(lastPageBySpaceUrl.get(encodeRelay(url)) || path) const onClick = () => goToSpace(url)
</script> </script>
<PrimaryNavItem <PrimaryNavItem
onclick={onClick} onclick={onClick}
title={displayRelayUrl(url)} title={displayRelayUrl(url)}
class="tooltip-right" class="tooltip-right"
notification={$notifications.has(path)}> notification={$notifications.has(makeSpacePath(url))}>
<SpaceAvatar {url} /> <RelayIcon {url} size={7} class="rounded-full" />
</PrimaryNavItem> </PrimaryNavItem>
+5 -11
View File
@@ -1,16 +1,11 @@
<script lang="ts"> <script lang="ts">
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {displayPubkey} from "@welshman/util" import {displayPubkey} from "@welshman/util"
import { import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
deriveHandleForPubkey,
displayHandle,
deriveProfile,
deriveProfileDisplay,
} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import WotScore from "@app/components/WotScore.svelte" import WotScore from "@app/components/WotScore.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
@@ -26,8 +21,7 @@
const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props() const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props()
const relays = removeNil([url]) const relays = removeUndefined([url])
const profile = deriveProfile(pubkey, relays)
const profileDisplay = deriveProfileDisplay(pubkey, relays) const profileDisplay = deriveProfileDisplay(pubkey, relays)
const handle = deriveHandleForPubkey(pubkey) const handle = deriveHandleForPubkey(pubkey)
@@ -38,7 +32,7 @@
<div class="flex max-w-full items-start gap-3"> <div class="flex max-w-full items-start gap-3">
<Button onclick={openProfile} class="py-1"> <Button onclick={openProfile} class="py-1">
<Avatar src={$profile?.picture} size={avatarSize} /> <ProfileCircle {pubkey} size={avatarSize} />
</Button> </Button>
<div class="flex min-w-0 flex-col"> <div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
+7 -11
View File
@@ -3,17 +3,13 @@
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import type {Filter} from "@welshman/util" import type {Filter} from "@welshman/util"
import {deriveEvents} from "@welshman/store" import {deriveArray, deriveEventsById} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {NOTE, ROOMS, COMMENT} from "@welshman/util" import {NOTE, ROOMS, COMMENT} from "@welshman/util"
import {repository, loadRelaySelections} from "@welshman/app" import {repository, loadRelayList} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileSpaces from "@app/components/ProfileSpaces.svelte" import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
import { import {deriveGroupList, getSpaceUrlsFromGroupList, MESSAGE_KINDS} from "@app/core/state"
deriveGroupSelections,
getSpaceUrlsFromGroupSelections,
MESSAGE_KINDS,
} from "@app/core/state"
import {goToEvent} from "@app/util/routes" import {goToEvent} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
@@ -24,9 +20,9 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const filters: Filter[] = [{authors: [pubkey], limit: 1}] const filters: Filter[] = [{authors: [pubkey], limit: 1}]
const events = deriveEvents(repository, {filters}) const events = deriveArray(deriveEventsById({repository, filters}))
const selections = deriveGroupSelections(pubkey) const groupList = deriveGroupList(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupSelections($selections)) const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
const viewEvent = () => goToEvent($events[0]!) const viewEvent = () => goToEvent($events[0]!)
@@ -34,7 +30,7 @@
onMount(async () => { onMount(async () => {
// Make sure we have their relay selections before we load their posts // Make sure we have their relay selections before we load their posts
await loadRelaySelections(pubkey) await loadRelayList(pubkey)
// Load groups and at least one note, regardless of time frame // Load groups and at least one note, regardless of time frame
load({ load({
+14 -7
View File
@@ -1,17 +1,24 @@
<script lang="ts"> <script lang="ts">
import Avatar from "@lib/components/Avatar.svelte" import cx from "classnames"
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {deriveProfile} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import UserCircle from "@assets/icons/user-circle.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = { type Props = {
pubkey: string pubkey: string
class?: string
size?: number
url?: string url?: string
} & Record<string, any> }
const {pubkey, url, ...props}: Props = $props() const {pubkey, url, size = 7, ...props}: Props = $props()
const profile = deriveProfile(pubkey, removeNil([url])) const profile = deriveProfile(pubkey, removeUndefined([url]))
</script> </script>
<Avatar src={$profile?.picture} icon={UserCircle} {...props} /> <ImageIcon
{size}
alt=""
class={cx(props.class, "rounded-full")}
src={$profile?.picture || UserRounded} />
+12 -3
View File
@@ -1,13 +1,22 @@
<script lang="ts"> <script lang="ts">
import {getProfile} from "@welshman/app"
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
const {...props} = $props() type Props = {
pubkeys: string[]
size?: number
}
const {pubkeys, size = 7}: Props = $props()
</script> </script>
<div class="flex pr-3"> <div class="flex pr-3">
{#each props.pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)} {#each pubkeys
.filter(p => getProfile(p)?.picture)
.toSorted()
.slice(0, 15) as pubkey (pubkey)}
<div class="z-feature -mr-3 inline-block"> <div class="z-feature -mr-3 inline-block">
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {...props} /> <ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
</div> </div>
{/each} {/each}
</div> </div>
+4 -2
View File
@@ -7,8 +7,9 @@
DELETE, DELETE,
isReplaceable, isReplaceable,
getAddress, getAddress,
RelayMode,
} from "@welshman/util" } from "@welshman/util"
import {pubkey, publishThunk, repository} from "@welshman/app" import {pubkey, publishThunk, repository, derivePubkeyRelays} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -19,12 +20,13 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {logout} from "@app/core/commands" import {logout} from "@app/core/commands"
import {INDEXER_RELAYS, PLATFORM_NAME, userSpaceUrls, userWriteRelays} from "@app/core/state" import {INDEXER_RELAYS, PLATFORM_NAME, userSpaceUrls} from "@app/core/state"
let progress: number | undefined = $state(undefined) let progress: number | undefined = $state(undefined)
let confirmText = $state("") let confirmText = $state("")
const CONFIRM_TEXT = "permanently delete my nostr account" const CONFIRM_TEXT = "permanently delete my nostr account"
const userWriteRelays = derivePubkeyRelays($pubkey!, RelayMode.Write)
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT) const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
const showProgress = $derived(progress !== undefined) const showProgress = $derived(progress !== undefined)
+83 -5
View File
@@ -1,19 +1,29 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {shouldUnwrap} from "@welshman/app" import {removeUndefined} from "@welshman/lib"
import {ManagementMethod} from "@welshman/util"
import {shouldUnwrap, manageRelay, deriveProfile, displayProfileByPubkey} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl" import Letter from "@assets/icons/letter-opened.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Avatar from "@lib/components/Avatar.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import Profile from "@app/components/Profile.svelte" import Profile from "@app/components/Profile.svelte"
import ProfileInfo from "@app/components/ProfileInfo.svelte" import ProfileInfo from "@app/components/ProfileInfo.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import ProfileBadges from "@app/components/ProfileBadges.svelte" import ProfileBadges from "@app/components/ProfileBadges.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte" import ChatEnable from "@app/components/ChatEnable.svelte"
import {pubkeyLink} from "@app/core/state" import {pubkeyLink, deriveUserIsSpaceAdmin} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {makeChatPath} from "@app/util/routes" import {makeChatPath} from "@app/util/routes"
export type Props = { export type Props = {
@@ -23,15 +33,83 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profile = deriveProfile(pubkey, removeUndefined([url]))
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const back = () => history.back() const back = () => history.back()
const chatPath = makeChatPath([pubkey]) const chatPath = makeChatPath([pubkey])
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
const openChat = () => ($shouldUnwrap ? goto(chatPath) : pushModal(ChatEnable, {next: chatPath})) const openChat = () => ($shouldUnwrap ? goto(chatPath) : pushModal(ChatEnable, {next: chatPath}))
const toggleMenu = (pubkey: string) => {
showMenu = !showMenu
}
const closeMenu = () => {
showMenu = false
}
const banMember = () =>
pushModal(Confirm, {
title: "Ban User",
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => {
const {error} = await manageRelay(url!, {
method: ManagementMethod.BanPubkey,
params: [pubkey],
})
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "User has successfully been banned!"})
back()
}
},
})
let showMenu = $state(false)
</script> </script>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<Profile showPubkey avatarSize={14} {pubkey} {url} /> <div class="flex justify-between">
<Profile showPubkey avatarSize={14} {pubkey} {url} />
{#if $profile || $userIsAdmin}
<div class="relative">
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
<Icon icon={MenuDots} />
</Button>
{#if showMenu}
<Popover hideOnClick onClose={closeMenu}>
<ul
transition:fly
class="bg-alt menu absolute right-0 z-popover w-48 gap-1 rounded-box p-2 shadow-md">
{#if $profile}
<li>
<Button onclick={showInfo}>
<Icon icon={Code2} />
User Details
</Button>
</li>
{/if}
{#if $userIsAdmin}
<li>
<Button class="text-error" onclick={banMember}>
<Icon icon={MinusCircle} />
Ban User
</Button>
</li>
{/if}
</ul>
</Popover>
{/if}
</div>
{/if}
</div>
<ProfileInfo {pubkey} {url} /> <ProfileInfo {pubkey} {url} />
<ProfileBadges {pubkey} {url} /> <ProfileBadges {pubkey} {url} />
<ModalFooter> <ModalFooter>
@@ -41,7 +119,7 @@
</Button> </Button>
<div class="flex gap-2"> <div class="flex gap-2">
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral"> <Link external href={pubkeyLink(pubkey)} class="btn btn-neutral">
<Avatar src="/coracle.png" /> <ImageIcon alt="" src="/coracle.png" />
Open in Coracle Open in Coracle
</Link> </Link>
<Button onclick={openChat} class="btn btn-primary"> <Button onclick={openChat} class="btn btn-primary">
+1 -1
View File
@@ -32,7 +32,7 @@
} }
success = true success = true
pushToast({message: "Success! Please check your inbox and continue when you're ready."}) pushToast({message: "Success! Please check your messages and continue when you're ready."})
await logout() await logout()
} finally { } finally {
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {deriveProfile} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
@@ -10,7 +10,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profile = deriveProfile(pubkey, removeNil([url])) const profile = deriveProfile(pubkey, removeUndefined([url]))
</script> </script>
{#if $profile} {#if $profile}
+6 -8
View File
@@ -2,8 +2,6 @@
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {NOTE} from "@welshman/util" import {NOTE} from "@welshman/util"
import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte" import NoteItem from "@app/components/NoteItem.svelte"
interface Props { interface Props {
@@ -24,16 +22,16 @@
<div class="col-4"> <div class="col-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#await events} {#await events}
<p class="center my-12 flex"> <p class="center flex min-h-6">
<Spinner loading /> <span class="loading loading-spinner"></span>
</p> </p>
{:then events} {:then events}
{#each events as event (event.id)} {#each events as event (event.id)}
<div in:fly> <NoteItem {url} {event} />
<NoteItem {url} {event} />
</div>
{:else} {:else}
{@render fallback?.()} <div class="min-h-6">
{@render fallback?.()}
</div>
{/each} {/each}
{/await} {/await}
</div> </div>
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {removeNil} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {deriveProfileDisplay} from "@welshman/app" import {deriveProfileDisplay} from "@welshman/app"
type Props = { type Props = {
@@ -9,7 +9,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profileDisplay = deriveProfileDisplay(pubkey, removeNil([url])) const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
</script> </script>
{$profileDisplay} {$profileDisplay}
+5 -5
View File
@@ -5,10 +5,10 @@
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte" import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte" import RelayName from "@app/components/RelayName.svelte"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {deriveGroupSelections, getSpaceUrlsFromGroupSelections} from "@app/core/state" import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/core/state"
type Props = { type Props = {
pubkey: string pubkey: string
@@ -16,8 +16,8 @@
const {pubkey}: Props = $props() const {pubkey}: Props = $props()
const selections = deriveGroupSelections(pubkey) const groupList = deriveGroupList(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupSelections($selections)) const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
const back = () => history.back() const back = () => history.back()
</script> </script>
@@ -26,7 +26,7 @@
{#each spaceUrls as url (url)} {#each spaceUrls as url (url)}
<div class="card2 bg-alt flex flex-row items-center gap-2"> <div class="card2 bg-alt flex flex-row items-center gap-2">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<SpaceAvatar {url} /> <RelayIcon {url} size={12} />
</div> </div>
<div class="flex flex-grow flex-col"> <div class="flex flex-grow flex-col">
<RelayName {url} /> <RelayName {url} />
+40 -26
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames"
import {onMount} from "svelte" import {onMount} from "svelte"
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib" import {groupBy, map, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
import { import {
REPORT, REPORT,
REACTION, REACTION,
@@ -14,14 +15,14 @@
DELETE, DELETE,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, EventContent, Zap} from "@welshman/util" import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
import {deriveEvents, deriveEventsMapped} from "@welshman/store" import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app" import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
import {isMobile, preventDefault, stopPropagation} from "@lib/html" import {isMobile, preventDefault, stopPropagation} from "@lib/html"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Reaction from "@app/components/Reaction.svelte" import Reaction from "@app/components/Reaction.svelte"
import EventReportDetails from "@app/components/EventReportDetails.svelte" import ReportDetails from "@app/components/ReportDetails.svelte"
import {REACTION_KINDS} from "@app/core/state" import {REACTION_KINDS} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
@@ -45,19 +46,22 @@
children, children,
}: Props = $props() }: Props = $props()
const reports = deriveEvents(repository, { const reports = deriveArray(
filters: [{kinds: [REPORT], "#e": [event.id]}], deriveEventsById({repository, filters: [{kinds: [REPORT], "#e": [event.id]}]}),
}) )
const reactions = deriveEvents(repository, { const reactions = deriveArray(
filters: [{kinds: [REACTION], "#e": [event.id]}], deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": [event.id]}]}),
}) )
const zaps = deriveEventsMapped<Zap>(repository, { const zaps = deriveArray(
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}], deriveItemsByKey<Zap>({
itemToEvent: item => item.response, repository,
eventToItem: (response: TrustedEvent) => getValidZap(response, event), getKey: zap => zap.response.id,
}) filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
}),
)
const onReactionClick = (events: TrustedEvent[]) => { const onReactionClick = (events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey) const reaction = events.find(e => e.pubkey === $pubkey)
@@ -74,20 +78,20 @@
} }
} }
const onReportClick = () => pushModal(EventReportDetails, {url, event}) const onReportClick = () => pushModal(ReportDetails, {url, event})
const reportReasons = $derived(uniq($reports.map(e => getTag("e", e.tags)?.[2]))) const reportReasons = $derived(uniq(map(e => getTag("e", e.tags)?.[2], $reports.values())))
const getReactionKey = (e: TrustedEvent) => getEmojiTag(e.content, e.tags)?.join("") || e.content const getReactionKey = (e: TrustedEvent) => getEmojiTag(e.content, e.tags)?.join("") || e.content
const groupedReactions = $derived( const groupedReactions = $derived(
groupBy( groupBy(
getReactionKey, getReactionKey,
uniqBy(e => `${e.pubkey}${getReactionKey(e)}`, $reactions), uniqBy(e => `${e.pubkey}${getReactionKey(e)}`, $reactions.values()),
), ),
) )
const groupedZaps = $derived(groupBy(e => getReactionKey(e.request), $zaps)) const groupedZaps = $derived(groupBy(e => getReactionKey(e.request), $zaps.values()))
onMount(() => { onMount(() => {
const controller = new AbortController() const controller = new AbortController()
@@ -134,10 +138,15 @@
<button <button
type="button" type="button"
data-tip={tooltip} data-tip={tooltip}
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full text-xs font-normal {reactionClass}" class={cx(
class:tooltip={!noTooltip && !isMobile} reactionClass,
class:btn-neutral={!isOwn} "flex-inline btn btn-outline btn-neutral btn-xs flex items-center gap-1 rounded-full text-xs font-normal",
class:btn-primary={isOwn}> {
tooltip: !noTooltip && !isMobile,
"border-neutral-content/20": !isOwn,
"btn-primary": isOwn,
},
)}>
<Reaction event={zaps[0].request} /> <Reaction event={zaps[0].request} />
<span>{amount}</span> <span>{amount}</span>
</button> </button>
@@ -151,10 +160,15 @@
<button <button
type="button" type="button"
data-tip={tooltip} data-tip={tooltip}
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full font-normal {reactionClass}" class={cx(
class:tooltip={!noTooltip && !isMobile} reactionClass,
class:btn-neutral={!isOwn} "flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal",
class:btn-primary={isOwn} {
tooltip: !noTooltip && !isMobile,
"border-neutral-content/20": !isOwn,
"btn-primary": isOwn,
},
)}
onclick={stopPropagation(preventDefault(onClick))}> onclick={stopPropagation(preventDefault(onClick))}>
<Reaction event={events[0]} /> <Reaction event={events[0]} />
{#if events.length > 1} {#if events.length > 1}
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import {deriveRelay} from "@welshman/app"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = {
url: string
size?: number
class?: string
}
const {url, size = 7, ...props}: Props = $props()
const relay = deriveRelay(url)
</script>
<ImageIcon {size} alt="" src={$relay?.icon || RemoteControllerMinimalistic} class={props.class} />
+5 -1
View File
@@ -1,7 +1,11 @@
<script lang="ts"> <script lang="ts">
import {deriveRelayDisplay} from "@welshman/app" import {deriveRelayDisplay} from "@welshman/app"
const {url} = $props() type Props = {
url: string
}
const {url}: Props = $props()
const display = $derived(deriveRelayDisplay(url)) const display = $derived(deriveRelayDisplay(url))
</script> </script>
+7 -13
View File
@@ -1,21 +1,19 @@
<script lang="ts"> <script lang="ts">
import {deriveRelay} from "@welshman/app"
import Ghost from "@assets/icons/ghost-smile.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl" import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import RelayName from "@app/components/RelayName.svelte" import RelayName from "@app/components/RelayName.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte" import RelayDescription from "@app/components/RelayDescription.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte" import ProfileCircles from "@app/components/ProfileCircles.svelte"
import {deriveSpaceMembers, deriveUserRooms} from "@app/core/state" import {deriveGroupListPubkeys, deriveUserRooms} from "@app/core/state"
type Props = { type Props = {
url: string url: string
} }
const {url}: Props = $props() const {url}: Props = $props()
const relay = deriveRelay(url)
const rooms = deriveUserRooms(url) const rooms = deriveUserRooms(url)
const members = deriveSpaceMembers(url) const favorited = deriveGroupListPubkeys(url)
</script> </script>
<div class="col-4 text-left"> <div class="col-4 text-left">
@@ -25,11 +23,7 @@
<div class="avatar relative"> <div class="avatar relative">
<div <div
class="center !flex h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300"> class="center !flex h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
{#if $relay?.icon} <RelayIcon {url} />
<img alt="" src={$relay.icon} />
{:else}
<Icon icon={Ghost} size={5} />
{/if}
</div> </div>
</div> </div>
{#if $rooms.includes(url)} {#if $rooms.includes(url)}
@@ -49,10 +43,10 @@
</div> </div>
<RelayDescription {url} /> <RelayDescription {url} />
</div> </div>
{#if $members.length > 0} {#if $favorited.size > 0}
<div class="row-2 card2 card2-sm bg-alt"> <div class="row-2 card2 card2-sm bg-alt">
Members: Favorited By:
<ProfileCircles pubkeys={$members} /> <ProfileCircles pubkeys={Array.from($favorited)} />
</div> </div>
{/if} {/if}
</div> </div>
+46
View File
@@ -0,0 +1,46 @@
<script lang="ts">
import {REPORT} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {deriveEventsById} from "@welshman/store"
import {repository} from "@welshman/app"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import Button from "@lib/components/Button.svelte"
import ReportItem from "@app/components/ReportItem.svelte"
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
const reports = deriveEventsById({
repository,
filters: [{kinds: [REPORT], "#e": [event.id]}],
})
const back = () => history.back()
const onDelete = () => {
if ($reports.size === 0) {
back()
}
}
</script>
<div class="column gap-4">
<ModalHeader>
{#snippet title()}
<div>Report Details</div>
{/snippet}
{#snippet info()}
<div>All reports for this event are shown below.</div>
{/snippet}
</ModalHeader>
{#each $reports.values() as report (report.id)}
<div class="card2 card2-sm bg-alt">
<ReportItem {url} event={report} {onDelete} />
</div>
{/each}
<Button class="btn btn-primary" onclick={back}>Got it</Button>
</div>
+93
View File
@@ -0,0 +1,93 @@
<script lang="ts">
import {formatTimestamp} from "@welshman/lib"
import {getTag, getIdFilters} from "@welshman/util"
import {load, LOCAL_RELAY_URL} from "@welshman/net"
import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import ReportMenu from "@app/components/ReportMenu.svelte"
import {publishDelete, canEnforceNip70} from "@app/core/commands"
import {pushModal} from "@app/util/modal"
import {goToEvent} from "@app/util/routes"
type Props = {
url: string
event: TrustedEvent
onDelete?: () => void
}
const {url, event, onDelete}: Props = $props()
const etag = getTag("e", event.tags)
const ptag = getTag("p", event.tags)
const reason = etag?.[2] || ptag?.[2]
const shouldProtect = canEnforceNip70(url)
const onClick = (e: Event, event: TrustedEvent) => {
// @ts-ignore
if (e.target?.classList.contains("profile-name")) {
pushModal(ProfileDetail, {pubkey: event.pubkey, url})
} else {
goToEvent(event)
}
}
const deleteReport = async () => {
publishDelete({event, relays: [url], protect: await shouldProtect})
onDelete?.()
}
</script>
<div class="column gap-4">
<div class="flex justify-between">
<div>
<Profile pubkey={event.pubkey} {url} avatarSize={5} />
<span>
Reported this event
{#if reason}
as "{reason}"
{/if}
</span>
</div>
{#if event.pubkey === $pubkey}
<Button class="btn-default btn" onclick={deleteReport}>Delete Report</Button>
{:else}
<ReportMenu {url} {event} />
{/if}
</div>
{#if event.content}
<div class="border-l-2 border-primary pl-3">
<NoteContent {event} />
</div>
{/if}
<div class="card2 card2-sm bg-alt">
{#if etag}
{#await load({relays: [url, LOCAL_RELAY_URL], filters: getIdFilters([etag[1]])})}
<p>Loading</p>
{:then reportedEvents}
{#if reportedEvents.length === 0}
<p>Unable to find reported note.</p>
{:else}
{@const event = reportedEvents[0]}
<Button class="col-2 w-full" onclick={(e: Event) => onClick(e, event)}>
<div class="flex items-center justify-between gap-2">
<span class="profile-name">
@<ProfileName pubkey={event.pubkey} {url} />
</span>
<span class="text-xs opacity-75">
{formatTimestamp(event.created_at)}
</span>
</div>
<NoteContent {event} />
</Button>
{/if}
{/await}
{:else if ptag}
<Profile pubkey={ptag[1]} />
{/if}
</div>
</div>
+132
View File
@@ -0,0 +1,132 @@
<script lang="ts">
import {getTag, ManagementMethod} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {manageRelay, repository, displayProfileByPubkey} from "@welshman/app"
import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Popover from "@lib/components/Popover.svelte"
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal"
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
const etag = getTag("e", event.tags)
const ptag = getTag("p", event.tags)
const toggleMenu = () => {
isOpen = !isOpen
}
const closeMenu = () => {
isOpen = false
}
const dismissReport = async () => {
const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent,
params: [event.id, "Dismissed by admin"],
})
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "Content has successfully been deleted!"})
repository.removeEvent(event.id)
history.back()
}
}
const banContent = () => {
const [_, id, reason = ""] = etag!
pushModal(Confirm, {
title: `Delete Content`,
message: `Are you sure you want to delete this content from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent,
params: [id, reason],
})
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "Content has successfully been deleted!"})
repository.removeEvent(id)
history.back()
}
},
})
}
const banMember = () => {
const [pubkey, reason = ""] = ptag!
pushModal(Confirm, {
title: "Ban User",
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
method: ManagementMethod.BanPubkey,
params: [pubkey, reason],
})
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "User has successfully been banned!"})
history.back()
}
},
})
}
let isOpen = $state(false)
</script>
<div class="relative">
<Button class="btn btn-circle btn-ghost btn-sm" onclick={toggleMenu}>
<Icon icon={MenuDots} />
</Button>
{#if isOpen}
<Popover hideOnClick onClose={closeMenu}>
<ul
transition:fly
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
<li>
<Button onclick={dismissReport}>
<Icon icon={InboxOut} />
Dismiss Report
</Button>
</li>
{#if etag}
<li>
<Button class="text-error" onclick={banContent}>
<Icon icon={TrashBin2} />
Remove Content
</Button>
</li>
{/if}
{#if ptag}
<li>
<Button class="text-error" onclick={banMember}>
<Icon icon={MinusCircle} />
Ban User
</Button>
</li>
{/if}
</ul>
</Popover>
{/if}
</div>
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
import Lock from "@assets/icons/lock.svg?dataurl"
import Microphone from "@assets/icons/microphone.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {deriveRoom} from "@app/core/state"
interface Props {
h: any
url: any
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
</script>
{#if $room.isHidden}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="This room is not visible to non-members.">
<Icon size={4} icon={EyeClosed} />
</Button>
{:else if $room.isPrivate}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Only members can view messages.">
<Icon size={4} icon={Lock} />
</Button>
{:else if $room.isRestricted}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Only members can send messages.">
<Icon size={4} icon={Microphone} />
</Button>
{/if}
@@ -16,13 +16,14 @@
type Props = { type Props = {
url?: string url?: string
room?: string h?: string
content?: string content?: string
onEscape?: () => void
onEditPrevious?: () => void onEditPrevious?: () => void
onSubmit: (event: EventContent) => void onSubmit: (event: EventContent) => void
} }
const {url, room, content, onEditPrevious, onSubmit}: Props = $props() const {url, h, content, onEscape, onEditPrevious, onSubmit}: Props = $props()
const autofocus = !isMobile const autofocus = !isMobile
@@ -34,6 +35,10 @@
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "") editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
const handleKeyDown = async (event: KeyboardEvent) => { const handleKeyDown = async (event: KeyboardEvent) => {
if (event.key === "Escape") {
onEscape?.()
}
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) { if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
onEditPrevious?.() onEditPrevious?.()
} }
@@ -74,7 +79,7 @@
}) })
</script> </script>
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}> <form class="relative flex gap-2 p-2" onsubmit={preventDefault(submit)}>
<div class="join"> <div class="join">
<Button <Button
data-tip="Add an image" data-tip="Add an image"
@@ -90,7 +95,7 @@
<Tippy <Tippy
bind:popover bind:popover
component={ComposeMenu} component={ComposeMenu}
props={{url, room, onClick: hidePopover}} props={{url, h, onClick: hidePopover}}
params={{trigger: "manual", interactive: true}}> params={{trigger: "manual", interactive: true}}>
<Button <Button
data-tip="More options" data-tip="More options"
@@ -12,10 +12,10 @@
</script> </script>
<div <div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs" class="relative flex h-8 items-center justify-between border-l-2 border-solid border-primary bg-base-300 px-2 pr-7 text-xs"
transition:slide> transition:slide>
<p class="text-primary">Editing message</p> <p class="text-primary">Editing message</p>
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}> <Button onclick={clear} class="flex items-center">
<Icon icon={CloseCircle} /> <Icon icon={CloseCircle} />
</Button> </Button>
</div> </div>
+28 -162
View File
@@ -1,181 +1,47 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {uniqBy, nth} from "@welshman/lib" import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {deriveRelay, waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
import {preventDefault, compressFile} from "@lib/html"
import FieldInline from "@lib/components/FieldInline.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import IconPickerButton from "@lib/components/IconPickerButton.svelte" import RoomForm from "@app/components/RoomForm.svelte"
import {hasNip29, loadChannel} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {pushToast} from "@app/util/toast"
import {uploadFile} from "@app/core/commands"
const {url} = $props() const {url} = $props()
const room = makeRoomMeta()
const relay = deriveRelay(url)
const back = () => history.back() const back = () => history.back()
const tryCreate = async () => { const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, room.h))
room.tags = uniqBy(nth(0), [...room.tags, ["name", name]])
if (imageFile) {
const {error, result} = await uploadFile(imageFile)
if (error) {
return pushToast({theme: "error", message: error})
}
room.tags.push(["picture", result.url, ...result.tags])
} else if (selectedIcon) {
room.tags.push(["picture", selectedIcon])
}
const createMessage = await waitForThunkError(createRoom(url, room))
if (createMessage && !createMessage.match(/^duplicate:|already a member/)) {
return pushToast({theme: "error", message: createMessage})
}
const editMessage = await waitForThunkError(editRoom(url, room))
if (editMessage) {
return pushToast({theme: "error", message: editMessage})
}
const joinMessage = await waitForThunkError(joinRoom(url, room))
if (joinMessage && !joinMessage.includes("already")) {
return pushToast({theme: "error", message: joinMessage})
}
await loadChannel(url, room.id)
goto(makeSpacePath(url, room.id))
}
const create = async () => {
loading = true
try {
await tryCreate()
} finally {
loading = false
}
}
let name = $state("")
let loading = $state(false)
let imageFile = $state<File | undefined>()
let imagePreview = $state<string | undefined>()
let selectedIcon = $state<string | undefined>()
const handleImageUpload = async (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (file && file.type.startsWith("image/")) {
selectedIcon = undefined
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
const reader = new FileReader()
reader.onload = e => {
imagePreview = e.target?.result as string
}
reader.readAsDataURL(imageFile)
}
}
const handleIconSelect = (iconUrl: string) => {
imageFile = undefined
imagePreview = undefined
selectedIcon = iconUrl
}
</script> </script>
<form class="column gap-4" onsubmit={preventDefault(create)}> <RoomForm {url} {onsubmit}>
<ModalHeader> {#snippet header()}
{#snippet title()} <ModalHeader>
<div>Create a Room</div> {#snippet title()}
{/snippet} <div>Create a Room</div>
{#snippet info()}
<div>
On <span class="text-primary">{displayRelayUrl(url)}</span>
</div>
{/snippet}
</ModalHeader>
{#if hasNip29($relay)}
<FieldInline>
{#snippet label()}
<p>Room Name</p>
{/snippet} {/snippet}
{#snippet input()} {#snippet info()}
<label class="input input-bordered flex w-full items-center gap-2"> <div>
<Icon icon={Hashtag} /> On <span class="text-primary">{displayRelayUrl(url)}</span>
<input bind:value={name} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<div class="flex items-center justify-between">
<p class="font-bold">Room Icon</p>
<div class="flex items-center gap-4">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<img
src={imagePreview}
alt="Room icon preview"
class="h-8 w-8 rounded-lg object-cover" />
</div>
{:else if selectedIcon}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<Icon icon={selectedIcon} class="h-8 w-8" />
</div>
{:else}
<span class="text-sm opacity-75">No icon selected</span>
{/if}
<div class="flex gap-2">
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
<Icon icon={StickerSmileSquare} size={4} />
Select
</IconPickerButton>
<label class="btn btn-neutral btn-sm cursor-pointer">
<Icon icon={UploadMinimalistic} size={4} />
Upload
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
</label>
</div> </div>
</div> {/snippet}
</div> </ModalHeader>
{:else} {/snippet}
<p class="bg-alt card2 row-2"> {#snippet footer({loading})}
<Icon icon={Danger} /> <ModalFooter>
This relay does not support creating rooms. <Button class="btn btn-link" onclick={back}>
</p> <Icon icon={AltArrowLeft} />
{/if} Go back
<ModalFooter> </Button>
<Button class="btn btn-link" onclick={back}> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Icon icon={AltArrowLeft} /> <Spinner {loading}>Create Room</Spinner>
Go back <Icon icon={AltArrowRight} />
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={!name || loading || !hasNip29($relay)}> </ModalFooter>
<Spinner {loading}>Create Room</Spinner> {/snippet}
<Icon icon={AltArrowRight} /> </RoomForm>
</Button>
</ModalFooter>
</form>
+186
View File
@@ -0,0 +1,186 @@
<script lang="ts">
import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
import type {Thunk} from "@welshman/app"
import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
import Pen from "@assets/icons/pen.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Login3 from "@assets/icons/login-3.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import Lock from "@assets/icons/lock.svg?dataurl"
import Microphone from "@assets/icons/microphone.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import RoomMembers from "@app/components/RoomMembers.svelte"
import RoomEdit from "@app/components/RoomEdit.svelte"
import RoomName from "@app/components/RoomName.svelte"
import RoomImage from "@app/components/RoomImage.svelte"
import {
deriveRoom,
deriveRoomMembers,
deriveUserIsRoomAdmin,
deriveUserRoomMembershipStatus,
MembershipStatus,
} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
type Props = {
url: string
h: string
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
const members = deriveRoomMembers(url, h)
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
const back = () => history.back()
const startEdit = () => pushModal(RoomEdit, {url, h})
const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
loading = true
try {
const message = await waitForThunkError(f(url, makeRoomMeta({h})))
if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message})
}
} finally {
loading = false
}
}
const join = () => handleLoading(joinRoom)
const leave = () => handleLoading(leaveRoom)
const showMembers = () => pushModal(RoomMembers, {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))
}
},
})
let loading = $state(false)
</script>
<div class="flex flex-col gap-3">
<div class="flex justify-between">
<div class="flex gap-3">
<RoomImage {url} {h} size={8} />
<div class="flex min-w-0 flex-col">
<RoomName {url} {h} class="text-2xl" />
<span class="text-primary">{displayRelayUrl(url)}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
{#if $room?.isRestricted}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Only members can send messages.">
<Icon size={4} icon={Microphone} />
</Button>
{/if}
{#if $room?.isPrivate}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Only members can view messages.">
<Icon size={4} icon={Lock} />
</Button>
{/if}
{#if $room?.isHidden}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="This room is not visible to non-members.">
<Icon size={4} icon={EyeClosed} />
</Button>
{/if}
{#if $room?.isClosed}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Requests to join this room will be ignored.">
<Icon size={4} icon={MinusCircle} />
</Button>
{/if}
</div>
</div>
{#if $room?.about}
<p>{$room.about}</p>
{/if}
{#if $members.length > 0}
<div class="card2 card2-sm bg-alt flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<span>Members:</span>
<ProfileCircles pubkeys={$members} />
</div>
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
</div>
{/if}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<div class="flex gap-2">
{#if $userIsAdmin}
<Button class="btn btn-outline btn-error" onclick={startDelete}>
<Icon icon={TrashBin2} />
<span class="hidden md:inline">Delete Room</span>
</Button>
<Button class="btn btn-primary" onclick={startEdit}>
<Icon icon={Pen} />
Edit Room
</Button>
{:else if $membershipStatus === MembershipStatus.Initial}
<Button class="btn btn-neutral" disabled={loading} onclick={join}>
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon={Login3} />
{/if}
Join member list
</Button>
{:else if $membershipStatus === MembershipStatus.Pending}
<Button class="btn btn-neutral">
<Icon icon={ClockCircle} />
Membership pending
</Button>
{:else}
<Button class="btn btn-neutral" disabled={loading} onclick={leave}>
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon={Login3} />
{/if}
Leave member list
</Button>
{/if}
</div>
</ModalFooter>
</div>
+53
View File
@@ -0,0 +1,53 @@
<script lang="ts">
import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import RoomForm from "@app/components/RoomForm.svelte"
import {deriveRoom} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
type Props = {
url: string
h: string
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
const back = () => history.back()
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, h))
</script>
<RoomForm {url} {onsubmit} initialValues={$room}>
{#snippet header()}
<ModalHeader>
{#snippet title()}
<div>Edit a Room</div>
{/snippet}
{#snippet info()}
<div>
On <span class="text-primary">{displayRelayUrl(url)}</span>
</div>
{/snippet}
</ModalHeader>
{/snippet}
{#snippet footer({loading})}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Save Changes</Spinner>
</Button>
</ModalFooter>
{/snippet}
</RoomForm>
+202
View File
@@ -0,0 +1,202 @@
<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} 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, {maxWidth: 128, maxHeight: 128})
if (error) {
return pushToast({theme: "error", message: error})
}
room.picture = result.url
room.pictureMeta = result.tags
}
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)
const handleImageUpload = async (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (file && file.type.startsWith("image/")) {
const reader = new FileReader()
reader.onload = e => {
imageFile = file
imagePreview = e.target?.result as string
}
reader.readAsDataURL(file)
}
}
const handleIconSelect = (iconUrl: string) => {
imagePreview = iconUrl
const parts = iconUrl.split(",")
const imageData = atob(parts[1])
const result = new Uint8Array(imageData.length)
for (let n = 0; n < imageData.length; n++) {
result[n] = imageData.charCodeAt(n)
}
imageFile = new File([result], `icon.svg`, {type: "image/svg+xml"})
}
</script>
<form class="column gap-4" onsubmit={preventDefault(trySubmit)}>
{@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="" class="rounded-lg" />
</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="" class="rounded-lg" />
{:else}
<Icon icon={Hashtag} />
{/if}
<input bind:value={values.name} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Description</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<input bind:value={values.about} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Restricted</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
<span class="text-sm opacity-75">Only allow members to send messages</span>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Private</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
<span class="text-sm opacity-75">Only allow members to read messages</span>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Hidden</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isHidden} />
<span class="text-sm opacity-75">Hide this group from non-members</span>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Closed</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
<span class="text-sm opacity-75">Ignore requests to join</span>
{/snippet}
</FieldInline>
{@render footer({loading})}
</form>
+22
View File
@@ -0,0 +1,22 @@
<script lang="ts">
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import {deriveRoom} from "@app/core/state"
interface Props {
h: string
url: string
size?: number
}
const {url, h, size = 5}: Props = $props()
const room = deriveRoom(url, h)
</script>
{#if $room.picture}
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
{:else}
<Icon icon={Hashtag} {size} />
{/if}
@@ -7,7 +7,6 @@
thunks, thunks,
pubkey, pubkey,
mergeThunks, mergeThunks,
deriveProfile,
deriveProfileDisplay, deriveProfileDisplay,
displayProfileByPubkey, displayProfileByPubkey,
} from "@welshman/app" } from "@welshman/app"
@@ -16,21 +15,21 @@
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import ReplyAlt from "@assets/icons/reply.svg?dataurl" import ReplyAlt from "@assets/icons/reply.svg?dataurl"
import TapTarget from "@lib/components/TapTarget.svelte" import TapTarget from "@lib/components/TapTarget.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ThunkFailure from "@app/components/ThunkFailure.svelte" import ThunkFailure from "@app/components/ThunkFailure.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ChannelItemZapButton from "@app/components/ChannelItemZapButton.svelte" import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte"
import ChannelItemEmojiButton from "@app/components/ChannelItemEmojiButton.svelte" import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte"
import ChannelItemMenuButton from "@app/components/ChannelItemMenuButton.svelte" import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
import ChannelItemMenuMobile from "@app/components/ChannelItemMenuMobile.svelte" import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
import ChannelItemContent from "@app/components/ChannelItemContent.svelte" import RoomItemContent from "@app/components/RoomItemContent.svelte"
import {colors, ENABLE_ZAPS, deriveEventsForUrl} from "@app/core/state" import {colors, ENABLE_ZAPS, deriveEventsForUrl} from "@app/core/state"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands" import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
import {getChannelItemPath} from "@app/util/routes" import {getRoomItemPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
interface Props { interface Props {
@@ -53,10 +52,9 @@
onEdit, onEdit,
}: Props = $props() }: Props = $props()
const path = getChannelItemPath(url, event) const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const today = formatTimestampAsDate(now()) const today = formatTimestampAsDate(now())
const profile = deriveProfile(event.pubkey, [url])
const profileDisplay = deriveProfileDisplay(event.pubkey, [url]) const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id)) const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
@@ -65,7 +63,7 @@
const reply = () => replyTo!(event) const reply = () => replyTo!(event)
const edit = canEdit(event) ? () => onEdit(event) : undefined const edit = canEdit(event) ? () => onEdit(event) : undefined
const onTap = () => pushModal(ChannelItemMenuMobile, {url, event, reply, edit}) const onTap = () => pushModal(RoomItemMenuMobile, {url, event, reply, edit})
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
@@ -83,7 +81,10 @@
<div class="flex w-full gap-3 overflow-auto"> <div class="flex w-full gap-3 overflow-auto">
{#if showPubkey} {#if showPubkey}
<Button onclick={openProfile} class="flex items-start"> <Button onclick={openProfile} class="flex items-start">
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} /> <ProfileCircle
pubkey={event.pubkey}
class="border border-solid border-base-content"
size={8} />
</Button> </Button>
{:else} {:else}
<div class="w-8 min-w-8 max-w-8"></div> <div class="w-8 min-w-8 max-w-8"></div>
@@ -105,7 +106,7 @@
</div> </div>
{/if} {/if}
<div class:mt-2={showPubkey && event.kind !== MESSAGE}> <div class:mt-2={showPubkey && event.kind !== MESSAGE}>
<ChannelItemContent {url} {event} /> <RoomItemContent {url} {event} />
{#if thunk} {#if thunk}
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" /> <ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
{/if} {/if}
@@ -142,9 +143,9 @@
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all" class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
class:group-hover:opacity-100={!isMobile}> class:group-hover:opacity-100={!isMobile}>
{#if ENABLE_ZAPS} {#if ENABLE_ZAPS}
<ChannelItemZapButton {url} {event} /> <RoomItemZapButton {url} {event} />
{/if} {/if}
<ChannelItemEmojiButton {url} {event} /> <RoomItemEmojiButton {url} {event} />
{#if replyTo} {#if replyTo}
<Button class="btn join-item btn-xs" onclick={reply}> <Button class="btn join-item btn-xs" onclick={reply}>
<Icon icon={Reply} size={4} /> <Icon icon={Reply} size={4} />
@@ -155,7 +156,7 @@
<Icon icon={Pen} size={4} /> <Icon icon={Pen} size={4} />
</Button> </Button>
{/if} {/if}
<ChannelItemMenuButton {url} {event} /> <RoomItemMenuButton {url} {event} />
</button> </button>
{/if} {/if}
</TapTarget> </TapTarget>
@@ -5,11 +5,11 @@
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import NoteContent from "@app/components/NoteContent.svelte" import NoteContent from "@app/components/NoteContent.svelte"
import {getChannelItemPath} from "@app/util/routes" import {getRoomItemPath} from "@app/util/routes"
const props: ComponentProps<typeof NoteContent> = $props() const props: ComponentProps<typeof NoteContent> = $props()
const path = getChannelItemPath(props.url!, props.event) const path = getRoomItemPath(props.url!, props.event)
</script> </script>
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}> <div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
@@ -1,15 +1,19 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@welshman/app" import {ManagementMethod} from "@welshman/util"
import {pubkey, manageRelay, repository} from "@welshman/app"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl" import Danger from "@assets/icons/danger.svg?dataurl"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import EventInfo from "@app/components/EventInfo.svelte" import EventInfo from "@app/components/EventInfo.svelte"
import EventReport from "@app/components/EventReport.svelte" import Report from "@app/components/Report.svelte"
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte" import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {deriveUserIsSpaceAdmin} from "@app/core/state"
type Props = { type Props = {
url: string url: string
@@ -19,9 +23,11 @@
const {url, event, onClick}: Props = $props() const {url, event, onClick}: Props = $props()
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const report = () => { const report = () => {
onClick() onClick()
pushModal(EventReport, {url, event}) pushModal(Report, {url, event})
} }
const showInfo = () => { const showInfo = () => {
@@ -33,9 +39,29 @@
onClick() onClick()
pushModal(EventDeleteConfirm, {url, event}) pushModal(EventDeleteConfirm, {url, event})
} }
const showAdminDelete = () =>
pushModal(Confirm, {
title: `Delete Message`,
message: `Are you sure you want to delete this message from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent,
params: [event.id],
})
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "Event has successfully been deleted!"})
repository.removeEvent(event.id)
history.back()
}
},
})
</script> </script>
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl"> <ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md">
<li> <li>
<Button onclick={showInfo}> <Button onclick={showInfo}>
<Icon size={4} icon={Code2} /> <Icon size={4} icon={Code2} />
@@ -56,5 +82,13 @@
Report Content Report Content
</Button> </Button>
</li> </li>
{#if $userIsAdmin}
<li>
<Button class="text-error" onclick={showAdminDelete}>
<Icon size={4} icon={TrashBin2} />
Delete Message
</Button>
</li>
{/if}
{/if} {/if}
</ul> </ul>
@@ -5,7 +5,7 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Tippy from "@lib/components/Tippy.svelte" import Tippy from "@lib/components/Tippy.svelte"
import ChannelItemMenu from "@app/components/ChannelItemMenu.svelte" import RoomItemMenu from "@app/components/RoomItemMenu.svelte"
const {url, event} = $props() const {url, event} = $props()
@@ -34,7 +34,7 @@
</Button> </Button>
<Tippy <Tippy
bind:popover bind:popover
component={ChannelItemMenu} component={RoomItemMenu}
props={{url, event, onClick}} props={{url, event, onClick}}
params={{trigger: "manual", interactive: true}} /> params={{trigger: "manual", interactive: true}} />
</div> </div>
@@ -17,7 +17,7 @@
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte" import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import {ENABLE_ZAPS} from "@app/core/state" import {ENABLE_ZAPS} from "@app/core/state"
import {publishReaction, canEnforceNip70} from "@app/core/commands" import {publishReaction, canEnforceNip70} from "@app/core/commands"
import {getChannelItemPath} from "@app/util/routes" import {getRoomItemPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
type Props = { type Props = {
@@ -28,7 +28,7 @@
const {url, event, reply}: Props = $props() const {url, event, reply}: Props = $props()
const path = getChannelItemPath(url, event) const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -58,12 +58,12 @@
{#if event.pubkey === $pubkey} {#if event.pubkey === $pubkey}
<Button class="btn btn-neutral text-error" onclick={showDelete}> <Button class="btn btn-neutral text-error" onclick={showDelete}>
<Icon size={4} icon={TrashBin2} /> <Icon size={4} icon={TrashBin2} />
Delete Delete Message
</Button> </Button>
{/if} {/if}
<Button class="btn btn-neutral" onclick={showInfo}> <Button class="btn btn-neutral" onclick={showInfo}>
<Icon size={4} icon={Code2} /> <Icon size={4} icon={Code2} />
Show JSON Message Info
</Button> </Button>
{#if path} {#if path}
<Link class="btn btn-neutral" href={path}> <Link class="btn btn-neutral" href={path}>
@@ -71,18 +71,18 @@
View Details View Details
</Link> </Link>
{/if} {/if}
<Button class="btn btn-outline btn-neutral w-full" onclick={sendReply}>
<Icon size={4} icon={Reply} />
Reply
</Button>
<Button class="btn btn-secondary w-full" onclick={showEmojiPicker}>
<Icon size={4} icon={SmileCircle} />
React
</Button>
{#if ENABLE_ZAPS} {#if ENABLE_ZAPS}
<ZapButton replaceState {url} {event} class="btn btn-primary w-full"> <ZapButton replaceState {url} {event} class="btn btn-neutral w-full">
<Icon size={4} icon={Bolt} /> <Icon size={4} icon={Bolt} />
Zap Send Zap
</ZapButton> </ZapButton>
{/if} {/if}
<Button class="btn btn-neutral w-full" onclick={sendReply}>
<Icon size={4} icon={Reply} />
Send Reply
</Button>
<Button class="btn btn-neutral w-full" onclick={showEmojiPicker}>
<Icon size={4} icon={SmileCircle} />
Send Reaction
</Button>
</div> </div>
@@ -1,21 +1,21 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import RoomName from "@app/components/RoomName.svelte"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
type Props = { type Props = {
room: string h: string
url: string url: string
class?: string class?: string
unstyled?: boolean unstyled?: boolean
} }
const {room, url, unstyled, ...props}: Props = $props() const {h, url, unstyled, ...props}: Props = $props()
const path = makeSpacePath(url, room) const path = makeSpacePath(url, h)
</script> </script>
<Link href={path} class={cx(props.class, {"link-content bg-alt": !unstyled})}> <Link href={path} class={cx(props.class, {"link-content bg-alt": !unstyled})}>
#<ChannelName {room} {url} /> #<RoomName {h} {url} />
</Link> </Link>
+109
View File
@@ -0,0 +1,109 @@
<script lang="ts">
import {waitForThunkError, removeRoomMember} from "@welshman/app"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import {fly} from "@lib/transition"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Popover from "@lib/components/Popover.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Profile from "@app/components/Profile.svelte"
import RoomName from "@app/components/RoomName.svelte"
import RoomMembersAdd from "@app/components/RoomMembersAdd.svelte"
import {deriveRoom, deriveRoomMembers, deriveUserIsRoomAdmin} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
interface Props {
url: string
h: string
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
const members = deriveRoomMembers(url, h)
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
const back = () => history.back()
const toggleMenu = (pubkey: string) => {
menuPubkey = menuPubkey === pubkey ? undefined : pubkey
}
const closeMenu = () => {
menuPubkey = undefined
}
const addMember = () => pushModal(RoomMembersAdd, {url, h})
const removeMember = (pubkey: string) =>
pushModal(Confirm, {
title: "Remove Member",
message: "Are you sure you want to remove this user from the room?",
confirm: async () => {
const error = await waitForThunkError(removeRoomMember(url, $room, pubkey))
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "Member has successfully been removed!"})
back()
}
},
})
let menuPubkey = $state<string | undefined>()
</script>
<div class="column gap-4">
<div class="flex min-w-0 flex-col gap-1">
<h1 class="ellipsize whitespace-nowrap text-2xl font-bold">Members</h1>
<p class="ellipsize text-sm opacity-75">of <RoomName {url} {h} /></p>
</div>
{#if $userIsAdmin}
<div class="flex gap-2">
<Button class="btn btn-primary" onclick={addMember}>
<Icon icon={AddCircle} />
Add members
</Button>
</div>
{/if}
{#each $members as pubkey (pubkey)}
<div class="card2 bg-alt relative">
<div class="flex items-center justify-between gap-2">
<div class="min-w-0 flex-1">
<Profile {pubkey} {url} />
</div>
<div class="relative">
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
<Icon icon={MenuDots} />
</Button>
{#if menuPubkey === pubkey}
<Popover hideOnClick onClose={closeMenu}>
<ul
transition:fly
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
<li>
<Button class="text-error" onclick={() => removeMember(pubkey)}>
<Icon icon={MinusCircle} />
Remove Member
</Button>
</li>
</ul>
</Popover>
{/if}
</div>
</div>
</div>
{/each}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
</ModalFooter>
</div>
+77
View File
@@ -0,0 +1,77 @@
<script lang="ts">
import {addRoomMember, waitForThunkError} from "@welshman/app"
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 Field from "@lib/components/Field.svelte"
import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import RoomName from "@app/components/RoomName.svelte"
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
import {pushToast} from "@app/util/toast"
import {deriveRoom} from "@app/core/state"
interface Props {
url: string
h: string
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
const back = () => history.back()
const addMember = async () => {
loading = true
try {
const errors = await Promise.all(
pubkeys.map(pubkey => waitForThunkError(addRoomMember(url, $room, pubkey))),
)
for (const error of errors) {
if (error) {
return pushToast({theme: "error", message: errors[0]})
}
}
pushToast({message: "Members have successfully been added!"})
back()
} finally {
loading = false
}
}
let loading = $state(false)
let pubkeys: string[] = $state([])
</script>
<div class="column gap-4">
<ModalHeader>
{#snippet title()}
<div>Add Members</div>
{/snippet}
{#snippet info()}
<div>to <RoomName {url} {h} /></div>
{/snippet}
</ModalHeader>
<Field>
{#snippet label()}
<p>Search for People</p>
{/snippet}
{#snippet input()}
<ProfileMultiSelect bind:value={pubkeys} />
{/snippet}
</Field>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button class="btn btn-primary" onclick={addMember} disabled={loading}>
<Spinner {loading}>Save changes</Spinner>
</Button>
</ModalFooter>
</div>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import {deriveRoom} from "@app/core/state"
type Props = {
url: string
h: string
class?: string
}
const {url, h, ...props}: Props = $props()
const room = deriveRoom(url, h)
</script>
<span class="ellipsize {props.class}">
{$room?.name || h}
</span>
@@ -0,0 +1,21 @@
<script lang="ts">
import RoomName from "@app/components/RoomName.svelte"
import RoomImage from "@app/components/RoomImage.svelte"
interface Props {
h: string
url: string
class?: string
}
const {url, h, ...props}: Props = $props()
</script>
<div class="flex flex-grow items-center justify-between gap-4 {props.class}">
<div class="flex items-center gap-3">
<RoomImage {url} {h} />
<div class="min-w-0 overflow-hidden text-ellipsis">
<RoomName {url} {h} />
</div>
</div>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
import {nsecEncode} from "nostr-tools/nip19" import {nsecEncode} from "nostr-tools/nip19"
import {encrypt} from "nostr-tools/nip49" import {encrypt} from "nostr-tools/nip49"
import {hexToBytes} from "@welshman/lib" import {hexToBytes} from "@welshman/lib"
import {makeSecret} from "@welshman/signer" import {makeSecret} from "@welshman/util"
import type {Profile} from "@welshman/util" import type {Profile} from "@welshman/util"
import {preventDefault, downloadText} from "@lib/html" import {preventDefault, downloadText} from "@lib/html"
import Key from "@assets/icons/key-minimalistic.svg?dataurl" import Key from "@assets/icons/key-minimalistic.svg?dataurl"
+2 -2
View File
@@ -14,7 +14,7 @@
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte" import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {checkRelayAccess} from "@app/core/commands" import {attemptRelayAccess} from "@app/core/commands"
import {deriveSocket} from "@app/core/state" import {deriveSocket} from "@app/core/state"
type Props = { type Props = {
@@ -31,7 +31,7 @@
loading = true loading = true
try { try {
const message = await checkRelayAccess(url, claim) const message = await attemptRelayAccess(url, claim)
if (message) { if (message) {
return pushToast({theme: "error", message, timeout: 30_000}) return pushToast({theme: "error", message, timeout: 30_000})
+26 -6
View File
@@ -1,20 +1,25 @@
<script lang="ts"> <script lang="ts">
import Login from "@assets/icons/login-3.svg?dataurl" import Login from "@assets/icons/login-3.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl" import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import SpaceCreateExternal from "@app/components/SpaceCreateExternal.svelte"
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte" import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
const startCreate = () => pushModal(SpaceCreateExternal) type Props = {
hideDiscover?: boolean
}
const {hideDiscover}: Props = $props()
const startJoin = () => pushModal(SpaceInviteAccept) const startJoin = () => pushModal(SpaceInviteAccept)
</script> </script>
<div class="column gap-4"> <div class="column gap-2">
<ModalHeader> <ModalHeader>
{#snippet title()} {#snippet title()}
<div>Add a Space</div> <div>Add a Space</div>
@@ -23,8 +28,23 @@
<div>Spaces are places where communities come together to work, play, and hang out.</div> <div>Spaces are places where communities come together to work, play, and hang out.</div>
{/snippet} {/snippet}
</ModalHeader> </ModalHeader>
{#if !hideDiscover}
<Link href="/discover">
<CardButton class="btn-primary">
{#snippet icon()}
<div><Icon icon={Compass} size={7} /></div>
{/snippet}
{#snippet title()}
<div>Explore Spaces</div>
{/snippet}
{#snippet info()}
<div>Join create, or browse spaces</div>
{/snippet}
</CardButton>
</Link>
{/if}
<Button onclick={startJoin}> <Button onclick={startJoin}>
<CardButton class="btn-primary"> <CardButton class={hideDiscover ? "btn-primary" : "btn-neutral"}>
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Login} size={7} /></div> <div><Icon icon={Login} size={7} /></div>
{/snippet} {/snippet}
@@ -36,7 +56,7 @@
{/snippet} {/snippet}
</CardButton> </CardButton>
</Button> </Button>
<Button onclick={startCreate}> <Link href="/spaces/create">
<CardButton class="btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div> <div><Icon icon={AddCircle} size={7} /></div>
@@ -48,5 +68,5 @@
<div>Just a few questions and you'll be on your way.</div> <div>Just a few questions and you'll be on your way.</div>
{/snippet} {/snippet}
</CardButton> </CardButton>
</Button> </Link>
</div> </div>
+29 -6
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {parse, renderAsHtml} from "@welshman/content" import {parse, renderAsHtml} from "@welshman/content"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -11,12 +12,29 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte" import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {removeSpaceMembership, publishLeaveRequest, removeTrustedRelay} from "@app/core/commands"
const {url, error} = $props() const {url, error} = $props()
const back = () => history.back() const back = () => goto("/home")
const requestAccess = () => pushModal(SpaceAccessRequest, {url}) const requestAccess = () => pushModal(SpaceAccessRequest, {url})
const leaveSpace = async () => {
loading = true
try {
await removeSpaceMembership(url)
await publishLeaveRequest({url})
await removeTrustedRelay(url)
} finally {
loading = false
}
goto("/home")
}
let loading = $state(false)
</script> </script>
<form class="column gap-4" onsubmit={preventDefault(requestAccess)}> <form class="column gap-4" onsubmit={preventDefault(requestAccess)}>
@@ -37,11 +55,16 @@
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go Home
</Button>
<Button type="submit" class="btn btn-primary">
Request Access
<Icon icon={AltArrowRight} />
</Button> </Button>
<div class="flex gap-2">
<Button class="btn btn-outline btn-error" onclick={leaveSpace} disabled={loading}>
Leave Space
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
Request Access
<Icon icon={AltArrowRight} />
</Button>
</div>
</ModalFooter> </ModalFooter>
</form> </form>
-20
View File
@@ -1,20 +0,0 @@
<script lang="ts">
import {displayRelayUrl} from "@welshman/util"
import Avatar from "@lib/components/Avatar.svelte"
import {deriveRelay} from "@welshman/app"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
interface Props {
url?: string
}
const {url = ""}: Props = $props()
const relay = deriveRelay(url)
</script>
<Avatar
icon={RemoteControllerMinimalistic}
class="!h-10 !w-10"
alt={displayRelayUrl(url)}
src={$relay?.icon} />
+15 -8
View File
@@ -8,22 +8,26 @@
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import SpaceVisitConfirm, {confirmSpaceVisit} from "@app/components/SpaceVisitConfirm.svelte" import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
import {attemptRelayAccess} from "@app/core/commands" import {attemptRelayAccess} from "@app/core/commands"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
const {url} = $props() const {url} = $props()
const back = () => history.back() const back = () => history.back()
const next = () => { const next = async () => {
if (!error && Pool.get().get(url).auth.status === AuthStatus.None) { if (error) {
pushModal(SpaceVisitConfirm, {url}, {replaceState: true}) return pushToast({theme: "error", message: error, timeout: 30_000})
}
if (Pool.get().get(url).auth.status === AuthStatus.None) {
pushModal(SpaceJoinConfirm, {url}, {replaceState: true})
} else { } else {
confirmSpaceVisit(url) await confirmSpaceJoin(url)
} }
} }
@@ -49,7 +53,10 @@
</ModalHeader> </ModalHeader>
<div class="m-auto flex flex-col gap-4"> <div class="m-auto flex flex-col gap-4">
{#if loading} {#if loading}
<Spinner loading>Hold tight, we're checking your connection.</Spinner> <p class="flex items-center gap-3">
<span class="loading loading-spinner"></span>
Hold tight, we're checking your connection.
</p>
{:else if error} {:else if error}
<p>Oops! We ran into some problems:</p> <p>Oops! We ran into some problems:</p>
<p class="card2 bg-alt">{error}</p> <p class="card2 bg-alt">{error}</p>
@@ -70,7 +77,7 @@
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading}> <Button type="submit" class="btn btn-primary" disabled={loading}>
Go to Space Join Space
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
</ModalFooter> </ModalFooter>
-76
View File
@@ -1,76 +0,0 @@
<script lang="ts">
import {preventDefault} from "@lib/html"
import Button from "@lib/components/Button.svelte"
import Field from "@lib/components/Field.svelte"
import FireMinimalistic from "@assets/icons/fire-minimalistic.svg?dataurl"
import Server from "@assets/icons/server.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import InfoRelay from "@app/components/InfoRelay.svelte"
import InputProfilePicture from "@app/components/InputProfilePicture.svelte"
import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
import {pushModal} from "@app/util/modal"
const back = () => history.back()
const next = () => pushModal(SpaceCreateFinish)
let file: File | undefined = $state()
let name = $state("")
let relay = $state("")
</script>
<form class="column gap-4" onsubmit={preventDefault(next)}>
<ModalHeader>
{#snippet title()}
<div>Customize your Space</div>
{/snippet}
{#snippet info()}
<div>Give people a few details to go on. You can always change this later.</div>
{/snippet}
</ModalHeader>
<div class="flex justify-center py-2">
<InputProfilePicture bind:file />
</div>
<Field>
{#snippet label()}
<p>Space Name</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={FireMinimalistic} />
<input bind:value={name} class="grow" type="text" />
</label>
{/snippet}
</Field>
<Field>
{#snippet label()}
<p>Relay</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Server} />
<input bind:value={relay} class="grow" type="text" />
</label>
{/snippet}
{#snippet info()}
<p>
This can be any nostr relay where you'd like to host your space.
<Button class="link" onclick={() => pushModal(InfoRelay)}>What is a relay?</Button>
</p>
{/snippet}
</Field>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button type="submit" class="btn btn-primary">
Next
<Icon icon={AltArrowRight} />
</Button>
</ModalFooter>
</form>

Some files were not shown because too many files have changed in this diff Show More