Compare commits

...

108 Commits

Author SHA1 Message Date
Jon Staab 5525e45a15 Bump version, upgrade welshman 2025-11-05 09:42:27 -08:00
Jon Staab 80a2ae60b0 Bump version 2025-11-04 17:28:27 -08:00
Jon Staab d7e95f5d2f Fix chat url 2025-11-04 17:25:50 -08:00
Jon Staab ca4e5ae5ee Add shadow to thread items etc, bump welshman, update changelog, update version 2025-11-04 17:14:33 -08:00
Jon Staab b673658c0c Handle escape in chat 2025-11-04 16:59:17 -08:00
Jon Staab 5c5c130700 Add landlubber link if user is admin 2025-11-04 16:55:26 -08:00
Jon Staab 2d89ca6c0e Support invite links on discover page 2025-11-04 16:39:34 -08:00
Jon Staab 806a7c2609 Persist alert kinds again 2025-11-04 16:25:21 -08:00
Jon Staab 501ce8067d Detect nip29 properly before choosing smart path, more robust auth error checking 2025-11-04 16:14:32 -08:00
Jon Staab 6429f82829 Improve claim/access detection 2025-11-04 15:36:20 -08:00
Jon Staab fe626218ea Ignore aborted signatures when checking auth 2025-11-04 09:34:07 -08:00
Jon Staab b62b1bc063 Don't source local .env file on build 2025-11-04 09:18:26 -08:00
Jon Staab d980f36246 Use request instead of load to avoid timeouts 2025-11-04 09:05:17 -08:00
Jon Staab b469addd29 Remove withGetter 2025-11-03 14:52:12 -08:00
Jon Staab 6923c2a8b7 Tweak modal, reduce storage on mobile 2025-11-03 14:43:27 -08:00
Jon Staab 1d3f32fb99 Only return error from attemptRelayAccess if there is a claim sent 2025-11-03 12:08:50 -08:00
Jon Staab 42a550788a Fix some alerts stuff 2025-11-03 11:10:16 -08:00
Jon Staab b1c68972c9 Streamline deriveRoom 2025-10-31 16:19:22 -07:00
Jon Staab 3978e32d5f Tweak access terminology, relay access attempts 2025-10-31 16:00:14 -07:00
Jon Staab ba2b5d182e Fix alerts 2025-10-31 14:51:59 -07:00
Jon Staab bef04fa899 Add holis to hosting suggestions 2025-10-31 14:02:52 -07:00
Jon Staab 4f8609421c Fix membership status 2025-10-31 12:10:16 -07:00
Jon Staab 07660c9d44 Re-work rooms derivation 2025-10-30 15:52:24 -07:00
Jon Staab a324dad2ba Rename channel to room 2025-10-30 15:36:14 -07:00
Jon Staab dbaa0f5d49 Rename room variables to h 2025-10-30 15:33:34 -07:00
Jon Staab 478721d349 Add room editing 2025-10-30 15:22:31 -07:00
Jon Staab a669a23dbc Tweak reaction buttons 2025-10-30 12:53:21 -07:00
Jon Staab cfeb6478cc Fix flapping subscription 2025-10-30 12:06:53 -07:00
Jon Staab 64539c49c1 Fix link, spinner animation 2025-10-30 07:20:09 -07:00
Jon Staab 0399ae37ec Move space create to its own page 2025-10-29 12:52:26 -07:00
Jon Staab 173a411a36 Update space create dialog 2025-10-29 11:18:27 -07:00
Jon Staab 62013a2ea2 Tweak mobile space menu 2025-10-28 16:50:15 -07:00
Jon Staab c82cf4a4c2 Update platform url 2025-10-28 16:08:48 -07:00
Jon Staab df42085be6 Sync messages at the space level 2025-10-28 15:46:25 -07:00
Jon Staab b09d3065ae Fix app url on capacitor deployments 2025-10-28 15:40:28 -07:00
Jon Staab c050f5a9e3 Update changelog 2025-10-28 15:37:36 -07:00
Jon Staab 78e6c0eca0 Bump version 2025-10-28 15:35:39 -07:00
Jon Staab da4da45348 Load rooms correctly 2025-10-28 14:53:44 -07:00
Jon Staab dc2af86db8 Bump welshman 2025-10-28 13:26:02 -07:00
Jon Staab 7502004aba Improve syncing 2025-10-28 11:29:59 -07:00
Jon Staab 2e8678e4c6 Bump welshman 2025-10-27 15:08:34 -07:00
Jon Staab 97569016fc Bump version 2025-10-27 14:19:06 -07:00
Jon Staab fe72798592 Send leave request 2025-10-27 14:13:17 -07:00
Jon Staab 4583c4e028 fix zapper loading 2025-10-27 13:36:29 -07:00
Jon Staab 0b98197a86 Add room deletion 2025-10-24 13:36:59 -07:00
Jon Staab 0e94a9c33f Use imperative svelte api for modals 2025-10-24 10:27:15 -07:00
Jon Staab 3dff1fcb4d Switch to new relays store 2025-10-24 09:38:57 -07:00
Jon Staab e163286dd4 Re-render suggestions on search update; prioritize space members in search 2025-10-24 09:09:59 -07:00
Jon Staab a99e12f12e Bump welshman 2025-10-24 06:47:20 -07:00
Matthew Remmel c3dd997e57 Add icon picker to room create component 2025-10-24 06:38:03 -07:00
Matthew Remmel a730384baf Add relay members list and room join/leave events 2025-10-24 05:03:22 -07:00
Jon Staab 43cf91e877 Remove connection toast now that we have a cta surfaced 2025-10-22 08:35:42 -07:00
Jon Staab 75bee027e1 Remove shards entirely, fix setup in layout 2025-10-21 10:29:29 -07:00
Jon Staab 5cbf69a8bd Push shards into storage lib 2025-10-21 09:26:06 -07:00
Jon Staab ecbb3086d8 Handle hot module unloading in layout 2025-10-21 08:27:30 -07:00
Jon Staab 7476767aa7 Add space status indicator #245 2025-10-20 17:05:22 -07:00
Jon Staab e5b8987a9d Move nav item 2025-10-20 16:06:00 -07:00
Jon Staab 6ca74c21bf Update to new version of welshman, including new thunks and wrap manager 2025-10-20 15:42:41 -07:00
Jon Staab e0099141aa Refactor synchronization logic 2025-10-17 12:23:03 -05:00
Jon Staab d0491ed202 Re-work space navigation #223 2025-10-17 12:23:03 -05:00
Jon Staab cbc2137ced Show all messages in non-nip29 chat 2025-10-17 12:23:03 -05:00
Jon Staab f9ac13ba11 Re-work space navigation #223 2025-10-17 12:21:22 -05:00
Jon Staab b3533c285f Show all messages in non-nip29 chat 2025-10-17 09:13:54 -07:00
Matthew Remmel a636ae6592 Simplify room create permission derive 2025-10-17 09:13:54 -07:00
Matthew Remmel 69e3ee0aff Move create room permission check to menu space 2025-10-17 09:13:54 -07:00
Matthew Remmel a39a87ba6d Disable create room button if no permission 2025-10-17 09:13:54 -07:00
Matthew Remmel 5b22d6ac01 Allow editing previous messages in channel chat 2025-10-17 11:13:09 -05:00
Jon Staab 7334cd26f8 Bump version 2025-10-13 15:17:46 -07:00
Jon Staab 44555215cf Track shards separately, upgrade deps 2025-10-13 13:41:27 -07:00
Jon Staab 0cc25913c0 Optimize event storage 2025-10-13 12:46:56 -07:00
Jon Staab 004b30b737 Update caniuse 2025-10-13 11:48:22 -07:00
Jon Staab 632f330b4c Re-work storage to optimize file access 2025-10-06 17:01:25 -07:00
Jon Staab 666433912f Only show send toast in chat if send_delay is set 2025-10-06 11:27:07 -07:00
Jon Staab db98ce8db7 Bump welshman 2025-10-06 11:26:27 -07:00
Jon Staab 71dcfae5ff Add heading, update changelog, bump version 2025-10-02 12:43:19 -07:00
Jon Staab 04155f5b23 Bump welshman 2025-10-01 17:03:25 -07:00
Jon Staab b4058389ec Avoid decrypt errors 2025-10-01 10:06:21 -07:00
Jon Staab 483fa81b74 Fix some storage bugs 2025-09-30 17:04:52 -07:00
Jon Staab a8d1c4bbbc Refactor storage 2025-09-30 16:28:12 -07:00
Matthew Remmel 0a8c2faa74 Add filestorage adapters 2025-09-30 10:52:24 -07:00
Jon Staab dd3231e70f Bring back blossom server auth, fix duplicate direct messages 2025-09-29 14:25:07 -07:00
Jon Staab 7ff9c00032 Force url extension for encrypted uploads 2025-09-29 14:25:07 -07:00
Matthew Remmel 9ed483abf7 Ignore exception from if failing to set badge 2025-09-29 10:32:13 -07:00
Matthew Remmel b9aeaf29a4 Disable notification sound when tab is focused 2025-09-29 10:32:13 -07:00
Matthew Remmel 65e3f81f36 Remove comments and test lines 2025-09-29 10:32:13 -07:00
Matthew Remmel c6641dba31 Move notification sound and badge settings to settings store 2025-09-29 10:32:13 -07:00
Matthew Remmel e48d1e0e59 Fix async bug and add sound component for notification sound 2025-09-29 10:32:13 -07:00
Matthew Remmel d1e5aee84e Add naive badge count implementation 2025-09-29 10:32:13 -07:00
Matthew Remmel 5cb22d0bed Add checkboxes for badge/sound settings 2025-09-29 10:32:13 -07:00
Matthew Remmel d1c6f53d7c Add royalty-free sound effect for new notification 2025-09-29 10:32:13 -07:00
Matthew Remmel 6e238f98c0 Auto-add receiving address on wallet setup 2025-09-29 10:25:02 -07:00
Jon Staab 290274d6c8 Tweak light theme, remove conditional button classes 2025-09-25 10:52:32 -07:00
Jon Staab e1de0239c9 Remove css that was breaking tooltips 2025-09-25 10:43:55 -07:00
Jon Staab bec77d59e8 Add theme toggle on mobile, change button color for quick links 2025-09-25 10:37:53 -07:00
Jon Staab 84f8794d7c Make link previews less aggressive 2025-09-25 10:12:58 -07:00
Jon Staab 4cddf41bf3 Set initial delay to 0 2025-09-24 09:50:07 -07:00
Jon Staab 125a7e238e Add qr scanner to discover page 2025-09-22 15:56:48 -07:00
Jon Staab 468200b717 Link directly to discover page 2025-09-22 15:48:13 -07:00
Jon Staab bdfcb99781 Show more information about signer type 2025-09-22 15:09:41 -07:00
Jon Staab 38da650861 Add qr code to invite screen 2025-09-22 14:57:43 -07:00
Jon Staab dd006badfc Bring back blossom feature detection 2025-09-22 14:05:57 -07:00
Jon Staab 87e4e3fe5b Catch all upload errors 2025-09-22 11:43:44 -07:00
Jon Staab af3e38254f Fix focus on input list 2025-09-22 11:06:16 -07:00
Jon Staab 70843f54d3 Increase contrast on mention badges in editor 2025-09-22 10:40:54 -07:00
Jon Staab bda75b29b4 Handle bunker login errors better 2025-09-22 10:35:31 -07:00
Jon Staab 750830d593 Bump version again 2025-09-18 14:39:15 -07:00
Jon Staab 3c0f1a1d2f Restore icons 2025-09-18 14:13:08 -07:00
Jon Staab 4253b0ed29 Remove all icons 2025-09-18 14:12:08 -07:00
1447 changed files with 13252 additions and 9503 deletions
+3 -2
View File
@@ -1,6 +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_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
@@ -10,7 +11,7 @@ VITE_PLATFORM_ACCENT="#7161FF"
VITE_PLATFORM_SECONDARY="#EB5E28" VITE_PLATFORM_SECONDARY="#EB5E28"
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities." VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/,wss://indexer.coracle.social/ VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/,wss://indexer.coracle.social/
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/ VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://offchain.pub/
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/ VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
+63
View File
@@ -1,5 +1,68 @@
# Changelog # Changelog
# 1.5.1
* Fix chat path link
# 1.5.0
* Restyle mobile dialogs
* Add room membership lists
* Add space membership lists
* Add edit room form
* Support closed/private/restricted/hidden rooms
* Add hosting services page
* Improve performance and UI
* Fix push notifications
* Improve error detection and handling
* Support invite links on discover page
* Add link to landlubber if user is admin
* Clear reply/share/edit on escape
# 1.4.1
* Improve data synchronization
* Fix app url on capacitor deployments
# 1.4.0
* Allow "editing" chat messages
* Check for room create permission
* Re-work space navigation
* Show all messages in non-nip29 chat
* Improve synchronization logic
* Add connection status to space menu
* Add icon picker to room create component
* Improve mention suggestions
* Improve storage adapter and relay list performance
* Fix modals
* Add room deletion
* Fix zapper loading
* Add support for relay/group member lists and join/leave events
# 1.3.1
* Fix memory leak in storage adapter
* Show fewer annoying toast messages
# 1.3.0
* Add optional badge and sound for notifications
* Improve link rendering
* Remove imgproxy
* Bring back blossom feature detection for spaces
* Improve light theme
* Add more info to signer status
* Simplify navigation for adding a space
* Add ability to scan QR code for invite links
* Streamline wallet setup and move receive address setting
* Remove indexeddb on mobile, use capacitor file storage API
* Fix duplicate DMs showing up
# 1.2.5
* Fix icons in build
# 1.2.4 # 1.2.4
* Add direct message alerts * Add direct message alerts
+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 25 versionCode 33
versionName "1.2.4" versionName "1.5.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+1
View File
@@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies { dependencies {
implementation project(':capacitor-community-safe-area') implementation project(':capacitor-community-safe-area')
implementation project(':capacitor-app') implementation project(':capacitor-app')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-keyboard') implementation project(':capacitor-keyboard')
implementation project(':capacitor-preferences') implementation project(':capacitor-preferences')
implementation project(':capacitor-push-notifications') implementation project(':capacitor-push-notifications')
+12 -9
View File
@@ -1,27 +1,30 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android' include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/android/capacitor') project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/android/capacitor')
include ':capacitor-community-safe-area' include ':capacitor-community-safe-area'
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area/android') project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area/android')
include ':capacitor-app' include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app/android') project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem/android')
include ':capacitor-keyboard' include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard/android') project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard/android')
include ':capacitor-preferences' include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.2.0/node_modules/@capacitor/preferences/android') project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences/android')
include ':capacitor-push-notifications' include ':capacitor-push-notifications'
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications/android') project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications/android')
include ':capawesome-capacitor-android-dark-mode-support' include ':capawesome-capacitor-android-dark-mode-support'
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@7.0.0_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-android-dark-mode-support/android') project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@7.0.0_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
include ':capawesome-capacitor-badge' include ':capawesome-capacitor-badge'
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge/android') project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge/android')
include ':nostr-signer-capacitor-plugin' include ':nostr-signer-capacitor-plugin'
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin/android') project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin/android')
-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"
+8 -4
View File
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
@@ -21,6 +22,7 @@
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Flotilla Chat.entitlements"; sourceTree = "<group>"; }; 051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Flotilla Chat.entitlements"; sourceTree = "<group>"; };
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; }; 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; }; 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* Flotilla Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Flotilla Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 504EC3041FED79650016851F /* Flotilla Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Flotilla Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -58,6 +60,7 @@
504EC2FB1FED79650016851F = { 504EC2FB1FED79650016851F = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */,
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */, 051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
504EC3061FED79650016851F /* App */, 504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */, 504EC3051FED79650016851F /* Products */,
@@ -162,6 +165,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */, 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */, 50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */, 504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
@@ -354,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 = 17; CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.4; MARKETING_VERSION = 1.5.2;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -380,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 = 17; CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat"; INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.4; MARKETING_VERSION = 1.5.2;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
+11 -10
View File
@@ -1,4 +1,4 @@
require_relative '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios/scripts/pods_helpers' require_relative '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '14.0' platform :ios, '14.0'
use_frameworks! use_frameworks!
@@ -9,15 +9,16 @@ use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios' pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios'
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area' pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app' pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app'
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard' pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem'
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.2.0/node_modules/@capacitor/preferences' pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications' pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge' pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin' pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin'
end end
target 'Flotilla Chat' do target 'Flotilla Chat' do
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
</array>
</dict>
</plist>
+53 -52
View File
@@ -1,6 +1,6 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "1.2.4", "version": "1.5.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -10,76 +10,77 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src && eslint src", "lint": "prettier --check src && eslint src",
"format": "prettier --write src", "format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte)$' | xargs -r prettier --write",
"format:all": "prettier --write src",
"prepare": "husky" "prepare": "husky"
}, },
"devDependencies": { "devDependencies": {
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@eslint/js": "^9.26.0", "@eslint/js": "^9.37.0",
"@sentry/cli": "^2.40.0", "@sentry/cli": "^2.56.1",
"@sveltejs/kit": "^2.5.27", "@sveltejs/kit": "^2.46.5",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.0", "@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.21",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"eslint": "^9.0.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.2",
"eslint-plugin-svelte": "^2.45.1", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.0.0", "globals": "^15.15.0",
"postcss": "^8.4.40", "postcss": "^8.5.6",
"prettier": "^3.1.1", "prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.0.0", "svelte": "^5.39.12",
"svelte-check": "^4.0.0", "svelte-check": "^4.3.3",
"tailwindcss": "^3.4.7", "tailwindcss": "^3.4.18",
"typescript": "^5.5.0", "typescript": "^5.9.3",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.46.1",
"vite": "^5.4.4" "vite": "^5.4.20"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@capacitor-community/safe-area": "7.0.0-alpha.1", "@capacitor-community/safe-area": "7.0.0-alpha.1",
"@capacitor/android": "^7.0.0", "@capacitor/android": "^7.4.3",
"@capacitor/app": "^7.0.0", "@capacitor/app": "^7.1.0",
"@capacitor/cli": "^7.0.0", "@capacitor/cli": "^7.4.3",
"@capacitor/core": "^7.0.1", "@capacitor/core": "^7.4.3",
"@capacitor/ios": "^7.0.0", "@capacitor/filesystem": "^7.1.4",
"@capacitor/keyboard": "^7.0.0", "@capacitor/ios": "^7.4.3",
"@capacitor/keyboard": "^7.0.3",
"@capacitor/preferences": "^7.0.2", "@capacitor/preferences": "^7.0.2",
"@capacitor/push-notifications": "^7.0.1", "@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/sdk": "^5.1.0", "@getalby/sdk": "^5.1.2",
"@poppanator/sveltekit-svg": "^4.2.1", "@poppanator/sveltekit-svg": "^4.2.1",
"@sentry/browser": "^8.35.0", "@sentry/browser": "^8.55.0",
"@sveltejs/adapter-static": "^3.0.4", "@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.12.0", "@tiptap/core": "^2.26.3",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@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.6", "@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.4.7", "@welshman/app": "^0.6.5",
"@welshman/content": "^0.4.7", "@welshman/content": "^0.6.5",
"@welshman/editor": "^0.4.7", "@welshman/editor": "^0.6.5",
"@welshman/feeds": "^0.4.7", "@welshman/feeds": "^0.6.5",
"@welshman/lib": "^0.4.7", "@welshman/lib": "^0.6.5",
"@welshman/net": "^0.4.7", "@welshman/net": "^0.6.5",
"@welshman/relay": "^0.4.7", "@welshman/router": "^0.6.5",
"@welshman/router": "^0.4.7", "@welshman/signer": "^0.6.5",
"@welshman/signer": "^0.4.7", "@welshman/store": "^0.6.5",
"@welshman/store": "^0.4.7", "@welshman/util": "^0.6.5",
"@welshman/util": "^0.4.7",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"daisyui": "^4.12.10", "daisyui": "^4.12.24",
"date-picker-svelte": "^2.13.0", "date-picker-svelte": "^2.16.0",
"dotenv": "^16.4.5", "dotenv": "^16.6.1",
"emoji-picker-element": "^1.22.8", "emoji-picker-element": "^1.27.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.1.0",
"husky": "^9.1.6", "husky": "^9.1.7",
"idb": "^8.0.0", "idb": "^8.0.3",
"nostr-signer-capacitor-plugin": "^0.0.4", "nostr-signer-capacitor-plugin": "^0.0.4",
"nostr-tools": "^2.14.2", "nostr-tools": "^2.14.2",
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.14",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"throttle-debounce": "^5.0.2", "throttle-debounce": "^5.0.2",
+2002 -1845
View File
File diff suppressed because it is too large Load Diff
+5 -9
View File
@@ -62,6 +62,8 @@
--primary-content: oklch(var(--pc)); --primary-content: oklch(var(--pc));
--secondary: oklch(var(--s)); --secondary: oklch(var(--s));
--secondary-content: oklch(var(--sc)); --secondary-content: oklch(var(--sc));
--neutral: oklch(var(--n));
--neutral-content: oklch(var(--nc));
} }
/* safe area insets */ /* safe area insets */
@@ -215,12 +217,6 @@
@apply ellipsize; @apply ellipsize;
} }
@media (max-width: 639px) {
[data-tip]::before {
display: none;
}
}
.content-padding-x { .content-padding-x {
@apply px-4 sm:px-8 md:px-12; @apply px-4 sm:px-8 md:px-12;
} }
@@ -278,8 +274,8 @@
} }
.tiptap { .tiptap {
--tiptap-object-bg: var(--base-100); --tiptap-object-bg: var(--neutral);
--tiptap-object-fg: var(--base-content); --tiptap-object-fg: var(--neutral-content);
--tiptap-active-bg: var(--primary); --tiptap-active-bg: var(--primary);
--tiptap-active-fg: var(--primary-content); --tiptap-active-fg: var(--primary-content);
} }
@@ -399,7 +395,7 @@ progress[value]::-webkit-progress-value {
/* chat view */ /* chat view */
.chat__compose { .chat__compose {
@apply cb cw fixed; @apply cb cw fixed z-compose;
} }
.chat__scroll-down { .chat__scroll-down {
+2 -2
View File
@@ -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, getMembershipUrls, userMembership} from "@app/core/state" import {alerts, 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"
@@ -174,7 +174,7 @@
{#snippet input()} {#snippet input()}
<select bind:value={url} class="select select-bordered"> <select bind:value={url} class="select select-bordered">
<option value="" disabled selected>Choose a space URL</option> <option value="" disabled selected>Choose a space URL</option>
{#each getMembershipUrls($userMembership) as url (url)} {#each $userSpaceUrls as url (url)}
<option value={url}>{displayRelayUrl(url)}</option> <option value={url}>{displayRelayUrl(url)}</option>
{/each} {/each}
</select> </select>
+49 -6
View File
@@ -3,6 +3,7 @@
import {getTagValue, getAddress} from "@welshman/util" import {getTagValue, getAddress} from "@welshman/util"
import {isRelayFeed, findFeed} from "@welshman/feeds" import {isRelayFeed, findFeed} from "@welshman/feeds"
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 AddCircle from "@assets/icons/add-circle.svg?dataurl" import AddCircle from "@assets/icons/add-circle.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"
@@ -10,8 +11,16 @@
import AlertItem from "@app/components/AlertItem.svelte" import AlertItem from "@app/components/AlertItem.svelte"
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 {alerts, dmAlert, deriveAlertStatus, userInboxRelays, getAlertFeed} from "@app/core/state" import {
alerts,
dmAlert,
deriveAlertStatus,
userInboxRelays,
getAlertFeed,
userSettingsValues,
} from "@app/core/state"
import {deleteAlert, createDmAlert} from "@app/core/commands" import {deleteAlert, createDmAlert} from "@app/core/commands"
import {clearBadges} from "../util/notifications"
type Props = { type Props = {
url?: string url?: string
@@ -42,11 +51,11 @@
const uncheckDmAlert = async (message: string) => { const uncheckDmAlert = async (message: string) => {
await sleep(100) await sleep(100)
toggle.checked = false directMessagesNotificationToggle.checked = false
pushToast({theme: "error", message}) pushToast({theme: "error", message})
} }
const onToggle = async () => { const onDirectMessagesNotificationToggle = async () => {
if ($dmAlert) { if ($dmAlert) {
deleteAlert($dmAlert) deleteAlert($dmAlert)
} else { } else {
@@ -64,7 +73,19 @@
} }
} }
let toggle: HTMLInputElement const onShowBadgeOnUnreadToggle = async () => {
$userSettingsValues.show_notifications_badge = !$userSettingsValues.show_notifications_badge
if (!$userSettingsValues.show_notifications_badge) {
await clearBadges()
}
}
const onDirectMessagesNotificationSoundToggle = async () => {
$userSettingsValues.play_notification_sound = !$userSettingsValues.play_notification_sound
}
let directMessagesNotificationToggle: HTMLInputElement
</script> </script>
<div class="col-4"> <div class="col-4">
@@ -88,14 +109,36 @@
</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-xl">
<div class="flex items-center justify-between">
<strong class="flex items-center gap-3">
<Icon icon={Bell} />
Notifications
</strong>
</div>
<div class="flex justify-between"> <div class="flex justify-between">
<p>Notify me about new direct messages</p> <p>Notify me about new direct messages</p>
<input <input
type="checkbox" type="checkbox"
class="toggle toggle-primary" class="toggle toggle-primary"
bind:this={toggle} bind:this={directMessagesNotificationToggle}
checked={Boolean($dmAlert)} checked={Boolean($dmAlert)}
oninput={onToggle} /> oninput={onDirectMessagesNotificationToggle} />
</div>
<div class="flex justify-between">
<p>Show badge for unread direct messages</p>
<input
type="checkbox"
class="toggle toggle-primary"
checked={Boolean($userSettingsValues.show_notifications_badge)}
oninput={onShowBadgeOnUnreadToggle} />
</div>
<div class="flex justify-between">
<p>Play sound for new direct messages</p>
<input
type="checkbox"
class="toggle toggle-primary"
checked={Boolean($userSettingsValues.play_notification_sound)}
oninput={onDirectMessagesNotificationSoundToggle} />
</div> </div>
{#if $dmStatus} {#if $dmStatus}
{@const status = getTagValue("status", $dmStatus.tags) || "error"} {@const status = getTagValue("status", $dmStatus.tags) || "error"}
+33 -28
View File
@@ -1,31 +1,33 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue} from "@welshman/util"
import {pubkey} from "@welshman/app" import {pubkey} 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 Link from "@lib/components/Link.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"
import EventActions from "@app/components/EventActions.svelte" import EventActions from "@app/components/EventActions.svelte"
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte" import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands" import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
import {makeCalendarPath} from "@app/util/routes" import {makeCalendarPath, makeSpacePath} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import Pen2 from "@assets/icons/pen-2.svg?dataurl" import Pen2 from "@assets/icons/pen-2.svg?dataurl"
const { type Props = {
url,
event,
showActivity = false,
}: {
url: string url: string
event: TrustedEvent event: TrustedEvent
showRoom?: boolean
showActivity?: boolean showActivity?: boolean
} = $props() }
const shouldProtect = canEnforceNip70(url) const {url, event, showRoom, showActivity}: Props = $props()
const h = getTagValue("h", event.tags)
const path = makeCalendarPath(url, event.id) const path = makeCalendarPath(url, event.id)
const shouldProtect = canEnforceNip70(url)
const editEvent = () => pushModal(CalendarEventEdit, {url, event}) const editEvent = () => pushModal(CalendarEventEdit, {url, event})
@@ -36,24 +38,27 @@
publishReaction({...template, event, relays: [url], protect: await shouldProtect}) publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script> </script>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-grow flex-wrap justify-end gap-2">
<div class="flex flex-grow flex-wrap justify-end gap-2"> {#if h && showRoom}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" /> <Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
<ThunkStatusOrDeleted {event} /> Posted in #<RoomName {h} {url} />
{#if showActivity} </Link>
<EventActivity {url} {path} {event} /> {/if}
{/if} <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
<EventActions {url} {event} noun="Event"> <ThunkStatusOrDeleted {event} />
{#snippet customActions()} {#if showActivity}
{#if event.pubkey === $pubkey} <EventActivity {url} {path} {event} />
<li> {/if}
<Button onclick={editEvent}> <EventActions {url} {event} noun="Event">
<Icon size={4} icon={Pen2} /> {#snippet customActions()}
Edit Event {#if event.pubkey === $pubkey}
</Button> <li>
</li> <Button onclick={editEvent}>
{/if} <Icon size={4} icon={Pen2} />
{/snippet} Edit Event
</EventActions> </Button>
</div> </li>
{/if}
{/snippet}
</EventActions>
</div> </div>
@@ -4,12 +4,13 @@
type Props = { type Props = {
url: string url: string
h?: string
} }
const {url}: Props = $props() const {url, h}: Props = $props()
</script> </script>
<CalendarEventForm {url}> <CalendarEventForm {url} {h}>
{#snippet header()} {#snippet header()}
<ModalHeader> <ModalHeader>
{#snippet title()} {#snippet title()}
+11 -8
View File
@@ -8,13 +8,16 @@
const {event}: Props = $props() const {event}: Props = $props()
const meta = $derived(fromPairs(event.tags) as Record<string, string>) const meta = $derived(fromPairs(event.tags) as Record<string, string>)
const startDate = $derived(secondsToDate(parseInt(meta.start))) const start = $derived(parseInt(meta.start))
</script> </script>
<div {#if !isNaN(start)}
class="hidden h-32 w-32 min-w-32 flex-col items-center justify-center gap-1 rounded-box bg-base-300 p-2 sm:flex"> {@const startDate = secondsToDate(start)}
<strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong> <div
<span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span> class="hidden h-32 w-32 min-w-32 flex-col items-center justify-center gap-1 rounded-box bg-base-300 p-2 sm:flex">
<span class="text-xs opacity-75" <strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong>
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span> <span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
</div> <span class="text-xs opacity-75"
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span>
</div>
{/if}
+6 -1
View File
@@ -23,6 +23,7 @@
type Props = { type Props = {
url: string url: string
h?: string
header: Snippet header: Snippet
initialValues?: { initialValues?: {
d: string d: string
@@ -34,7 +35,7 @@
} }
} }
const {url, header, initialValues}: Props = $props() const {url, h, header, initialValues}: Props = $props()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -84,6 +85,10 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
if (h) {
tags.push(["h", h])
}
const event = makeEvent(EVENT_TIME, {content, tags}) const event = makeEvent(EVENT_TIME, {content, tags})
pushToast({message: "Your event has been saved!"}) pushToast({message: "Your event has been saved!"})
+12 -10
View File
@@ -17,18 +17,20 @@
const meta = $derived(fromPairs(event.tags) as Record<string, string>) const meta = $derived(fromPairs(event.tags) as Record<string, string>)
const start = $derived(parseInt(meta.start)) const start = $derived(parseInt(meta.start))
const end = $derived(parseInt(meta.end)) const end = $derived(parseInt(meta.end))
const startDateDisplay = $derived(formatTimestampAsDate(start))
const endDateDisplay = $derived(formatTimestampAsDate(end))
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
</script> </script>
<div class="flex flex-grow flex-wrap justify-between gap-2"> <div class="flex flex-grow flex-wrap justify-between gap-2">
<p class="text-xl">{meta.title || meta.name}</p> <p class="text-xl">{meta.title || meta.name}</p>
<div class="flex items-center gap-2 text-sm"> {#if !isNaN(start) && !isNaN(end)}
<Icon icon={ClockCircle} size={4} /> {@const startDateDisplay = formatTimestampAsDate(start)}
<span class="sm:hidden">{formatTimestampAsDate(start)}</span> {@const endDateDisplay = formatTimestampAsDate(end)}
{formatTimestampAsTime(start)}{isSingleDay {@const isSingleDay = startDateDisplay === endDateDisplay}
? formatTimestampAsTime(end) <div class="flex items-center gap-2 text-sm">
: formatTimestamp(end)} <Icon icon={ClockCircle} size={4} />
</div> <span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
{formatTimestampAsTime(start)}{isSingleDay
? formatTimestampAsTime(end)
: formatTimestamp(end)}
</div>
{/if}
</div> </div>
+10 -1
View File
@@ -1,9 +1,11 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {getTagValue} from "@welshman/util"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
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 RoomLink from "@app/components/RoomLink.svelte"
import {makeCalendarPath} from "@app/util/routes" import {makeCalendarPath} from "@app/util/routes"
type Props = { type Props = {
@@ -12,13 +14,20 @@
} }
const {url, event}: Props = $props() const {url, event}: Props = $props()
const h = getTagValue("h", event.tags)
</script> </script>
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}> <Link
class="col-3 card2 bg-alt w-full cursor-pointer shadow-xl"
href={makeCalendarPath(url, event.id)}>
<CalendarEventHeader {event} /> <CalendarEventHeader {event} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
<span class="whitespace-nowrap py-1 text-sm opacity-75"> <span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by <ProfileLink pubkey={event.pubkey} {url} /> Posted by <ProfileLink pubkey={event.pubkey} {url} />
{#if h}
in <RoomLink {url} {h} />
{/if}
</span> </span>
<CalendarEventActions showActivity {url} {event} /> <CalendarEventActions showActivity {url} {event} />
</div> </div>
-66
View File
@@ -1,66 +0,0 @@
<script lang="ts">
import {writable} from "svelte/store"
import type {EventContent} from "@welshman/util"
import {isMobile, preventDefault} from "@lib/html"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
import Plane from "@assets/icons/plane-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {makeEditor} from "@app/editor"
type Props = {
url?: string
onSubmit: (event: EventContent) => void
}
const {onSubmit, url}: Props = $props()
const autofocus = !isMobile
const uploading = writable(false)
export const focus = () => editor.then(ed => ed.chain().focus().run())
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
const submit = async () => {
if ($uploading) return
const ed = await editor
const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = ed.storage.nostr.getEditorTags()
if (!content) return
onSubmit({content, tags})
ed.chain().clearContent().run()
}
const editor = makeEditor({url, autofocus, submit, uploading, aggressive: true})
</script>
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
<Button
data-tip="Add an image"
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
disabled={$uploading}
onclick={uploadFiles}>
{#if $uploading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Icon icon={GallerySend} />
{/if}
</Button>
<div class="chat-editor flex-grow overflow-hidden">
<EditorContent {editor} />
</div>
<Button
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
disabled={$uploading}
onclick={submit}>
<Icon icon={Plane} />
</Button>
</form>
-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}
+8 -9
View File
@@ -11,6 +11,7 @@
MINUTE, MINUTE,
sortBy, sortBy,
remove, remove,
enumerate,
formatTimestampAsDate, formatTimestampAsDate,
} from "@welshman/lib" } from "@welshman/lib"
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util" import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
@@ -30,7 +31,6 @@
loadInboxRelaySelections, loadInboxRelaySelections,
inboxRelaySelectionsByPubkey, inboxRelaySelectionsByPubkey,
} from "@welshman/app" } from "@welshman/app"
import type {AbstractThunk} 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"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
@@ -126,14 +126,13 @@
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17 // Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
// Sleep 1 second between each one to make sure timestamps are distinct // Sleep 1 second between each one to make sure timestamps are distinct
const thunks: AbstractThunk[] = [] const thunks = Array.from(enumerate(templates)).map(([i, event]) =>
for (let i = 0; i < templates.length; i++) { sendWrapped({
const template = templates[i] event,
recipients: pubkeys,
thunks.push( delay: $userSettingsValues.send_delay + ms(i),
await sendWrapped({pubkeys, template, delay: $userSettingsValues.send_delay + ms(i)}), }),
) )
}
pushToast({ pushToast({
timeout: 30_000, timeout: 30_000,
+2 -3
View File
@@ -10,11 +10,10 @@
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
type Props = { type Props = {
url?: string
onSubmit: (event: EventContent) => void onSubmit: (event: EventContent) => void
} }
const {onSubmit, url}: Props = $props() const {onSubmit}: Props = $props()
const autofocus = !isMobile const autofocus = !isMobile
@@ -39,11 +38,11 @@
} }
const editor = makeEditor({ const editor = makeEditor({
url,
autofocus, autofocus,
submit, submit,
uploading, uploading,
aggressive: true, aggressive: true,
encryptFiles: true,
}) })
</script> </script>
+4 -9
View File
@@ -5,7 +5,7 @@
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.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 NoteContent from "@app/components/NoteContent.svelte" import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
const { const {
verb, verb,
@@ -19,16 +19,11 @@
</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 border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide> transition:slide>
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p> <p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id} {#key event.id}
<NoteContent <NoteContentMinimal trimParent {event} />
{event}
hideMediaAtDepth={0}
minLength={100}
maxLength={300}
expandMode="disabled" />
{/key} {/key}
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}> <Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
<Icon icon={CloseCircle} /> <Icon icon={CloseCircle} />
+4 -8
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {shouldUnwrap} from "@welshman/app"
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 Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -9,7 +10,6 @@
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 {PLATFORM_NAME} from "@app/core/state" import {PLATFORM_NAME} from "@app/core/state"
import {enableGiftWraps} from "@app/core/commands"
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
const {next} = $props() const {next} = $props()
@@ -18,17 +18,13 @@
let loading = $state(false) let loading = $state(false)
const enableChat = async () => {
enableGiftWraps()
clearModals()
goto(nextUrl)
}
const submit = async () => { const submit = async () => {
loading = true loading = true
try { try {
await enableChat() shouldUnwrap.set(true)
clearModals()
goto(nextUrl)
} finally { } finally {
loading = false loading = false
} }
+12 -5
View File
@@ -2,7 +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 {thunks, pubkey, deriveProfile, deriveProfileDisplay, sendWrapped} from "@welshman/app" import {
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"
@@ -29,19 +36,19 @@
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props() const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
const thunk = $thunks[event.id]
const isOwn = event.pubkey === $pubkey const isOwn = event.pubkey === $pubkey
const profile = deriveProfile(event.pubkey) const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length] const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const reply = () => replyTo(event) const reply = () => replyTo(event)
const deleteReaction = (event: TrustedEvent) => const deleteReaction = (event: TrustedEvent) =>
sendWrapped({template: makeDelete({event, protect: false}), pubkeys}) sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
const createReaction = (template: EventContent) => const createReaction = (template: EventContent) =>
sendWrapped({template: makeReaction({event, protect: false, ...template}), pubkeys}) sendWrapped({event: makeReaction({event, protect: false, ...template}), recipients: pubkeys})
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey}) const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
@@ -15,7 +15,10 @@
const {event, pubkeys}: Props = $props() const {event, pubkeys}: Props = $props()
const onEmoji = (emoji: NativeEmoji) => const onEmoji = (emoji: NativeEmoji) =>
sendWrapped({template: makeReaction({event, content: emoji.unicode, protect: false}), pubkeys}) sendWrapped({
event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys,
})
</script> </script>
<EmojiButton {onEmoji} class="btn join-item btn-xs"> <EmojiButton {onEmoji} class="btn join-item btn-xs">
@@ -24,7 +24,10 @@
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => { const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
history.back() history.back()
sendWrapped({template: makeReaction({event, content: emoji.unicode, protect: false}), pubkeys}) sendWrapped({
event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys,
})
}).bind(undefined, event, pubkeys) }).bind(undefined, event, pubkeys)
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true}) const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
+53
View File
@@ -0,0 +1,53 @@
<script lang="ts">
import {onMount} from "svelte"
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic.svg?dataurl"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import {pushModal} from "@app/util/modal"
import CalendarEventCreate from "@app/components/CalendarEventCreate.svelte"
import ThreadCreate from "@app/components/ThreadCreate.svelte"
import GoalCreate from "@app/components/GoalCreate.svelte"
type Props = {
url: string
onClick: () => void
h?: string
}
const {url, h, onClick}: Props = $props()
const createGoal = () => pushModal(GoalCreate, {url, h})
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, h})
const createThread = () => pushModal(ThreadCreate, {url, h})
let ul: Element
onMount(() => {
ul.addEventListener("click", onClick)
})
</script>
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}>
<li>
<Button onclick={createGoal}>
<Icon size={4} icon={StarFallMinimalistic} />
Create Funding Goal
</Button>
</li>
<li>
<Button onclick={createCalendarEvent}>
<Icon size={4} icon={CalendarMinimalistic} />
Create Calendar Event
</Button>
</li>
<li>
<Button onclick={createThread}>
<Icon size={4} icon={NotesMinimalistic} />
Create Thread
</Button>
</li>
</ul>
+40 -27
View File
@@ -18,6 +18,7 @@
isAddress, isAddress,
isNewline, isNewline,
} from "@welshman/content" } from "@welshman/content"
import type {Parsed} from "@welshman/content"
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 Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
@@ -39,10 +40,8 @@
minLength?: number minLength?: number
maxLength?: number maxLength?: number
showEntire?: boolean showEntire?: boolean
hideMediaAtDepth?: number
expandMode?: string expandMode?: string
minimalQuote?: boolean trimParent?: boolean
depth?: number
url?: string url?: string
} }
@@ -51,10 +50,8 @@
minLength = 500, minLength = 500,
maxLength = 700, maxLength = 700,
showEntire = $bindable(false), showEntire = $bindable(false),
hideMediaAtDepth = 1,
expandMode = "block", expandMode = "block",
minimalQuote = false, trimParent = false,
depth = 0,
url, url,
}: Props = $props() }: Props = $props()
@@ -67,13 +64,13 @@
const isBlock = (i: number) => { const isBlock = (i: number) => {
const parsed = fullContent[i] const parsed = fullContent[i]
if (!parsed || hideMediaAtDepth <= depth) return false if (!parsed) return false
if (isLink(parsed) && $userSettingsValues.show_media && isStartOrEnd(i)) { if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
return true return true
} }
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i)) { if (isQuote(parsed) && isStartAndEnd(i)) {
return true return true
} }
@@ -95,7 +92,7 @@
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i) const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
const isStartOrEnd = (i: number) => isStart(i) || isEnd(i) const isQuote = (p: Parsed) => isEvent(p) || isAddress(p)
const ignoreWarning = () => { const ignoreWarning = () => {
warning = null warning = null
@@ -105,15 +102,37 @@
$userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1], $userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
) )
const shortContent = $derived( const dropWhile = <T,>(f: (x: T) => boolean, xs: Iterable<T>) => {
showEntire const result: T[] = []
? fullContent
: truncate(fullContent, { for (const x of xs) {
minLength, if (result.length === 0 && f(x)) {
maxLength, continue
mediaLength: hideMediaAtDepth <= depth ? 20 : 200, }
}),
) result.push(x)
}
return result
}
const shortContent = $derived.by(() => {
let result = fullContent
if (trimParent && result.length > 0 && isQuote(result[0])) {
result = dropWhile(p => isQuote(p) || isNewline(p), result)
}
if (!showEntire) {
result = truncate(result, {
minLength,
maxLength,
mediaLength: 200,
})
}
return result
})
const hasEllipsis = $derived(shortContent.some(isEllipsis)) const hasEllipsis = $derived(shortContent.some(isEllipsis))
const expandInline = $derived(hasEllipsis && expandMode === "inline") const expandInline = $derived(hasEllipsis && expandMode === "inline")
@@ -154,15 +173,9 @@
{/if} {/if}
{:else if isProfile(parsed)} {:else if isProfile(parsed)}
<ContentMention value={parsed.value} {url} /> <ContentMention value={parsed.value} {url} />
{:else if isEvent(parsed) || isAddress(parsed)} {:else if isQuote(parsed)}
{#if isBlock(i)} {#if isBlock(i)}
<ContentQuote <ContentQuote {url} value={parsed.value} {event} />
{depth}
{url}
{hideMediaAtDepth}
value={parsed.value}
{event}
minimal={minimalQuote} />
{:else} {:else}
<Link <Link
external external
+1 -5
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type {ParsedEmojiValue} from "@welshman/content" import type {ParsedEmojiValue} from "@welshman/content"
import {imgproxy} from "@app/core/state"
export let value: ParsedEmojiValue export let value: ParsedEmojiValue
@@ -8,10 +7,7 @@
</script> </script>
{#if value.url} {#if value.url}
<img <img {alt} src={value.url} class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
{alt}
src={imgproxy(value.url, {w: 24, h: 24})}
class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
{:else} {:else}
{alt} {alt}
{/if} {/if}
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {ellipsize, displayUrl, postJson} from "@welshman/lib" import {ellipsize, displayUrl, postJson} from "@welshman/lib"
import {dufflepud, imgproxy} from "@app/core/state" import {dufflepud} from "@app/core/state"
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"
@@ -51,7 +51,7 @@
<img <img
alt="Link preview" alt="Link preview"
onerror={onError} onerror={onError}
src={imgproxy(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" />
{/if} {/if}
<div class="flex flex-col gap-2 p-4"> <div class="flex flex-col gap-2 p-4">
@@ -1,10 +1,17 @@
<script lang="ts"> <script lang="ts">
import {onMount, onDestroy} from "svelte" import {onMount, onDestroy} from "svelte"
import {displayUrl} from "@welshman/lib" import {displayUrl} from "@welshman/lib"
import {getTags, decryptFile, getTagValue, tagsFromIMeta} from "@welshman/util" import {
getTags,
getBlob,
decryptFile,
getTagValue,
tagsFromIMeta,
makeBlossomAuthEvent,
} from "@welshman/util"
import {signer} from "@welshman/app"
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 {imgproxy} from "@app/core/state"
const {value, event, ...props} = $props() const {value, event, ...props} = $props()
@@ -14,18 +21,34 @@
.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)
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)
const onError = () => { const onError = async () => {
hasError = true // If the image failed to load, try authenticating
if (hash && $signer) {
const server = new URL(url).origin
const template = makeBlossomAuthEvent({action: "get", server, hashes: [hash]})
const authEvent = await $signer.sign(template)
const res = await getBlob(server, hash, {authEvent})
if (res.status === 200) {
src = URL.createObjectURL(await res.blob())
} else {
hasError = true
}
} else {
hasError = true
}
} }
let hasError = $state(false) let hasError = $state(false)
let src = $state(imgproxy(url)) let src = $state("")
onMount(async () => { onMount(async () => {
// If we have an encryption algorithm, fetch and decrypt
if (algorithm === "aes-gcm" && key && nonce) { if (algorithm === "aes-gcm" && key && nonce) {
const response = await fetch(url) const response = await fetch(url)
@@ -33,8 +56,10 @@
const ciphertext = new Uint8Array(await response.arrayBuffer()) const ciphertext = new Uint8Array(await response.arrayBuffer())
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm}) const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
src = URL.createObjectURL(new Blob([decryptedData])) src = URL.createObjectURL(new Blob([new Uint8Array(decryptedData)]))
} }
} else {
src = url
} }
}) })
@@ -48,6 +73,6 @@
<Icon icon={LinkRound} size={3} class="inline-block" /> <Icon icon={LinkRound} size={3} class="inline-block" />
{displayUrl(url)} {displayUrl(url)}
</a> </a>
{:else} {:else if src}
<img alt="" {src} onerror={onError} {...props} /> <img alt="" {src} onerror={onError} {...props} />
{/if} {/if}
+135
View File
@@ -0,0 +1,135 @@
<script lang="ts">
import {fromNostrURI} from "@welshman/util"
import {nthEq} from "@welshman/lib"
import {
parse,
truncate,
renderAsHtml,
isText,
isEmoji,
isTopic,
isCode,
isCashu,
isInvoice,
isLink,
isProfile,
isEvent,
isAddress,
isNewline,
} from "@welshman/content"
import type {Parsed} from "@welshman/content"
import Link from "@lib/components/Link.svelte"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ContentToken from "@app/components/ContentToken.svelte"
import ContentEmoji from "@app/components/ContentEmoji.svelte"
import ContentCode from "@app/components/ContentCode.svelte"
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
import ContentNewline from "@app/components/ContentNewline.svelte"
import ContentTopic from "@app/components/ContentTopic.svelte"
import ContentMention from "@app/components/ContentMention.svelte"
import {entityLink, userSettingsValues} from "@app/core/state"
interface Props {
event: any
trimParent?: boolean
url?: string
}
const {event, trimParent = false, url}: Props = $props()
const fullContent = parse(event)
const isBoundary = (i: number) => {
const parsed = fullContent[i]
if (!parsed || isNewline(parsed)) return true
if (isText(parsed)) return Boolean(parsed.value.match(/^\s+$/))
return false
}
const isStart = (i: number) => isBoundary(i - 1)
const isEnd = (i: number) => isBoundary(i + 1)
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
const isQuote = (p: Parsed) => isEvent(p) || isAddress(p)
const ignoreWarning = () => {
warning = null
}
let warning = $state(
$userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
)
const dropWhile = <T,>(f: (x: T) => boolean, xs: Iterable<T>) => {
const result: T[] = []
for (const x of xs) {
if (result.length === 0 && f(x)) {
continue
}
result.push(x)
}
return result
}
const shortContent = $derived.by(() => {
let result = fullContent
if (trimParent && result.length > 0 && isQuote(result[0])) {
result = dropWhile(p => isQuote(p) || isNewline(p), result)
}
return truncate(result, {minLength: 200, maxLength: 300, mediaLength: 20})
})
</script>
<div class="relative">
{#if warning}
<div class="card2 card2-sm bg-alt row-2">
<Icon icon={Danger} />
<p>
This note has been flagged by the author as "{warning}".<br />
<Button class="link" onclick={ignoreWarning}>Show anyway</Button>
</p>
</div>
{:else}
<div class="overflow-hidden text-ellipsis break-words">
{#each shortContent as parsed, i}
{#if isNewline(parsed)}
<ContentNewline value={parsed.value} />
{:else if isTopic(parsed)}
<ContentTopic value={parsed.value} />
{:else if isEmoji(parsed)}
<ContentEmoji value={parsed.value} />
{:else if isCode(parsed)}
<ContentCode
value={parsed.value}
isBlock={isStartAndEnd(i) || parsed.value.includes("\n")} />
{:else if isCashu(parsed) || isInvoice(parsed)}
<ContentToken value={parsed.value} />
{:else if isLink(parsed)}
<ContentLinkInline value={parsed.value} />
{:else if isProfile(parsed)}
<ContentMention value={parsed.value} {url} />
{:else if isQuote(parsed)}
<Link
external
class="overflow-hidden text-ellipsis whitespace-nowrap underline"
href={entityLink(parsed.raw)}>
{fromNostrURI(parsed.raw).slice(0, 16) + "…"}
</Link>
{:else}
{@html renderAsHtml(parsed)}
{/if}
{/each}
</div>
{/if}
</div>
+6 -9
View File
@@ -6,20 +6,17 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte" import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
import {deriveEvent, entityLink} from "@app/core/state" import {deriveEvent, entityLink} from "@app/core/state"
import {goToEvent} from "@app/util/routes" import {goToEvent} from "@app/util/routes"
type Props = { type Props = {
value: any value: any
hideMediaAtDepth: number
event: TrustedEvent event: TrustedEvent
depth: number
url?: string url?: string
minimal?: boolean
} }
const {value, event, depth, hideMediaAtDepth, url, minimal}: Props = $props() const {value, event, url}: Props = $props()
const {id, identifier, kind, pubkey, relays = []} = value const {id, identifier, kind, pubkey, relays = []} = value
const idOrAddress = id || new Address(kind, pubkey, identifier).toString() const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
@@ -43,17 +40,17 @@
} }
</script> </script>
<Button class="my-2 block max-w-full text-left" {onclick}> <Button class="my-2 block w-full max-w-full text-left" {onclick}>
{#if $quote} {#if $quote}
{#if minimal && $quote.kind === MESSAGE} {#if $quote.kind === MESSAGE}
<div <div
class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90" class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90"
style="background-color: color-mix(in srgb, var(--primary) 10%, var(--base-300) 90%);"> style="background-color: color-mix(in srgb, var(--primary) 10%, var(--base-300) 90%);">
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} /> <NoteContentMinimal trimParent {url} event={$quote} />
</div> </div>
{:else} {:else}
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4"> <NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} /> <NoteContentMinimal {url} event={$quote} />
</NoteCard> </NoteCard>
{/if} {/if}
{:else} {:else}
+9 -9
View File
@@ -4,39 +4,39 @@
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.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 Content from "@app/components/Content.svelte" import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
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}
<span class="text-nowrap">{formatTimestamp(earliest.created_at)}</span> <span class="text-nowrap">{formatTimestamp(earliest.created_at)}</span>
</div> </div>
<Content minimalQuote minLength={100} maxLength={400} event={earliest} /> <NoteContentMinimal event={earliest} />
</div> </div>
</div> </div>
<div class="ml-13 flex items-center justify-between"> <div class="ml-13 flex items-center justify-between">
@@ -67,7 +67,7 @@
{formatTimestamp(latest.created_at)} {formatTimestamp(latest.created_at)}
</span> </span>
</div> </div>
<Content minimalQuote minLength={100} maxLength={400} event={latest} /> <NoteContentMinimal event={latest} />
</div> </div>
</Button> </Button>
{/if} {/if}
+17 -6
View File
@@ -1,20 +1,24 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT} from "@welshman/util" import {COMMENT} from "@welshman/util"
import {pubkey} from "@welshman/app" import {pubkey, relaysByUrl} from "@welshman/app"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl"
import {setKey} from "@lib/implicit"
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 EventInfo from "@app/components/EventInfo.svelte" import EventInfo from "@app/components/EventInfo.svelte"
import EventReport from "@app/components/EventReport.svelte" import EventReport from "@app/components/EventReport.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 {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl" import {makeSpaceChatPath} from "@app/util/routes"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl"
type Props = { type Props = {
url: string url: string
@@ -32,7 +36,14 @@
const showInfo = () => pushModal(EventInfo, {url, event}) const showInfo = () => pushModal(EventInfo, {url, event})
const share = () => pushModal(EventShare, {url, event}) const share = async () => {
if (hasNip29($relaysByUrl.get(url))) {
pushModal(EventShare, {url, event})
} else {
setKey("share", event)
goto(makeSpaceChatPath(url))
}
}
const showDelete = () => pushModal(EventDeleteConfirm, {url, event}) const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
+10 -10
View File
@@ -4,14 +4,14 @@
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"
import {setKey} from "@lib/implicit"
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 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"
import {setKey} from "@lib/implicit"
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>
+22 -15
View File
@@ -1,23 +1,27 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue} from "@welshman/util"
import Link from "@lib/components/Link.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"
import EventActions from "@app/components/EventActions.svelte" import EventActions from "@app/components/EventActions.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} from "@app/util/routes" import {makeGoalPath, makeSpacePath} from "@app/util/routes"
interface Props { interface Props {
url: any url: string
event: any event: TrustedEvent
showRoom?: boolean
showActivity?: boolean showActivity?: boolean
} }
const {url, event, showActivity = false}: Props = $props() const {url, event, showRoom, showActivity}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const path = makeGoalPath(url, event.id) const path = makeGoalPath(url, event.id)
const h = getTagValue("h", event.tags)
const shouldProtect = canEnforceNip70(url)
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await shouldProtect}) publishDelete({relays: [url], event, protect: await shouldProtect})
@@ -26,13 +30,16 @@
publishReaction({...template, event, relays: [url], protect: await shouldProtect}) publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script> </script>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-grow flex-wrap justify-end gap-2">
<div class="flex flex-grow flex-wrap justify-end gap-2"> {#if h && showRoom}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" /> <Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
<ThunkStatusOrDeleted {event} /> Posted in #<RoomName {h} {url} />
{#if showActivity} </Link>
<EventActivity {url} {path} {event} /> {/if}
{/if} <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
<EventActions {url} {event} hideZap noun="Goal" /> <ThunkStatusOrDeleted {event} />
</div> {#if showActivity}
<EventActivity {url} {path} {event} />
{/if}
<EventActions {url} {event} hideZap noun="Goal" />
</div> </div>
+10 -1
View File
@@ -18,7 +18,12 @@
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
import {canEnforceNip70} from "@app/core/commands" import {canEnforceNip70} from "@app/core/commands"
const {url} = $props() type Props = {
url: string
h?: string
}
const {url, h}: Props = $props()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -59,6 +64,10 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
if (h) {
tags.push(["h", h])
}
publishThunk({ publishThunk({
relays: [url], relays: [url],
event: makeEvent(ZAP_GOAL, {content, tags}), event: makeEvent(ZAP_GOAL, {content, tags}),
+6 -1
View File
@@ -6,6 +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 RoomLink from "@app/components/RoomLink.svelte"
import {makeGoalPath} from "@app/util/routes" import {makeGoalPath} from "@app/util/routes"
type Props = { type Props = {
@@ -16,9 +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 h = getTagValue("h", event.tags)
</script> </script>
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeGoalPath(url, event.id)}> <Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl" href={makeGoalPath(url, event.id)}>
<p class="text-2xl">{event.content}</p> <p class="text-2xl">{event.content}</p>
<Content <Content
event={{content: summary, tags: event.tags}} event={{content: summary, tags: event.tags}}
@@ -30,6 +32,9 @@
<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 h}
in <RoomLink {url} {h} />
{/if}
</span> </span>
<GoalActions showActivity {url} {event} /> <GoalActions showActivity {url} {event} />
</div> </div>
+4 -3
View File
@@ -9,11 +9,12 @@
import ZapButton from "@app/components/ZapButton.svelte" import ZapButton from "@app/components/ZapButton.svelte"
type Props = { type Props = {
url: string url?: string
event: TrustedEvent event: TrustedEvent
class?: string
} }
const {url, event}: Props = $props() const {url, event, ...props}: Props = $props()
const zaps = deriveEventsMapped<Zap>(repository, { const zaps = deriveEventsMapped<Zap>(repository, {
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}], filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
@@ -27,7 +28,7 @@
const daysOld = Math.ceil((now() - event.created_at) / DAY) const daysOld = Math.ceil((now() - event.created_at) / DAY)
</script> </script>
<div class="card2 bg-alt flex flex-col gap-8"> <div class="flex flex-col gap-8 {props.class}">
<div class="flex gap-8"> <div class="flex gap-8">
<div> <div>
<p class="text-xl text-primary">{zapAmount} sats</p> <p class="text-xl text-primary">{zapAmount} sats</p>
+63
View File
@@ -0,0 +1,63 @@
<script lang="ts">
import {createSearch} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
const iconModules = import.meta.glob("@assets/icons/*.svg", {
query: "?dataurl",
eager: true,
})
const icons = Object.entries(iconModules)
.map(([path, module]) => {
const name = path.split("/").pop()?.replace(".svg", "") || ""
return {
name,
url: (module as any).default,
searchText: name.replace(/[-_]/g, " ").toLowerCase(),
}
})
.filter(icon => icon.name && !icon.name.startsWith("icon-") && icon.name !== "index")
.sort((a, b) => a.name.localeCompare(b.name))
const iconSearch = createSearch(icons, {
getValue: icon => icon.name,
fuseOptions: {
keys: ["name", "searchText"],
threshold: 0.4,
},
})
type Props = {
onSelect: (iconUrl: string) => void
}
const {onSelect}: Props = $props()
let searchTerm = $state("")
const filteredIcons = $derived(searchTerm ? iconSearch.searchOptions(searchTerm) : icons)
const handleSelect = (iconUrl: string) => {
onSelect(iconUrl)
}
</script>
<div class="w-96 rounded-box bg-base-100 p-4 shadow-lg">
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Magnifier} />
<input bind:value={searchTerm} class="grow" type="text" placeholder="Search icons..." />
</label>
<div class="mt-2 max-h-80 overflow-y-auto">
<div class="grid grid-cols-8 gap-2 p-2">
{#each filteredIcons as icon}
<button
class="flex aspect-square items-center justify-center rounded-box transition-colors hover:bg-primary hover:text-primary-content"
onclick={() => handleSelect(icon.url)}
title={icon.name}>
<Icon icon={icon.url} class="h-6 w-6" />
</button>
{/each}
</div>
</div>
</div>
+1 -1
View File
@@ -35,7 +35,7 @@
{/snippet} {/snippet}
</CardButton> </CardButton>
</Button> </Button>
<Button onclick={signUp} class="dark:btn-neutral"> <Button onclick={signUp} class="btn-neutral">
<CardButton> <CardButton>
{#snippet icon()} {#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div> <div><Icon icon={AddCircle} size={7} /></div>
+1 -4
View File
@@ -17,7 +17,6 @@
import {pushModal, clearModals} from "@app/util/modal" import {pushModal, clearModals} from "@app/util/modal"
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state" import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {loadUserData} from "@app/core/requests"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
let signers: any[] = $state([]) let signers: any[] = $state([])
@@ -27,9 +26,7 @@
const signUp = () => pushModal(SignUp) const signUp = () => pushModal(SignUp)
const onSuccess = async (session: Session, relays: string[] = []) => { const onSuccess = async (session: Session) => {
await loadUserData(session.pubkey, relays)
addSession(session) addSession(session)
pushToast({message: "Successfully logged in!"}) pushToast({message: "Successfully logged in!"})
setChecked("*") setChecked("*")
+9 -6
View File
@@ -14,7 +14,6 @@
import BunkerConnect from "@app/components/BunkerConnect.svelte" import BunkerConnect from "@app/components/BunkerConnect.svelte"
import BunkerUrl from "@app/components/BunkerUrl.svelte" import BunkerUrl from "@app/components/BunkerUrl.svelte"
import {Nip46Controller} from "@app/util/nip46" import {Nip46Controller} from "@app/util/nip46"
import {loadUserData} from "@app/core/requests"
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -32,8 +31,6 @@
onNostrConnect: async (response: Nip46ResponseWithResult) => { onNostrConnect: async (response: Nip46ResponseWithResult) => {
const pubkey = await controller.broker.getPublicKey() const pubkey = await controller.broker.getPublicKey()
await loadUserData(pubkey)
loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS) loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS)
setChecked("*") setChecked("*")
clearModals() clearModals()
@@ -48,13 +45,20 @@
try { try {
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl($bunker) const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl($bunker)
if (!signerPubkey || relays.length === 0) { if (!signerPubkey) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Sorry, it looks like that's an invalid bunker link.", message: "Sorry, it looks like that's an invalid bunker link.",
}) })
} }
if (relays.length === 0) {
return pushToast({
theme: "error",
message: "That bunker link does not include any relays.",
})
}
controller.loading.set(true) controller.loading.set(true)
const {clientSecret} = controller const {clientSecret} = controller
@@ -67,8 +71,6 @@
broker.cleanup() broker.cleanup()
controller.stop() controller.stop()
await loadUserData(pubkey)
loginWithNip46(pubkey, clientSecret, signerPubkey, relays) loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
} else { } else {
return pushToast({ return pushToast({
@@ -91,6 +93,7 @@
} }
const selectConnect = () => { const selectConnect = () => {
controller.loading.set(false)
mode = "connect" mode = "connect"
} }
-3
View File
@@ -16,7 +16,6 @@
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 PasswordResetRequest from "@app/components/PasswordResetRequest.svelte" import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
import {loadUserData} from "@app/core/requests"
import {clearModals, pushModal} from "@app/util/modal" import {clearModals, pushModal} from "@app/util/modal"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -96,8 +95,6 @@
const pubkey = await broker.getPublicKey() const pubkey = await broker.getPublicKey()
const session = makeNip46Session(pubkey, clientSecret, response.event.pubkey, relays) const session = makeNip46Session(pubkey, clientSecret, response.event.pubkey, relays)
await loadUserData(pubkey)
addSession({...session, email}) addSession({...session, email})
broker.cleanup() broker.cleanup()
setChecked("*") setChecked("*")
+23 -6
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import Server from "@assets/icons/server.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl"
import Moon from "@assets/icons/moon.svg?dataurl"
import Settings from "@assets/icons/settings-minimalistic.svg?dataurl" import Settings from "@assets/icons/settings-minimalistic.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import Exit from "@assets/icons/logout-3.svg?dataurl" import Exit from "@assets/icons/logout-3.svg?dataurl"
@@ -13,13 +14,16 @@
import LogOut from "@app/components/LogOut.svelte" import LogOut from "@app/components/LogOut.svelte"
import {PLATFORM_NAME} from "@app/core/state" import {PLATFORM_NAME} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {theme} from "@app/util/theme"
const logout = () => pushModal(LogOut) const logout = () => pushModal(LogOut)
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
</script> </script>
<div class="column menu gap-2"> <div class="column menu gap-2">
<Link replaceState href="/settings/profile"> <Link replaceState href="/settings/profile">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={UserRounded} size={7} /></div> <div><Icon icon={UserRounded} size={7} /></div>
{/snippet} {/snippet}
@@ -32,7 +36,7 @@
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/alerts"> <Link replaceState href="/settings/alerts">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Bell} size={7} /></div> <div><Icon icon={Bell} size={7} /></div>
{/snippet} {/snippet}
@@ -45,7 +49,7 @@
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/wallet"> <Link replaceState href="/settings/wallet">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Wallet} size={7} /></div> <div><Icon icon={Wallet} size={7} /></div>
{/snippet} {/snippet}
@@ -58,7 +62,7 @@
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/relays"> <Link replaceState href="/settings/relays">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Server} size={7} /></div> <div><Icon icon={Server} size={7} /></div>
{/snippet} {/snippet}
@@ -71,7 +75,7 @@
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/content"> <Link replaceState href="/settings/content">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Settings} size={7} /></div> <div><Icon icon={Settings} size={7} /></div>
{/snippet} {/snippet}
@@ -83,8 +87,21 @@
{/snippet} {/snippet}
</CardButton> </CardButton>
</Link> </Link>
<Button onclick={toggleTheme}>
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Moon} size={7} /></div>
{/snippet}
{#snippet title()}
<div>Theme</div>
{/snippet}
{#snippet info()}
<div>Switch between light and dark mode</div>
{/snippet}
</CardButton>
</Button>
<Link replaceState href="/settings/about"> <Link replaceState href="/settings/about">
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><Icon icon={Code2} size={7} /></div> <div><Icon icon={Code2} size={7} /></div>
{/snippet} {/snippet}
+113 -58
View File
@@ -1,14 +1,18 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {displayRelayUrl, getTagValue} from "@welshman/util" import {derived} from "svelte/store"
import {displayRelayUrl, getTagValue, EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
import {deriveRelay} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl" import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Exit from "@assets/icons/logout-3.svg?dataurl" import Exit from "@assets/icons/logout-3.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Login from "@assets/icons/login-3.svg?dataurl" import Login from "@assets/icons/login-3.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl" import History from "@assets/icons/history.svg?dataurl"
import Tuning2 from "@assets/icons/tuning-2.svg?dataurl"
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl" import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl" import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl" import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
@@ -16,32 +20,39 @@
import ChatRound from "@assets/icons/chat-round.svg?dataurl" import ChatRound from "@assets/icons/chat-round.svg?dataurl"
import Bell from "@assets/icons/bell.svg?dataurl" import Bell from "@assets/icons/bell.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 Popover from "@lib/components/Popover.svelte" import Popover from "@lib/components/Popover.svelte"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte" import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte" import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte" import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
import SpaceDetail from "@app/components/SpaceDetail.svelte"
import SpaceInvite from "@app/components/SpaceInvite.svelte" import SpaceInvite from "@app/components/SpaceInvite.svelte"
import SpaceExit from "@app/components/SpaceExit.svelte" import SpaceExit from "@app/components/SpaceExit.svelte"
import SpaceJoin from "@app/components/SpaceJoin.svelte" import SpaceJoin from "@app/components/SpaceJoin.svelte"
import RelayName from "@app/components/RelayName.svelte"
import ProfileList from "@app/components/ProfileList.svelte" import ProfileList from "@app/components/ProfileList.svelte"
import AlertAdd from "@app/components/AlertAdd.svelte" import AlertAdd from "@app/components/AlertAdd.svelte"
import Alerts from "@app/components/Alerts.svelte" import Alerts from "@app/components/Alerts.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte" import RoomCreate from "@app/components/RoomCreate.svelte"
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte" import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
import { import {
ENABLE_ZAPS, ENABLE_ZAPS,
userRoomsByUrl, CONTENT_KINDS,
hasMembershipUrl, deriveSpaceMembers,
memberships, deriveEventsForUrl,
deriveUserRooms, deriveUserRooms,
deriveOtherRooms, deriveOtherRooms,
userSpaceUrls,
hasNip29, hasNip29,
alerts, alerts,
deriveUserCanCreateRoom,
deriveUserIsSpaceAdmin,
} from "@app/core/state" } from "@app/core/state"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath, makeChatPath} from "@app/util/routes"
const {url} = $props() const {url} = $props()
@@ -52,7 +63,14 @@
const calendarPath = makeSpacePath(url, "calendar") const calendarPath = makeSpacePath(url, "calendar")
const userRooms = deriveUserRooms(url) const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url) const otherRooms = deriveOtherRooms(url)
const members = deriveSpaceMembers(url)
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url))) const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const spaceKinds = derived(
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
$events => new Set($events.map(e => e.kind)),
)
const openMenu = () => { const openMenu = () => {
showMenu = true showMenu = true
@@ -62,13 +80,17 @@
showMenu = !showMenu showMenu = !showMenu
} }
const showDetail = () => pushModal(SpaceDetail, {url}, {replaceState})
const showMembers = () => const showMembers = () =>
pushModal( pushModal(
ProfileList, ProfileList,
{url, pubkeys: members, title: `Members of`, subtitle: displayRelayUrl(url)}, {url, pubkeys: $members, title: `Members of`, subtitle: displayRelayUrl(url)},
{replaceState}, {replaceState},
) )
const canCreateRoom = deriveUserCanCreateRoom(url)
const createInvite = () => pushModal(SpaceInvite, {url}, {replaceState}) const createInvite = () => pushModal(SpaceInvite, {url}, {replaceState})
const leaveSpace = () => pushModal(SpaceExit, {url}, {replaceState}) const leaveSpace = () => pushModal(SpaceExit, {url}, {replaceState})
@@ -88,10 +110,6 @@
let replaceState = $state(false) let replaceState = $state(false)
let element: Element | undefined = $state() let element: Element | undefined = $state()
const members = $derived(
$memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey),
)
onMount(() => { onMount(() => {
replaceState = Boolean(element?.closest(".drawer")) replaceState = Boolean(element?.closest(".drawer"))
}) })
@@ -100,23 +118,22 @@
<div bind:this={element} class="flex h-full flex-col justify-between"> <div bind:this={element} class="flex h-full flex-col justify-between">
<SecondaryNavSection> <SecondaryNavSection>
<div> <div>
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}> <Button
<strong class="ellipsize flex items-center gap-3"> class="flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
{displayRelayUrl(url)} onclick={openMenu}>
</strong> <div class="flex items-center justify-between">
<Icon icon={AltArrowDown} /> <strong class="ellipsize flex items-center gap-1">
</SecondaryNavItem> <RelayName {url} />
</strong>
<Icon icon={AltArrowDown} />
</div>
<span class="text-xs text-primary">{displayRelayUrl(url)}</span>
</Button>
{#if showMenu} {#if showMenu}
<Popover hideOnClick onClose={toggleMenu}> <Popover hideOnClick onClose={toggleMenu}>
<ul <ul
transition:fly transition:fly
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl"> class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl">
<li>
<Button onclick={showMembers}>
<Icon icon={UserRounded} />
View Members ({members.length})
</Button>
</li>
<li> <li>
<Button onclick={createInvite}> <Button onclick={createInvite}>
<Icon icon={LinkRound} /> <Icon icon={LinkRound} />
@@ -124,7 +141,34 @@
</Button> </Button>
</li> </li>
<li> <li>
{#if $userRoomsByUrl.has(url)} <Button onclick={showDetail}>
<Icon icon={RemoteControllerMinimalistic} />
Space Information
</Button>
</li>
<li>
<Button onclick={showMembers}>
<Icon icon={UserRounded} />
View Members ({$members.length})
</Button>
</li>
{#if $userIsAdmin}
<li>
<Link external href="https://landlubber.coracle.social">
<Icon icon={Tuning2} />
Manage Space
</Link>
</li>
{:else if $relay?.pubkey}
<li>
<Link href={makeChatPath([$relay.pubkey])}>
<Icon icon={Letter} />
Contact Owner
</Link>
</li>
{/if}
<li>
{#if $userSpaceUrls.includes(url)}
<Button onclick={leaveSpace} class="text-error"> <Button onclick={leaveSpace} class="text-error">
<Icon icon={Exit} /> <Icon icon={Exit} />
Leave Space Leave Space
@@ -141,10 +185,19 @@
{/if} {/if}
</div> </div>
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto"> <div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto">
<SecondaryNavItem {replaceState} href={makeSpacePath(url)}> {#if hasNip29($relay)}
<Icon icon={HomeSmile} /> Home <SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
</SecondaryNavItem> <Icon icon={History} /> Recent Activity
{#if ENABLE_ZAPS} </SecondaryNavItem>
{:else}
<SecondaryNavItem
{replaceState}
href={chatPath}
notification={$notifications.has(chatPath)}>
<Icon icon={ChatRound} /> Chat
</SecondaryNavItem>
{/if}
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
<SecondaryNavItem <SecondaryNavItem
{replaceState} {replaceState}
href={goalsPath} href={goalsPath}
@@ -152,25 +205,29 @@
<Icon icon={StarFallMinimalistic} /> Goals <Icon icon={StarFallMinimalistic} /> Goals
</SecondaryNavItem> </SecondaryNavItem>
{/if} {/if}
<SecondaryNavItem {#if $spaceKinds.has(THREAD)}
{replaceState} <SecondaryNavItem
href={threadsPath} {replaceState}
notification={$notifications.has(threadsPath)}> href={threadsPath}
<Icon icon={NotesMinimalistic} /> Threads notification={$notifications.has(threadsPath)}>
</SecondaryNavItem> <Icon icon={NotesMinimalistic} /> Threads
<SecondaryNavItem </SecondaryNavItem>
{replaceState} {/if}
href={calendarPath} {#if $spaceKinds.has(EVENT_TIME)}
notification={$notifications.has(calendarPath)}> <SecondaryNavItem
<Icon icon={CalendarMinimalistic} /> Calendar {replaceState}
</SecondaryNavItem> href={calendarPath}
notification={$notifications.has(calendarPath)}>
<Icon icon={CalendarMinimalistic} /> Calendar
</SecondaryNavItem>
{/if}
{#if hasNip29($relay)} {#if hasNip29($relay)}
{#if $userRooms.length > 0} {#if $userRooms.length > 0}
<div class="h-2"></div> <div class="h-2"></div>
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader> <SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
{/if} {/if}
{#each $userRooms as room, i (room)} {#each $userRooms as h, i (h)}
<MenuSpaceRoomItem {replaceState} notify {url} {room} /> <MenuSpaceRoomItem {replaceState} notify {url} {h} />
{/each} {/each}
{#if $otherRooms.length > 0} {#if $otherRooms.length > 0}
<div class="h-2"></div> <div class="h-2"></div>
@@ -182,27 +239,25 @@
{/if} {/if}
</SecondaryNavHeader> </SecondaryNavHeader>
{/if} {/if}
{#each $otherRooms as room, i (room)} {#each $otherRooms as h, i (h)}
<MenuSpaceRoomItem {replaceState} {url} {room} /> <MenuSpaceRoomItem {replaceState} {url} {h} />
{/each} {/each}
<SecondaryNavItem {replaceState} onclick={addRoom}> {#if $canCreateRoom}
<Icon icon={AddCircle} /> <SecondaryNavItem {replaceState} onclick={addRoom}>
Create room <Icon icon={AddCircle} />
</SecondaryNavItem> Create room
{:else} </SecondaryNavItem>
<SecondaryNavItem {/if}
{replaceState}
href={chatPath}
notification={$notifications.has(chatPath)}>
<Icon icon={ChatRound} /> Chat
</SecondaryNavItem>
{/if} {/if}
</div> </div>
</SecondaryNavSection> </SecondaryNavSection>
<div class="p-4"> <div class="flex flex-col gap-2 p-4">
<button class="btn btn-neutral btn-sm w-full" onclick={manageAlerts}> <Button class="btn btn-neutral btn-sm" onclick={showDetail}>
<SocketStatusIndicator {url} />
</Button>
<Button class="btn btn-neutral btn-sm" onclick={manageAlerts}>
<Icon icon={Bell} /> <Icon icon={Bell} />
Manage Alerts Manage Alerts
</button> </Button>
</div> </div>
</div> </div>
+7 -2
View File
@@ -6,17 +6,22 @@
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {pushDrawer} from "@app/util/modal" import {pushDrawer} from "@app/util/modal"
import {deriveSocketStatus} from "@app/core/state"
const {url} = $props() const {url} = $props()
const path = makeSpacePath(url) const path = makeSpacePath(url) + ":mobile"
const status = deriveSocketStatus(url)
const openMenu = () => pushDrawer(MenuSpace, {url}) const openMenu = () => pushDrawer(MenuSpace, {url})
</script> </script>
<Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden"> <Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
<Icon icon={MenuDots} /> <Icon icon={MenuDots} />
{#if $notifications.has(path)} {#if $status.theme !== "success"}
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-{$status.theme}"></div>
{:else if $notifications.has(path)}
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary"></div> <div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary"></div>
{/if} {/if}
</Button> </Button>
+5 -17
View File
@@ -1,36 +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?.closed || $channel?.private} <RoomNameWithImage {url} {h} />
<Icon icon={Lock} size={4} />
{:else}
<Icon icon={Hashtag} />
{/if}
<div class="min-w-0 overflow-hidden text-ellipsis">
<ChannelName {url} {room} />
</div>
</SecondaryNavItem> </SecondaryNavItem>
-39
View File
@@ -1,39 +0,0 @@
<script lang="ts">
import Login from "@assets/icons/login-3.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Divider from "@lib/components/Divider.svelte"
import CardButton from "@lib/components/CardButton.svelte"
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import {userRoomsByUrl, PLATFORM_RELAYS} from "@app/core/state"
import {pushModal} from "@app/util/modal"
const addSpace = () => pushModal(SpaceAdd)
</script>
<div class="column menu gap-2">
{#each PLATFORM_RELAYS as url (url)}
<MenuSpacesItem {url} />
{:else}
{#if $userRoomsByUrl.size > 0}
{#each $userRoomsByUrl.keys() as url (url)}
<MenuSpacesItem {url} />
{/each}
<Divider />
{/if}
<Button onclick={addSpace}>
<CardButton class="dark:btn-neutral">
{#snippet icon()}
<div><Icon icon={Login} size={7} /></div>
{/snippet}
{#snippet title()}
<div>Add a space</div>
{/snippet}
{#snippet info()}
<div>Join or create a new space</div>
{/snippet}
</CardButton>
</Button>
{/each}
</div>
+1 -1
View File
@@ -13,7 +13,7 @@
</script> </script>
<Link replaceState href={path}> <Link replaceState href={path}>
<CardButton class="dark:btn-neutral"> <CardButton class="btn-neutral">
{#snippet icon()} {#snippet icon()}
<div><SpaceAvatar {url} /></div> <div><SpaceAvatar {url} /></div>
{/snippet} {/snippet}
+41 -16
View File
@@ -1,29 +1,54 @@
<script lang="ts"> <script lang="ts">
import {onMount, mount, unmount, createRawSnippet} from "svelte"
import Drawer from "@lib/components/Drawer.svelte" import Drawer from "@lib/components/Drawer.svelte"
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 onKeyDown = (e: any) => { const closeModals = () => {
if (e.code === "Escape" && e.target === document.body) { if ($modal && !$modal.options.noEscape) {
clearModals() clearModals()
} }
} }
const m = $derived($modal) const onKeyDown = (e: any) => {
if (e.code === "Escape" && e.target === document.body) {
closeModals()
}
}
let element: HTMLElement
let instance: any | undefined
onMount(() => {
return modal.subscribe($modal => {
if (instance) {
unmount(instance, {outro: true})
instance = undefined
}
if ($modal) {
const {options, component, props} = $modal
const wrapper = options.drawer ? Drawer : Dialog
instance = mount(wrapper as any, {
target: element,
props: {
onClose: closeModals,
children: createRawSnippet(() => ({
render: () => "<div></div>",
setup: (target: Element) => {
const child = mount(component, {target, props})
return () => unmount(child)
},
})),
},
})
}
})
})
</script> </script>
<svelte:window onkeydown={onKeyDown} /> <svelte:window onkeydown={onKeyDown} />
{#if m?.options?.drawer} <div bind:this={element}></div>
<Drawer onClose={clearModals} {...m.options}>
{#key m.id}
<m.component {...m.props} />
{/key}
</Drawer>
{:else if m}
<Dialog onClose={clearModals} {...m.options}>
{#key m.id}
<m.component {...m.props} />
{/key}
</Dialog>
{/if}
@@ -0,0 +1,39 @@
<script lang="ts">
import {onMount} from "svelte"
import {userSettingsValues} from "@app/core/state"
import {notifications} from "../util/notifications"
let audioElement: HTMLAudioElement
let enabled = $state(false)
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
enabled = true
} else {
enabled = false
}
})
let notificationCount = $state($notifications.size)
const playSound = () => {
if (enabled && $userSettingsValues.play_notification_sound) {
audioElement?.play()
}
}
onMount(() => {
audioElement.load()
notifications.subscribe(notifications => {
if (notifications.size > notificationCount) {
playSound()
}
notificationCount = notifications.size
})
})
</script>
<audio bind:this={audioElement} src="/new-notification-3-398649.mp3"></audio>
+9 -13
View File
@@ -1,24 +1,20 @@
<script lang="ts"> <script lang="ts">
import type {ComponentProps} from "svelte" import type {ComponentProps} from "svelte"
import {EVENT_TIME} from "@welshman/util" import {EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
import NoteContentEventTime from "@app/components/NoteContentEventTime.svelte"
import NoteContentThread from "@app/components/NoteContentThread.svelte"
import NoteContentGoal from "@app/components/NoteContentGoal.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import CalendarEventDate from "@app/components/CalendarEventDate.svelte"
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
const props: ComponentProps<typeof Content> = $props() const props: ComponentProps<typeof Content> = $props()
</script> </script>
{#if props.event.kind === EVENT_TIME} {#if props.event.kind === EVENT_TIME}
<div class="flex items-start gap-4"> <NoteContentEventTime {...props} />
<CalendarEventDate event={props.event} /> {:else if props.event.kind === THREAD}
<div class="flex flex-grow flex-col"> <NoteContentThread {...props} />
<CalendarEventHeader event={props.event} /> {:else if props.event.kind === ZAP_GOAL}
<div class="flex py-2 opacity-50"> <NoteContentGoal {...props} />
<div class="h-px flex-grow bg-base-content opacity-25"></div>
</div>
<Content {...props} />
</div>
</div>
{:else} {:else}
<Content {...props} /> <Content {...props} />
{/if} {/if}
@@ -0,0 +1,19 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import Content from "@app/components/Content.svelte"
import CalendarEventDate from "@app/components/CalendarEventDate.svelte"
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
const props: ComponentProps<typeof Content> = $props()
</script>
<div class="flex items-start gap-4">
<CalendarEventDate event={props.event} />
<div class="flex flex-grow flex-col">
<CalendarEventHeader event={props.event} />
<div class="flex py-2 opacity-50">
<div class="h-px flex-grow bg-base-content opacity-25"></div>
</div>
<Content {...props} />
</div>
</div>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {getTagValue} from "@welshman/util"
import Content from "@app/components/Content.svelte"
import GoalSummary from "@app/components/GoalSummary.svelte"
const props: ComponentProps<typeof Content> = $props()
const content = getTagValue("summary", props.event.tags)
const fakeEvent = {content, tags: props.event.tags}
</script>
<div class="flex flex-col gap-2">
<p class="text-2xl">{props.event.content}</p>
<Content {...props} event={fakeEvent} expandMode="inline" minLength={50} maxLength={300} />
<GoalSummary url={props.url} event={props.event} />
</div>
@@ -0,0 +1,22 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
import NoteContentMinimalEventTime from "@app/components/NoteContentMinimalEventTime.svelte"
import NoteContentMinimalThread from "@app/components/NoteContentMinimalThread.svelte"
import NoteContentMinimalGoal from "@app/components/NoteContentMinimalGoal.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
const props: ComponentProps<typeof ContentMinimal> = $props()
</script>
<div class="text-xs">
{#if props.event.kind === EVENT_TIME}
<NoteContentMinimalEventTime {...props} />
{:else if props.event.kind === THREAD}
<NoteContentMinimalThread {...props} />
{:else if props.event.kind === ZAP_GOAL}
<NoteContentMinimalGoal {...props} />
{:else}
<ContentMinimal {...props} />
{/if}
</div>
@@ -0,0 +1,36 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {
fromPairs,
formatTimestamp,
formatTimestampAsDate,
formatTimestampAsTime,
} from "@welshman/lib"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
const props: ComponentProps<typeof ContentMinimal> = $props()
const meta = $derived(fromPairs(props.event.tags) as Record<string, string>)
const start = $derived(parseInt(meta.start))
const end = $derived(parseInt(meta.end))
</script>
<div class="flex flex-col">
<div class="flex flex-grow flex-wrap justify-between gap-2">
<p class="text-sm">{meta.title || meta.name}</p>
{#if !isNaN(start) && !isNaN(end)}
{@const startDateDisplay = formatTimestampAsDate(start)}
{@const endDateDisplay = formatTimestampAsDate(end)}
{@const isSingleDay = startDateDisplay === endDateDisplay}
<div class="flex items-center gap-2">
<Icon icon={ClockCircle} size={4} />
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
{formatTimestampAsTime(start)}{isSingleDay
? formatTimestampAsTime(end)
: formatTimestamp(end)}
</div>
{/if}
</div>
<ContentMinimal {...props} />
</div>
@@ -0,0 +1,34 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {deriveEventsMapped} from "@welshman/store"
import {repository, getValidZap} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
const props: ComponentProps<typeof ContentMinimal> = $props()
const content = getTagValue("summary", props.event.tags)
const fakeEvent = {content, tags: props.event.tags}
const zaps = deriveEventsMapped<Zap>(repository, {
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
itemToEvent: item => item.response,
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
})
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
</script>
<div class="flex justify-between">
<span class="text-sm">{props.event.content}</span>
<div class="flex items-center gap-1">
<Icon icon={Bolt} size={4} />
{zapAmount}/{goalAmount} sats funded
</div>
</div>
<ContentMinimal {...props} event={fakeEvent} />
@@ -0,0 +1,16 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {getTagValue} from "@welshman/util"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
const props: ComponentProps<typeof ContentMinimal> = $props()
const title = getTagValue("title", props.event.tags)
</script>
{#if title}
<span class="text-sm">{title}</span>
{/if}
{#if props.event.content}
<ContentMinimal {...props} />
{/if}
@@ -0,0 +1,28 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {formatTimestamp} from "@welshman/lib"
import {getTagValue} from "@welshman/util"
import Content from "@app/components/Content.svelte"
const props: ComponentProps<typeof Content> = $props()
const title = getTagValue("title", props.event.tags)
</script>
<div class="flex flex-col gap-2">
{#if title}
<div class="flex w-full items-center justify-between gap-2">
<p class="text-xl">{title}</p>
<p class="text-sm opacity-75">
{formatTimestamp(props.event.created_at)}
</p>
</div>
{:else}
<p class="mb-3 h-0 text-xs opacity-75">
{formatTimestamp(props.event.created_at)}
</p>
{/if}
{#if props.event.content}
<Content {...props} />
{/if}
</div>
+10 -20
View File
@@ -3,22 +3,20 @@
import {page} from "$app/stores" import {page} from "$app/stores"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {splitAt} from "@welshman/lib" import {splitAt} from "@welshman/lib"
import {userProfile} from "@welshman/app" import {userProfile, shouldUnwrap} from "@welshman/app"
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte" import ChatEnable from "@app/components/ChatEnable.svelte"
import MenuSpaces from "@app/components/MenuSpaces.svelte"
import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte" import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte"
import MenuSettings from "@app/components/MenuSettings.svelte" import MenuSettings from "@app/components/MenuSettings.svelte"
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte" import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
import {userRoomsByUrl, canDecrypt, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state" import {userSpaceUrls, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state"
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 Widget from "@assets/icons/widget.svg?dataurl"
import AddSquare from "@assets/icons/add-square.svg?dataurl" import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl" import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
@@ -31,15 +29,11 @@
const {children}: Props = $props() const {children}: Props = $props()
const addSpace = () => pushModal(SpaceAdd)
const showSpacesMenu = () => (spaceUrls.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)
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"})) const openChat = () => ($shouldUnwrap ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
const hasNotification = (url: string) => { const hasNotification = (url: string) => {
const path = makeSpacePath(url) const path = makeSpacePath(url)
@@ -52,9 +46,8 @@
const itemHeight = 56 const itemHeight = 56
const navPadding = 6 * itemHeight const navPadding = 6 * itemHeight
const itemLimit = $derived((windowHeight - navPadding) / itemHeight) const itemLimit = $derived((windowHeight - navPadding) / itemHeight)
const spaceUrls = $derived(Array.from($userRoomsByUrl.keys())) const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, $userSpaceUrls))
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, spaceUrls)) const anySpaceNotifications = $derived($userSpaceUrls.some(hasNotification))
const anySpaceNotifications = $derived(spaceUrls.some(hasNotification))
const otherSpaceNotifications = $derived(secondarySpaceUrls.some(hasNotification)) const otherSpaceNotifications = $derived(secondarySpaceUrls.some(hasNotification))
</script> </script>
@@ -83,8 +76,8 @@
<Avatar icon={Widget} class="!h-10 !w-10" /> <Avatar icon={Widget} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
{/if} {/if}
<PrimaryNavItem title="Add Space" onclick={addSpace} class="tooltip-right"> <PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
<Avatar icon={AddSquare} class="!h-10 !w-10" /> <Avatar icon={Compass} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
{/each} {/each}
</div> </div>
@@ -120,7 +113,7 @@
<div <div
class="border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden"> class="border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
<div class="content-padding-x content-sizing flex justify-between px-2"> <div class="content-padding-x content-sizing flex justify-between px-2">
<div class="flex gap-2 sm:gap-8"> <div class="flex gap-2 sm:gap-6">
<PrimaryNavItem title="Home" href="/home"> <PrimaryNavItem title="Home" href="/home">
<Avatar icon={HomeSmile} class="!h-10 !w-10" /> <Avatar icon={HomeSmile} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
@@ -131,10 +124,7 @@
<Avatar icon={Letter} class="!h-10 !w-10" /> <Avatar icon={Letter} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
{#if PLATFORM_RELAYS.length !== 1} {#if PLATFORM_RELAYS.length !== 1}
<PrimaryNavItem <PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
title="Spaces"
onclick={showSpacesMenu}
notification={anySpaceNotifications}>
<Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" /> <Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
{/if} {/if}
@@ -2,18 +2,18 @@
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte" import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath, goToSpace} from "@app/util/routes"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
const {url} = $props() const {url} = $props()
const path = makeSpacePath(url) const onClick = () => goToSpace(url)
</script> </script>
<PrimaryNavItem <PrimaryNavItem
onclick={onClick}
title={displayRelayUrl(url)} title={displayRelayUrl(url)}
href={path}
class="tooltip-right" class="tooltip-right"
notification={$notifications.has(path)}> notification={$notifications.has(makeSpacePath(url))}>
<SpaceAvatar {url} /> <SpaceAvatar {url} />
</PrimaryNavItem> </PrimaryNavItem>
+12 -8
View File
@@ -5,11 +5,15 @@
import type {Filter} from "@welshman/util" import type {Filter} from "@welshman/util"
import {deriveEvents} from "@welshman/store" import {deriveEvents} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {NOTE, ROOMS, MESSAGE, THREAD, COMMENT, getRelayTags, getListTags} from "@welshman/util" import {NOTE, ROOMS, COMMENT} from "@welshman/util"
import {repository, loadRelaySelections} from "@welshman/app" import {repository, loadRelaySelections} 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 {membershipsByPubkey} from "@app/core/state" import {
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"
@@ -21,8 +25,8 @@
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 = deriveEvents(repository, {filters})
const membership = $derived($membershipsByPubkey.get(pubkey)) const selections = deriveGroupSelections(pubkey)
const relays = $derived(getRelayTags(getListTags(membership))) const spaceUrls = $derived(getSpaceUrlsFromGroupSelections($selections))
const viewEvent = () => goToEvent($events[0]!) const viewEvent = () => goToEvent($events[0]!)
@@ -36,7 +40,7 @@
load({ load({
filters: [ filters: [
{authors: [pubkey], kinds: [ROOMS]}, {authors: [pubkey], kinds: [ROOMS]},
{authors: [pubkey], limit: 1, kinds: [NOTE, MESSAGE, THREAD, COMMENT]}, {authors: [pubkey], limit: 1, kinds: [NOTE, COMMENT, ...MESSAGE_KINDS]},
], ],
relays: Router.get().FromPubkeys([pubkey]).getUrls(), relays: Router.get().FromPubkeys([pubkey]).getUrls(),
}) })
@@ -49,10 +53,10 @@
Last active {formatTimestampRelative($events[0].created_at)} Last active {formatTimestampRelative($events[0].created_at)}
</Button> </Button>
{/if} {/if}
{#if relays.length > 0} {#if spaceUrls.length > 0}
<Button onclick={openSpaces} class="badge badge-neutral"> <Button onclick={openSpaces} class="badge badge-neutral">
{relays.length} {spaceUrls.length}
{relays.length === 1 ? "space" : "spaces"} {spaceUrls.length === 1 ? "space" : "spaces"}
</Button> </Button>
{/if} {/if}
</div> </div>
+2 -12
View File
@@ -19,13 +19,7 @@
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 { import {INDEXER_RELAYS, PLATFORM_NAME, userSpaceUrls, userWriteRelays} from "@app/core/state"
INDEXER_RELAYS,
PLATFORM_NAME,
userMembership,
getMembershipUrls,
userWriteRelays,
} from "@app/core/state"
let progress: number | undefined = $state(undefined) let progress: number | undefined = $state(undefined)
let confirmText = $state("") let confirmText = $state("")
@@ -46,11 +40,7 @@
const profileEvent = makeEvent(PROFILE, createProfile({name: "[deleted]"})) const profileEvent = makeEvent(PROFILE, createProfile({name: "[deleted]"}))
const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]}) const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
const denominator = chunks.length + 2 const denominator = chunks.length + 2
const relays = uniq([ const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls])
...INDEXER_RELAYS,
...$userWriteRelays,
...getMembershipUrls($userMembership),
])
let step = 0 let step = 0
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {shouldUnwrap} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl" import Letter from "@assets/icons/letter-opened.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -11,7 +12,7 @@
import ProfileInfo from "@app/components/ProfileInfo.svelte" import ProfileInfo from "@app/components/ProfileInfo.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 {canDecrypt, pubkeyLink} from "@app/core/state" import {pubkeyLink} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {makeChatPath} from "@app/util/routes" import {makeChatPath} from "@app/util/routes"
@@ -26,7 +27,7 @@
const chatPath = makeChatPath([pubkey]) const chatPath = makeChatPath([pubkey])
const openChat = () => ($canDecrypt ? goto(chatPath) : pushModal(ChatEnable, {next: chatPath})) const openChat = () => ($shouldUnwrap ? goto(chatPath) : pushModal(ChatEnable, {next: chatPath}))
</script> </script>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
+5 -28
View File
@@ -1,22 +1,13 @@
<script lang="ts"> <script lang="ts">
import {nthNe} from "@welshman/lib"
import type {Profile} from "@welshman/util" import type {Profile} from "@welshman/util"
import { import {getTag, makeProfile} from "@welshman/util"
getTag, import {pubkey, profilesByPubkey} from "@welshman/app"
makeEvent,
makeProfile,
editProfile,
createProfile,
isPublishedProfile,
uniqTags,
} from "@welshman/util"
import {Router} from "@welshman/router"
import {pubkey, profilesByPubkey, publishThunk} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileEditForm from "@app/components/ProfileEditForm.svelte" import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {PROTECTED, getMembershipUrls, userMembership} from "@app/core/state" import {PROTECTED} from "@app/core/state"
import {updateProfile} from "../core/commands"
const profile = $profilesByPubkey.get($pubkey!) || makeProfile() const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []) const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
@@ -25,21 +16,7 @@
const back = () => history.back() const back = () => history.back()
const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => { const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => {
const router = Router.get() updateProfile({profile, shouldBroadcast})
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
const scenarios = [router.FromRelays(getMembershipUrls($userMembership))]
if (shouldBroadcast) {
scenarios.push(router.FromUser(), router.Index())
template.tags = template.tags.filter(nthNe(0, "-"))
} else {
template.tags = uniqTags([...template.tags, PROTECTED])
}
const event = makeEvent(template.kind, template)
const relays = router.merge(scenarios).getUrls()
publishThunk({event, relays})
pushToast({message: "Your profile has been updated!"}) pushToast({message: "Your profile has been updated!"})
clearModals() clearModals()
} }
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {removeNil} from "@welshman/lib" import {removeNil} from "@welshman/lib"
import {deriveProfile} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import Content from "@app/components/Content.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
export type Props = { export type Props = {
pubkey: string pubkey: string
@@ -14,5 +14,5 @@
</script> </script>
{#if $profile} {#if $profile}
<Content event={{content: $profile.about || "", tags: []}} hideMediaAtDepth={0} /> <ContentMinimal event={{content: $profile.about || "", tags: []}} />
{/if} {/if}
+3 -1
View File
@@ -18,6 +18,8 @@
const openProfile = () => pushModal(ProfileDetail, {pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
</script> </script>
<Button onclick={preventDefault(openProfile)} class={cx(props.class, {"link-content": !unstyled})}> <Button
onclick={preventDefault(openProfile)}
class={cx(props.class, {"link-content bg-alt": !unstyled})}>
@<ProfileName {pubkey} {url} /> @<ProfileName {pubkey} {url} />
</Button> </Button>
+3 -2
View File
@@ -8,7 +8,7 @@
import SpaceAvatar from "@app/components/SpaceAvatar.svelte" import SpaceAvatar from "@app/components/SpaceAvatar.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 {getMembershipUrls, membershipsByPubkey} from "@app/core/state" import {deriveGroupSelections, getSpaceUrlsFromGroupSelections} from "@app/core/state"
type Props = { type Props = {
pubkey: string pubkey: string
@@ -16,7 +16,8 @@
const {pubkey}: Props = $props() const {pubkey}: Props = $props()
const spaceUrls = $derived(getMembershipUrls($membershipsByPubkey.get(pubkey))) const selections = deriveGroupSelections(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupSelections($selections))
const back = () => history.back() const back = () => history.back()
</script> </script>
+2 -2
View File
@@ -26,8 +26,8 @@
}) })
</script> </script>
<Button class="max-w-full {props.class}" onclick={copy}> <Button class="flex w-full justify-center {props.class}" onclick={copy}>
<div bind:this={wrapper} style={`height: ${height}px`}> <div bind:this={wrapper} class="w-md" style={`height: ${height}px`}>
<canvas <canvas
class="rounded-box" class="rounded-box"
bind:this={canvas} bind:this={canvas}
+22 -13
View File
@@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames"
import {onMount} from "svelte" import {onMount} from "svelte"
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib" import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
import { import {
REPORT,
REACTION, REACTION,
ZAP_RESPONSE, ZAP_RESPONSE,
getReplyFilters, getReplyFilters,
@@ -10,7 +12,6 @@
getEmojiTag, getEmojiTag,
fromMsats, fromMsats,
getTag, getTag,
REPORT,
DELETE, DELETE,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, EventContent, Zap} from "@welshman/util" import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
@@ -96,7 +97,7 @@
load({ load({
relays: [url], relays: [url],
signal: controller.signal, signal: controller.signal,
filters: getReplyFilters([event], {kinds: [REPORT, DELETE, ...REACTION_KINDS]}), filters: getReplyFilters([event], {kinds: REACTION_KINDS}),
onEvent: batch(300, (events: TrustedEvent[]) => { onEvent: batch(300, (events: TrustedEvent[]) => {
load({ load({
relays: [url], relays: [url],
@@ -118,7 +119,7 @@
<button <button
type="button" type="button"
data-tip={`This content has been reported as "${displayList(reportReasons)}".`} data-tip={`This content has been reported as "${displayList(reportReasons)}".`}
class="btn btn-error btn-xs tooltip-right flex items-center gap-1 rounded-full" class="btn btn-error btn-xs tooltip-right flex items-center gap-1 rounded-full font-normal"
class:tooltip={!noTooltip && !isMobile} class:tooltip={!noTooltip && !isMobile}
onclick={stopPropagation(preventDefault(onReportClick))}> onclick={stopPropagation(preventDefault(onReportClick))}>
<Icon icon={Danger} /> <Icon icon={Danger} />
@@ -134,11 +135,15 @@
<button <button
type="button" type="button"
data-tip={tooltip} data-tip={tooltip}
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full {reactionClass}" class={cx(
class:tooltip={!noTooltip && !isMobile} reactionClass,
class:border={isOwn} "flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full text-xs font-normal",
class:border-solid={isOwn} {
class:border-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>
@@ -152,11 +157,15 @@
<button <button
type="button" type="button"
data-tip={tooltip} data-tip={tooltip}
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full {reactionClass}" class={cx(
class:tooltip={!noTooltip && !isMobile} reactionClass,
class:border={isOwn} "flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal",
class:border-solid={isOwn} {
class:border-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}
+2 -2
View File
@@ -6,6 +6,6 @@
const relay = deriveRelay(props.url) const relay = deriveRelay(props.url)
</script> </script>
{#if $relay?.profile?.description} {#if $relay?.description}
<p class={props.class}>{$relay?.profile.description}</p> <p class={props.class}>{$relay.description}</p>
{/if} {/if}
+11 -11
View File
@@ -4,13 +4,13 @@
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import {displayUrl} from "@welshman/lib" import {displayUrl} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {deriveRelay} from "@welshman/app" import {deriveRelay, deriveRelayStats} from "@welshman/app"
const {url, children} = $props() const {url, children} = $props()
const relay = deriveRelay(url) const relay = deriveRelay(url)
const relayStats = deriveRelayStats(url)
const connections = $derived($relay?.stats?.open_count || 0) const connections = $derived($relayStats?.open_count || 0)
</script> </script>
<div class="card2 card2-sm bg-alt column gap-2"> <div class="card2 card2-sm bg-alt column gap-2">
@@ -21,20 +21,20 @@
</div> </div>
{@render children?.()} {@render children?.()}
</div> </div>
{#if $relay?.profile?.description} {#if $relay?.description}
<p class="ellipsize">{$relay?.profile.description}</p> <p class="ellipsize">{$relay.description}</p>
{/if} {/if}
<span class="flex items-center gap-1 whitespace-nowrap text-sm"> <span class="flex items-center gap-1 whitespace-nowrap text-sm">
{#if $relay?.profile?.contact} {#if $relay?.contact}
<Link external class="ellipsize underline" href={$relay.profile.contact} <Link external class="ellipsize underline" href={$relay.contact}
>{displayUrl($relay.profile.contact)}</Link> >{displayUrl($relay.contact)}</Link>
&bull; &bull;
{/if} {/if}
{#if Array.isArray($relay?.profile?.supported_nips)} {#if Array.isArray($relay?.supported_nips)}
<span <span
class="tooltip cursor-pointer underline" class="tooltip cursor-pointer underline"
data-tip="NIPs supported: {$relay.profile.supported_nips.join(', ')}"> data-tip="NIPs supported: {$relay.supported_nips.join(', ')}">
{$relay.profile.supported_nips.length} NIPs {$relay.supported_nips.length} NIPs
</span> </span>
&bull; &bull;
{/if} {/if}
+8 -7
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import {gt} from "@welshman/lib"
import {deriveRelay} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import Ghost from "@assets/icons/ghost-smile.svg?dataurl" 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"
@@ -7,7 +6,7 @@
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 ProfileCircles from "@app/components/ProfileCircles.svelte" import ProfileCircles from "@app/components/ProfileCircles.svelte"
import {membersByUrl, userRoomsByUrl} from "@app/core/state" import {deriveSpaceMembers, deriveUserRooms} from "@app/core/state"
type Props = { type Props = {
url: string url: string
@@ -15,6 +14,8 @@
const {url}: Props = $props() const {url}: Props = $props()
const relay = deriveRelay(url) const relay = deriveRelay(url)
const rooms = deriveUserRooms(url)
const members = deriveSpaceMembers(url)
</script> </script>
<div class="col-4 text-left"> <div class="col-4 text-left">
@@ -24,14 +25,14 @@
<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?.profile?.icon} {#if $relay?.icon}
<img alt="" src={$relay.profile.icon} /> <img alt="" src={$relay.icon} />
{:else} {:else}
<Icon icon={Ghost} size={5} /> <Icon icon={Ghost} size={5} />
{/if} {/if}
</div> </div>
</div> </div>
{#if $userRoomsByUrl.has(url)} {#if $rooms.includes(url)}
<div <div
class="tooltip absolute -right-1 -top-1 h-5 w-5 rounded-full bg-primary" class="tooltip absolute -right-1 -top-1 h-5 w-5 rounded-full bg-primary"
data-tip="You are already a member of this space."> data-tip="You are already a member of this space.">
@@ -48,10 +49,10 @@
</div> </div>
<RelayDescription {url} /> <RelayDescription {url} />
</div> </div>
{#if gt($membersByUrl.get(url)?.size, 0)} {#if $members.length > 0}
<div class="row-2 card2 card2-sm bg-alt"> <div class="row-2 card2 card2-sm bg-alt">
Members: Members:
<ProfileCircles pubkeys={Array.from($membersByUrl.get(url) || [])} /> <ProfileCircles pubkeys={$members} />
</div> </div>
{/if} {/if}
</div> </div>
+119
View File
@@ -0,0 +1,119 @@
<script lang="ts">
import type {Instance} from "tippy.js"
import {writable} from "svelte/store"
import type {EventContent} from "@welshman/util"
import {isMobile, preventDefault} from "@lib/html"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
import WidgetAdd from "@assets/icons/widget-add.svg?dataurl"
import Plane from "@assets/icons/plane-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Tippy from "@lib/components/Tippy.svelte"
import ComposeMenu from "@app/components/ComposeMenu.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {makeEditor} from "@app/editor"
import {onDestroy, onMount} from "svelte"
type Props = {
url?: string
h?: string
content?: string
onEscape?: () => void
onEditPrevious?: () => void
onSubmit: (event: EventContent) => void
}
const {url, h, content, onEscape, onEditPrevious, onSubmit}: Props = $props()
const autofocus = !isMobile
const uploading = writable(false)
export const focus = () => editor.then(ed => ed.chain().focus().run())
export const canEnterEditPrevious = () =>
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
const handleKeyDown = async (event: KeyboardEvent) => {
if (event.key === "Escape") {
onEscape?.()
}
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
onEditPrevious?.()
}
}
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
const showPopover = () => popover?.show()
const hidePopover = () => popover?.hide()
const submit = async () => {
if ($uploading) return
const ed = await editor
const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = ed.storage.nostr.getEditorTags()
if (!content) return
onSubmit({content, tags})
ed.chain().clearContent().run()
}
const editor = makeEditor({url, content, autofocus, submit, uploading, aggressive: true})
let popover: Instance | undefined = $state()
onMount(async () => {
const ed = await editor
ed.view.dom.addEventListener("keydown", handleKeyDown)
})
onDestroy(async () => {
const ed = await editor
ed?.view?.dom.removeEventListener("keydown", handleKeyDown)
})
</script>
<form class="relative flex gap-2 p-2" onsubmit={preventDefault(submit)}>
<div class="join">
<Button
data-tip="Add an image"
class="center join-item tooltip tooltip-right h-10 w-10 min-w-10 rounded-full border border-solid border-base-200 bg-base-300"
disabled={$uploading}
onclick={uploadFiles}>
{#if $uploading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Icon icon={GallerySend} />
{/if}
</Button>
<Tippy
bind:popover
component={ComposeMenu}
props={{url, h, onClick: hidePopover}}
params={{trigger: "manual", interactive: true}}>
<Button
data-tip="More options"
class="center join-item tooltip tooltip-right h-10 w-10 min-w-10 rounded-full border border-solid border-base-200 bg-base-300"
disabled={$uploading}
onclick={showPopover}>
<Icon icon={WidgetAdd} />
</Button>
</Tippy>
</div>
<div class="chat-editor flex-grow overflow-hidden">
<EditorContent {editor} />
</div>
<Button
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
disabled={$uploading}
onclick={submit}>
<Icon icon={Plane} />
</Button>
</form>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
const {
clear,
}: {
clear: () => void
} = $props()
</script>
<div
class="relative flex h-8 items-center justify-between border-l-2 border-solid border-primary bg-base-300 px-2 pr-7 text-xs"
transition:slide>
<p class="text-primary">Editing message</p>
<Button onclick={clear} class="flex items-center">
<Icon icon={CloseCircle} />
</Button>
</div>
@@ -5,7 +5,7 @@
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.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 NoteContent from "@app/components/NoteContent.svelte" import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
const { const {
verb, verb,
@@ -19,16 +19,11 @@
</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 border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide> transition:slide>
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p> <p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id} {#key event.id}
<NoteContent <NoteContentMinimal trimParent {event} />
{event}
hideMediaAtDepth={0}
minLength={100}
maxLength={300}
expandMode="disabled" />
{/key} {/key}
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}> <Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
<Icon icon={CloseCircle} /> <Icon icon={CloseCircle} />
+30 -90
View File
@@ -1,107 +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 {preventDefault} from "@lib/html"
import Field from "@lib/components/Field.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
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 Spinner from "@lib/components/Spinner.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 {hasNip29, loadChannel} from "@app/core/state" import RoomForm from "@app/components/RoomForm.svelte"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {pushToast} from "@app/util/toast"
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]])
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)
</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)}
<Field>
{#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" /> </div>
</label>
{/snippet} {/snippet}
</Field> </ModalHeader>
{:else} {/snippet}
<p class="bg-alt card2 row-2"> {#snippet footer({loading})}
<Icon icon={Danger} /> <ModalFooter>
This relay does not support creating rooms. <Button class="btn btn-link" onclick={back}>
</p> <Icon icon={AltArrowLeft} />
{/if} Go back
<ModalFooter> </Button>
<Button class="btn btn-link" onclick={back}> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Icon icon={AltArrowLeft} /> <Spinner {loading}>Create Room</Spinner>
Go back <Icon icon={AltArrowRight} />
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={!name || loading || !hasNip29($relay)}> </ModalFooter>
<Spinner {loading}>Create Room</Spinner> {/snippet}
<Icon icon={AltArrowRight} /> </RoomForm>
</Button>
</ModalFooter>
</form>
+82
View File
@@ -0,0 +1,82 @@
<script lang="ts">
import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util"
import {deleteRoom, waitForThunkError, repository} from "@welshman/app"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import RoomForm from "@app/components/RoomForm.svelte"
import {deriveRoom} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
type Props = {
url: string
h: string
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
const back = () => history.back()
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, h))
const startDelete = () =>
pushModal(Confirm, {
title: "Are you sure you want to delete this room?",
message:
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
confirm: async () => {
const thunk = deleteRoom(url, $room)
const message = await waitForThunkError(thunk)
if (message) {
repository.removeEvent(thunk.event.id)
pushToast({theme: "error", message})
} else {
goto(makeSpacePath(url))
}
},
})
</script>
<RoomForm {url} {onsubmit} initialValues={$room}>
{#snippet header()}
<ModalHeader>
{#snippet title()}
<div>Edit a Room</div>
{/snippet}
{#snippet info()}
<div>
On <span class="text-primary">{displayRelayUrl(url)}</span>
</div>
{/snippet}
</ModalHeader>
{/snippet}
{#snippet footer({loading})}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<div class="flex gap-2">
<Button class="btn btn-outline btn-error" onclick={startDelete}>
<Icon icon={TrashBin2} />
<span class="hidden md:inline">Delete Room</span>
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Save Changes</Spinner>
</Button>
</div>
</ModalFooter>
{/snippet}
</RoomForm>
+206
View File
@@ -0,0 +1,206 @@
<script lang="ts">
import type {Snippet} from "svelte"
import type {RoomMeta} from "@welshman/util"
import {makeRoomMeta} from "@welshman/util"
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
import {preventDefault, compressFile} from "@lib/html"
import FieldInline from "@lib/components/FieldInline.svelte"
import Icon from "@lib/components/Icon.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
import {pushToast} from "@app/util/toast"
import {uploadFile} from "@app/core/commands"
type Props = {
url: string
header: Snippet
footer: Snippet<[{loading: boolean}]>
onsubmit: (room: RoomMeta) => void
initialValues?: RoomMeta
}
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
const values = $state(initialValues)
const submit = async () => {
const room = $state.snapshot(values)
if (imageFile) {
const {error, result} = await uploadFile(imageFile)
if (error) {
return pushToast({theme: "error", message: error})
}
room.picture = result.url
room.pictureMeta = result.tags
} else if (selectedIcon) {
room.picture = selectedIcon
}
const createMessage = await waitForThunkError(createRoom(url, room))
if (createMessage && !createMessage.includes("already")) {
return pushToast({theme: "error", message: createMessage})
}
const editMessage = await waitForThunkError(editRoom(url, room))
if (editMessage) {
return pushToast({theme: "error", message: editMessage})
}
const joinMessage = await waitForThunkError(joinRoom(url, room))
if (joinMessage && !joinMessage.includes("already")) {
return pushToast({theme: "error", message: joinMessage})
}
onsubmit(room)
}
const trySubmit = async () => {
loading = true
try {
await submit()
} finally {
loading = false
}
}
let loading = $state(false)
let imageFile = $state<File | undefined>()
let imagePreview = $state(initialValues.picture)
let selectedIcon = $state<string | undefined>()
const handleImageUpload = async (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (file && file.type.startsWith("image/")) {
selectedIcon = undefined
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
const reader = new FileReader()
reader.onload = e => {
imagePreview = e.target?.result as string
}
reader.readAsDataURL(imageFile)
}
}
const handleIconSelect = (iconUrl: string) => {
imageFile = undefined
imagePreview = undefined
selectedIcon = iconUrl
}
</script>
<form class="column gap-4" onsubmit={preventDefault(trySubmit)}>
{@render header()}
<FieldInline>
{#snippet label()}
<p>Icon</p>
{/snippet}
{#snippet input()}
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<ImageIcon src={imagePreview} alt="Room icon preview" />
</div>
{:else if selectedIcon}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<Icon icon={selectedIcon} class="h-8 w-8" />
</div>
{:else}
<span class="text-sm opacity-75">No icon selected</span>
{/if}
<div class="flex gap-2">
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
<Icon icon={StickerSmileSquare} size={4} />
Select
</IconPickerButton>
<label class="btn btn-neutral btn-sm cursor-pointer">
<Icon icon={UploadMinimalistic} size={4} />
Upload
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
</label>
</div>
</div>
</div>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Name</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
{#if imagePreview}
<ImageIcon src={imagePreview} alt="Room icon preview" />
{:else if selectedIcon}
<Icon icon={selectedIcon} class="h-8 w-8" />
{:else}
<Icon icon={Hashtag} />
{/if}
<input bind:value={values.name} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Description</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<input bind:value={values.about} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Restricted</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
<span class="text-sm opacity-75">Only allow members to send messages</span>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Private</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
<span class="text-sm opacity-75">Only allow members to read messages</span>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Hidden</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isHidden} />
<span class="text-sm opacity-75">Hide this group from non-members</span>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Closed</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
<span class="text-sm opacity-75">Ignore requests to join</span>
{/snippet}
</FieldInline>
{@render footer({loading})}
</form>
@@ -1,23 +1,36 @@
<script lang="ts"> <script lang="ts">
import {hash, now, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib" import cx from "classnames"
import {hash, now, displayList, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {thunks, deriveProfile, deriveProfileDisplay} from "@welshman/app" import {MESSAGE, COMMENT} from "@welshman/util"
import {
thunks,
pubkey,
mergeThunks,
deriveProfile,
deriveProfileDisplay,
displayProfileByPubkey,
} from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import ReplyAlt from "@assets/icons/reply.svg?dataurl"
import TapTarget from "@lib/components/TapTarget.svelte" import TapTarget from "@lib/components/TapTarget.svelte"
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import Reply from "@assets/icons/reply-2.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 Content from "@app/components/Content.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 ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ChannelMessageZapButton from "@app/components/ChannelMessageZapButton.svelte" import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte"
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte" import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte"
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte" import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte" import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
import {colors, ENABLE_ZAPS} from "@app/core/state" import RoomItemContent from "@app/components/RoomItemContent.svelte"
import {colors, ENABLE_ZAPS, deriveEventsForUrl} from "@app/core/state"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands" import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
import {getRoomItemPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
interface Props { interface Props {
@@ -26,20 +39,33 @@
replyTo?: (event: TrustedEvent) => void replyTo?: (event: TrustedEvent) => void
showPubkey?: boolean showPubkey?: boolean
inert?: boolean inert?: boolean
canEdit: (event: TrustedEvent) => boolean
onEdit: (event: TrustedEvent) => void
} }
const {url, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props() const {
url,
event,
replyTo = undefined,
showPubkey = false,
inert = false,
canEdit,
onEdit,
}: Props = $props()
const thunk = $thunks[event.id] const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const today = formatTimestampAsDate(now()) const today = formatTimestampAsDate(now())
const profile = deriveProfile(event.pubkey, [url]) const profile = deriveProfile(event.pubkey, [url])
const profileDisplay = deriveProfileDisplay(event.pubkey, [url]) const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length] const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const comments = deriveEventsForUrl(url, [{kinds: [COMMENT], "#e": [event.id]}])
const reply = () => replyTo!(event) const reply = () => replyTo!(event)
const edit = canEdit(event) ? () => onEdit(event) : undefined
const onTap = () => pushModal(ChannelMessageMenuMobile, {url, event, reply}) const onTap = () => pushModal(RoomItemMenuMobile, {url, event, reply, edit})
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
@@ -53,7 +79,7 @@
<TapTarget <TapTarget
data-event={event.id} data-event={event.id}
onTap={inert ? null : onTap} onTap={inert ? null : onTap}
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left"> class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left hover:bg-base-100/50">
<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">
@@ -78,10 +104,10 @@
</span> </span>
</div> </div>
{/if} {/if}
<div class="text-sm"> <div class:mt-2={showPubkey && event.kind !== MESSAGE}>
<Content minimalQuote {event} {url} /> <RoomItemContent {url} {event} />
{#if thunk} {#if thunk}
<ThunkFailure showToastOnRetry {thunk} class="mt-2" /> <ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
{/if} {/if}
</div> </div>
</div> </div>
@@ -93,21 +119,43 @@
{deleteReaction} {deleteReaction}
{createReaction} {createReaction}
reactionClass="tooltip-right" /> reactionClass="tooltip-right" />
{#if path && $comments.length > 0}
{@const pubkeys = $comments.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const tooltip = `${info} commented`}
<div data-tip={tooltip} class="tooltip tooltip-right flex">
<Link
href={path}
class={cx("btn btn-xs gap-1 rounded-full", {
"btn-neutral": !isOwn,
"btn-primary": isOwn,
})}>
<Icon icon={ReplyAlt} />
<span>{$comments.length} comment{$comments.length === 1 ? "" : "s"}</span>
</Link>
</div>
{/if}
</div> </div>
{#if !isMobile} {#if !isMobile}
<button <button
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}
<ChannelMessageZapButton {url} {event} /> <RoomItemZapButton {url} {event} />
{/if} {/if}
<ChannelMessageEmojiButton {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} />
</Button> </Button>
{/if} {/if}
<ChannelMessageMenuButton {url} {event} /> {#if edit}
<Button class="btn join-item btn-xs" onclick={edit}>
<Icon icon={Pen} size={4} />
</Button>
{/if}
<RoomItemMenuButton {url} {event} />
</button> </button>
{/if} {/if}
</TapTarget> </TapTarget>
@@ -0,0 +1,18 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {getPubkeyTagValues} from "@welshman/util"
import ProfileLink from "@app/components/ProfileLink.svelte"
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
</script>
{#each getPubkeyTagValues(event.tags) as pubkey}
<div class="py-1 text-center text-xs opacity-75">
<ProfileLink unstyled class="text-primary" {url} {pubkey} /> joined the room
</div>
{/each}
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts">
import cx from "classnames"
import type {ComponentProps} from "svelte"
import {MESSAGE} from "@welshman/util"
import {isMobile} from "@lib/html"
import Link from "@lib/components/Link.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import {getRoomItemPath} from "@app/util/routes"
const props: ComponentProps<typeof NoteContent> = $props()
const path = getRoomItemPath(props.url!, props.event)
</script>
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
{#if path && !isMobile}
<Link href={path}>
<NoteContent {...props} />
</Link>
{:else}
<NoteContent {...props} />
{/if}
</div>
@@ -1,16 +1,23 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@welshman/app" import {pubkey} from "@welshman/app"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.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 EventInfo from "@app/components/EventInfo.svelte" import EventInfo from "@app/components/EventInfo.svelte"
import EventReport from "@app/components/EventReport.svelte" import EventReport from "@app/components/EventReport.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 Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl"
const {url, event, onClick} = $props() type Props = {
url: string
event: TrustedEvent
onClick: () => void
}
const {url, event, onClick}: Props = $props()
const report = () => { const report = () => {
onClick() onClick()
@@ -32,7 +39,7 @@
<li> <li>
<Button onclick={showInfo}> <Button onclick={showInfo}>
<Icon size={4} icon={Code2} /> <Icon size={4} icon={Code2} />
Message Details Show JSON
</Button> </Button>
</li> </li>
{#if event.pubkey === $pubkey} {#if event.pubkey === $pubkey}
@@ -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 ChannelMessageMenu from "@app/components/ChannelMessageMenu.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={ChannelMessageMenu} component={RoomItemMenu}
props={{url, event, onClick}} props={{url, event, onClick}}
params={{trigger: "manual", interactive: true}} /> params={{trigger: "manual", interactive: true}} />
</div> </div>
@@ -2,7 +2,14 @@
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@welshman/app" import {pubkey} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import EmojiPicker from "@lib/components/EmojiPicker.svelte" import EmojiPicker from "@lib/components/EmojiPicker.svelte"
import ZapButton from "@app/components/ZapButton.svelte" import ZapButton from "@app/components/ZapButton.svelte"
@@ -10,12 +17,8 @@
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 {getRoomItemPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
type Props = { type Props = {
url: string url: string
@@ -25,6 +28,8 @@
const {url, event, reply}: Props = $props() const {url, event, reply}: Props = $props()
const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const onEmoji = (async (event: TrustedEvent, url: string, emoji: NativeEmoji) => { const onEmoji = (async (event: TrustedEvent, url: string, emoji: NativeEmoji) => {
@@ -49,29 +54,35 @@
const showDelete = () => pushModal(EventDeleteConfirm, {url, event}) const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
</script> </script>
<div class="col-2"> <div class="flex flex-col gap-2">
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
<Icon size={4} icon={SmileCircle} />
Send Reaction
</Button>
{#if ENABLE_ZAPS}
<ZapButton replaceState {url} {event} class="btn btn-secondary w-full">
<Icon size={4} icon={Bolt} />
Send Zap
</ZapButton>
{/if}
<Button class="btn btn-neutral w-full" onclick={sendReply}>
<Icon size={4} icon={Reply} />
Send Reply
</Button>
<Button class="btn btn-neutral" onclick={showInfo}>
<Icon size={4} icon={Code2} />
Message Details
</Button>
{#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 Message Delete
</Button> </Button>
{/if} {/if}
<Button class="btn btn-neutral" onclick={showInfo}>
<Icon size={4} icon={Code2} />
Show JSON
</Button>
{#if path}
<Link class="btn btn-neutral" href={path}>
<Icon size={4} icon={MenuDots} />
View Details
</Link>
{/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}
<ZapButton replaceState {url} {event} class="btn btn-primary w-full">
<Icon size={4} icon={Bolt} />
Zap
</ZapButton>
{/if}
</div> </div>
@@ -0,0 +1,18 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {getPubkeyTagValues} from "@welshman/util"
import ProfileLink from "@app/components/ProfileLink.svelte"
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
</script>
{#each getPubkeyTagValues(event.tags) as pubkey}
<div class="py-1 text-center text-xs opacity-75">
<ProfileLink unstyled class="text-primary" {url} {pubkey} /> left the room
</div>
{/each}
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import cx from "classnames"
import Link from "@lib/components/Link.svelte"
import RoomName from "@app/components/RoomName.svelte"
import {makeSpacePath} from "@app/util/routes"
type Props = {
h: string
url: string
class?: string
unstyled?: boolean
}
const {h, url, unstyled, ...props}: Props = $props()
const path = makeSpacePath(url, h)
</script>
<Link href={path} class={cx(props.class, {"link-content bg-alt": !unstyled})}>
#<RoomName {h} {url} />
</Link>
+7
View File
@@ -0,0 +1,7 @@
<script lang="ts">
import {roomsById, makeRoomId} from "@app/core/state"
const {url, h} = $props()
</script>
{$roomsById.get(makeRoomId(url, h))?.name || h}
@@ -0,0 +1,26 @@
<script lang="ts">
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import RoomName from "@app/components/RoomName.svelte"
import {deriveRoom} from "@app/core/state"
interface Props {
url: any
h: any
}
const {url, h}: Props = $props()
const room = deriveRoom(url, h)
</script>
{#if $room.picture}
{@const src = $room.picture}
<ImageIcon {src} alt="Room icon" />
{:else}
<Icon icon={Hashtag} />
{/if}
<div class="min-w-0 overflow-hidden text-ellipsis">
<RoomName {url} {h} />
</div>
+41 -23
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {spec, prop, avg} from "@welshman/lib" import {spec, prop, avg} from "@welshman/lib"
import {signerLog, SignerLogEntryStatus} from "@welshman/app" import {session, SessionMethod, signerLog, SignerLogEntryStatus} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl" import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
@@ -26,27 +26,45 @@
const logout = () => pushModal(LogOut) const logout = () => pushModal(LogOut)
</script> </script>
<div class="card2 bg-alt flex flex-col gap-4"> {#if $session && $session.method !== SessionMethod.Anonymous}
<div class="flex flex-col gap-2"> <div class="card2 bg-alt flex flex-col gap-4">
<div class="flex items-center justify-between"> <div class="flex flex-col gap-2">
<span class="text-xl font-bold">Signer Status</span> <div class="flex items-center justify-between">
<span class="flex items-center gap-2"> <span class="text-xl font-bold">Signer Status</span>
{#if isDisconnected} <span class="flex items-center gap-2">
<Icon icon={CloseCircle} class="text-error" size={4} /> Disconnected {#if isDisconnected}
{:else if recentFailure > 3} <Icon icon={CloseCircle} class="text-error" size={4} /> Disconnected
<Icon icon={Danger} class="text-warning" size={4} /> Partial Failure {:else if recentFailure > 3}
{:else if recentAvg > 1000 || recentPending > 3} <Icon icon={Danger} class="text-warning" size={4} /> Partial Failure
<Icon icon={ClockCircle} class="text-warning" size={4} /> Slow connection {:else if recentAvg > 1000 || recentPending > 3}
{:else if recentSuccess === 0 && recentFailure > 0}{:else} <Icon icon={ClockCircle} class="text-warning" size={4} /> Slow connection
<Icon icon={CheckCircle} class="text-success" size={4} /> Ok {:else if recentSuccess === 0 && recentFailure > 0}{:else}
{/if} <Icon icon={CheckCircle} class="text-success" size={4} /> Ok
</span> {/if}
</span>
</div>
<div class="flex justify-between text-sm opacity-75">
<p>
Logged in with
{#if $session.method === SessionMethod.Nip01}
private key
{:else if $session.method === SessionMethod.Nip07}
browser extension
{:else if $session.method === SessionMethod.Nip46}
remote signer
{:else if $session.method === SessionMethod.Nip55}
{$session.signer}
{:else if $session.method === SessionMethod.Pubkey}
public key (readonly)
{/if}
</p>
<p>
{success} requests succeeded, {failure} failed, {pending} pending
</p>
</div>
</div> </div>
<p class="text-sm opacity-75"> {#if isDisconnected}
{success} requests succeeded, {failure} failed, {pending} pending <Button class="btn btn-outline btn-error" onclick={logout}>Logout to Reconnect</Button>
</p> {/if}
</div> </div>
{#if isDisconnected} {/if}
<Button class="btn btn-outline btn-error" onclick={logout}>Logout to Reconnect</Button>
{/if}
</div>
@@ -0,0 +1,13 @@
<script lang="ts">
import StatusIndicator from "@lib/components/StatusIndicator.svelte"
import {deriveSocketStatus} from "@app/core/state"
type Props = {
url: string
}
const {url}: Props = $props()
const status = deriveSocketStatus(url)
</script>
<StatusIndicator class="bg-{$status.theme}">{$status.title}</StatusIndicator>

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