forked from coracle/flotilla
Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57447e5bf4 | |||
| 8e411daaef | |||
| 183aebf841 | |||
| e3e500ccc2 | |||
| e7a2535ece | |||
| 761e369313 | |||
| 5248275d73 | |||
| cb033279dd | |||
| 41d50d8c28 | |||
| a52c2b4c3c | |||
| b5917cb184 | |||
| 57348472f8 | |||
| 4b6223dc00 | |||
| 5525e45a15 | |||
| 80a2ae60b0 | |||
| d7e95f5d2f | |||
| ca4e5ae5ee | |||
| b673658c0c | |||
| 5c5c130700 | |||
| 2d89ca6c0e | |||
| 806a7c2609 | |||
| 501ce8067d | |||
| 6429f82829 | |||
| fe626218ea | |||
| b62b1bc063 | |||
| d980f36246 | |||
| b469addd29 | |||
| 6923c2a8b7 | |||
| 1d3f32fb99 | |||
| 42a550788a | |||
| b1c68972c9 | |||
| 3978e32d5f | |||
| ba2b5d182e | |||
| bef04fa899 | |||
| 4f8609421c | |||
| 07660c9d44 | |||
| a324dad2ba | |||
| dbaa0f5d49 | |||
| 478721d349 | |||
| a669a23dbc | |||
| cfeb6478cc | |||
| 64539c49c1 | |||
| 0399ae37ec | |||
| 173a411a36 | |||
| 62013a2ea2 | |||
| c82cf4a4c2 | |||
| df42085be6 | |||
| b09d3065ae | |||
| c050f5a9e3 | |||
| 78e6c0eca0 | |||
| da4da45348 | |||
| dc2af86db8 | |||
| 7502004aba | |||
| 2e8678e4c6 | |||
| 97569016fc | |||
| fe72798592 | |||
| 4583c4e028 | |||
| 0b98197a86 | |||
| 0e94a9c33f | |||
| 3dff1fcb4d | |||
| e163286dd4 | |||
| a99e12f12e | |||
| c3dd997e57 | |||
| a730384baf | |||
| 43cf91e877 | |||
| 75bee027e1 | |||
| 5cbf69a8bd | |||
| ecbb3086d8 | |||
| 7476767aa7 | |||
| e5b8987a9d | |||
| 6ca74c21bf | |||
| e0099141aa | |||
| d0491ed202 | |||
| cbc2137ced | |||
| f9ac13ba11 | |||
| b3533c285f | |||
| a636ae6592 | |||
| 69e3ee0aff | |||
| a39a87ba6d | |||
| 5b22d6ac01 | |||
| 7334cd26f8 | |||
| 44555215cf | |||
| 0cc25913c0 | |||
| 004b30b737 | |||
| 632f330b4c | |||
| 666433912f | |||
| db98ce8db7 | |||
| 71dcfae5ff | |||
| 04155f5b23 | |||
| b4058389ec | |||
| 483fa81b74 | |||
| a8d1c4bbbc | |||
| 0a8c2faa74 | |||
| dd3231e70f | |||
| 7ff9c00032 | |||
| 9ed483abf7 | |||
| b9aeaf29a4 | |||
| 65e3f81f36 | |||
| c6641dba31 | |||
| e48d1e0e59 | |||
| d1e5aee84e | |||
| 5cb22d0bed | |||
| d1c6f53d7c | |||
| 6e238f98c0 | |||
| 290274d6c8 | |||
| e1de0239c9 | |||
| bec77d59e8 | |||
| 84f8794d7c | |||
| 4cddf41bf3 | |||
| 125a7e238e | |||
| 468200b717 | |||
| bdfcb99781 | |||
| 38da650861 | |||
| dd006badfc | |||
| 87e4e3fe5b | |||
| af3e38254f | |||
| 70843f54d3 | |||
| bda75b29b4 | |||
| 750830d593 | |||
| 3c0f1a1d2f | |||
| 4253b0ed29 | |||
| 3c9b3f23df | |||
| e0d83608be | |||
| a0301d599b | |||
| 7dcaa0e8d7 | |||
| 129f49bcc7 | |||
| fc3b68c390 | |||
| 52c7df8a15 | |||
| ce1c4dd488 | |||
| fc6a1a3819 | |||
| 69bd6d0e70 | |||
| 6d383d54e8 | |||
| 998c48b1d3 | |||
| 7217d122b5 | |||
| 1c37c5bb3d | |||
| e8f785b558 | |||
| c94d314f6d | |||
| 2672a8f922 | |||
| 8a8d80d692 | |||
| 95698813c6 | |||
| 4001e877b4 | |||
| 99defc6d79 | |||
| a94883089e |
+4
-3
@@ -1,16 +1,17 @@
|
||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||
VITE_BURROW_URL=
|
||||
VITE_PLATFORM_URL=https://flotilla.social
|
||||
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||
VITE_PLATFORM_NAME=Flotilla
|
||||
VITE_PLATFORM_LOGO=static/flotilla.png
|
||||
VITE_PLATFORM_LOGO=static/logo.png
|
||||
VITE_PLATFORM_RELAYS=
|
||||
VITE_PLATFORM_ACCENT="#7161FF"
|
||||
VITE_PLATFORM_SECONDARY="#EB5E28"
|
||||
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_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_RELAY=wss://anchor.coracle.social/
|
||||
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||
|
||||
@@ -1,5 +1,95 @@
|
||||
# Changelog
|
||||
|
||||
# 1.5.3
|
||||
|
||||
* Add space edit form
|
||||
* Improve room syncing
|
||||
* Return better blossom errors
|
||||
* Fix access restricted bugs
|
||||
* Add room detail dialog
|
||||
* Fix broken link to self hosting
|
||||
* Tweak shadows
|
||||
* Always join spaces when visiting them
|
||||
|
||||
# 1.5.2
|
||||
|
||||
* Fix negentropy room syncing
|
||||
|
||||
# 1.5.1
|
||||
|
||||
* 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
|
||||
|
||||
* Add direct message alerts
|
||||
* Add alert settings page
|
||||
* Add instructions to key download
|
||||
* Add option that allows relays to strip signatures
|
||||
* Detect relays that mostly refuse to serve requests
|
||||
* Compress and upload profile images
|
||||
* Use system theme by default
|
||||
* Switch icon set, refactor how they're included
|
||||
* Use capacitor's preferences for storage instead of localStorage
|
||||
|
||||
# 1.2.3
|
||||
|
||||
* Add `created_at` to event info dialog
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ Here are a few important domain objects:
|
||||
|
||||
- Spaces are relays used as community groups. Their `url`s are core to a lot of data and components, and are frequently passed around from place to place.
|
||||
- Chats are direct message conversations. There is currently some ambiguity in routing, since relays that don't support NIP 29 also have a "chat" tab, which uses vanilla NIP-C7.
|
||||
- NIP 29 groups are variously called "rooms" and "channels". Conventionally, a "room" is a group id, while a "channel" as an object representing the group's metadata.
|
||||
- NIP 29 groups are called "rooms". Conventionally, "h" is a group id, while a "room" as an object representing the group's metadata.
|
||||
- "Alerts" are records of requests the user has made to be notified, following [this NIP](https://github.com/nostr-protocol/nips/pull/1796)
|
||||
|
||||
`app/core/requests` contains utilities related to loading data from the nostr network. This might include feed manager utilities, loaders, or listeners.
|
||||
|
||||
@@ -8,8 +8,8 @@ If you would like to be interoperable with Flotilla, please check out this guide
|
||||
|
||||
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
|
||||
|
||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust.
|
||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted. This is only used for build-time population of meta tags.
|
||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
|
||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted
|
||||
- `VITE_PLATFORM_NAME` - The name of the app
|
||||
- `VITE_PLATFORM_LOGO` - A logo url for the app
|
||||
- `VITE_PLATFORM_RELAYS` - A list of comma-separated relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "social.flotilla"
|
||||
minSdk rootProject.ext.minSdkVersion
|
||||
targetSdk rootProject.ext.targetSdkVersion
|
||||
versionCode 24
|
||||
versionName "1.2.3"
|
||||
versionCode 34
|
||||
versionName "1.5.3"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -11,8 +11,11 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-community-safe-area')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-keyboard')
|
||||
implementation project(':capacitor-preferences')
|
||||
implementation project(':capacitor-push-notifications')
|
||||
implementation project(':capawesome-capacitor-android-dark-mode-support')
|
||||
implementation project(':capawesome-capacitor-badge')
|
||||
implementation project(':nostr-signer-capacitor-plugin')
|
||||
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
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')
|
||||
|
||||
@@ -6,10 +6,6 @@ if [ -f .env.template ]; then
|
||||
source .env.template
|
||||
fi
|
||||
|
||||
if [ -f .env ]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Avoid overwriting env vars provided directly
|
||||
# https://stackoverflow.com/a/69127685/1467342
|
||||
eval "$temp_env"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
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 */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -58,6 +60,7 @@
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */,
|
||||
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
@@ -162,6 +165,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
@@ -354,14 +358,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -380,14 +384,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
+11
-9
@@ -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'
|
||||
use_frameworks!
|
||||
@@ -9,14 +9,16 @@ use_frameworks!
|
||||
install! 'cocoapods', :disable_input_output_paths => true
|
||||
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :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.2.0_@capacitor+core@7.2.0/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 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/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 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications'
|
||||
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge'
|
||||
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 '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.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.4.3/node_modules/@capacitor-community/safe-area'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app'
|
||||
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard'
|
||||
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences'
|
||||
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications'
|
||||
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
|
||||
|
||||
target 'Flotilla Chat' do
|
||||
|
||||
@@ -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>
|
||||
+55
-52
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "1.2.3",
|
||||
"version": "1.5.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -10,74 +10,77 @@
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@sentry/cli": "^2.40.0",
|
||||
"@sveltejs/kit": "^2.5.27",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"@eslint/js": "^9.37.0",
|
||||
"@sentry/cli": "^2.56.1",
|
||||
"@sveltejs/kit": "^2.46.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"classnames": "^2.5.1",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
"globals": "^15.0.0",
|
||||
"postcss": "^8.4.40",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.5.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^5.4.4"
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-prettier": "^9.1.2",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.15.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.39.12",
|
||||
"svelte-check": "^4.3.3",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.1",
|
||||
"vite": "^5.4.20"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@capacitor-community/safe-area": "7.0.0-alpha.1",
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/app": "^7.0.0",
|
||||
"@capacitor/cli": "^7.0.0",
|
||||
"@capacitor/core": "^7.0.1",
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/push-notifications": "^7.0.1",
|
||||
"@capacitor/android": "^7.4.3",
|
||||
"@capacitor/app": "^7.1.0",
|
||||
"@capacitor/cli": "^7.4.3",
|
||||
"@capacitor/core": "^7.4.3",
|
||||
"@capacitor/filesystem": "^7.1.4",
|
||||
"@capacitor/ios": "^7.4.3",
|
||||
"@capacitor/keyboard": "^7.0.3",
|
||||
"@capacitor/preferences": "^7.0.2",
|
||||
"@capacitor/push-notifications": "^7.0.3",
|
||||
"@capawesome/capacitor-android-dark-mode-support": "^7.0.0",
|
||||
"@capawesome/capacitor-badge": "^7.0.1",
|
||||
"@getalby/sdk": "^5.1.0",
|
||||
"@getalby/sdk": "^5.1.2",
|
||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||
"@sentry/browser": "^8.35.0",
|
||||
"@sveltejs/adapter-static": "^3.0.4",
|
||||
"@tiptap/core": "^2.12.0",
|
||||
"@sentry/browser": "^8.55.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@tiptap/core": "^2.26.3",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.6",
|
||||
"@welshman/app": "^0.4.3",
|
||||
"@welshman/content": "^0.4.3",
|
||||
"@welshman/editor": "^0.4.3",
|
||||
"@welshman/feeds": "^0.4.3",
|
||||
"@welshman/lib": "^0.4.3",
|
||||
"@welshman/net": "^0.4.3",
|
||||
"@welshman/relay": "^0.4.3",
|
||||
"@welshman/router": "^0.4.3",
|
||||
"@welshman/signer": "^0.4.3",
|
||||
"@welshman/store": "^0.4.3",
|
||||
"@welshman/util": "^0.4.3",
|
||||
"@vite-pwa/sveltekit": "^0.6.8",
|
||||
"@welshman/app": "^0.6.8",
|
||||
"@welshman/content": "^0.6.8",
|
||||
"@welshman/editor": "^0.6.8",
|
||||
"@welshman/feeds": "^0.6.8",
|
||||
"@welshman/lib": "^0.6.8",
|
||||
"@welshman/net": "^0.6.8",
|
||||
"@welshman/router": "^0.6.8",
|
||||
"@welshman/signer": "^0.6.8",
|
||||
"@welshman/store": "^0.6.8",
|
||||
"@welshman/util": "^0.6.8",
|
||||
"compressorjs": "^1.2.1",
|
||||
"daisyui": "^4.12.10",
|
||||
"date-picker-svelte": "^2.13.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"emoji-picker-element": "^1.22.8",
|
||||
"fuse.js": "^7.0.0",
|
||||
"husky": "^9.1.6",
|
||||
"idb": "^8.0.0",
|
||||
"daisyui": "^4.12.24",
|
||||
"date-picker-svelte": "^2.16.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"emoji-picker-element": "^1.27.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"idb": "^8.0.3",
|
||||
"nostr-signer-capacitor-plugin": "^0.0.4",
|
||||
"nostr-tools": "^2.14.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
|
||||
Generated
+2022
-1841
File diff suppressed because it is too large
Load Diff
+5
-9
@@ -62,6 +62,8 @@
|
||||
--primary-content: oklch(var(--pc));
|
||||
--secondary: oklch(var(--s));
|
||||
--secondary-content: oklch(var(--sc));
|
||||
--neutral: oklch(var(--n));
|
||||
--neutral-content: oklch(var(--nc));
|
||||
}
|
||||
|
||||
/* safe area insets */
|
||||
@@ -215,12 +217,6 @@
|
||||
@apply ellipsize;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
[data-tip]::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content-padding-x {
|
||||
@apply px-4 sm:px-8 md:px-12;
|
||||
}
|
||||
@@ -278,8 +274,8 @@
|
||||
}
|
||||
|
||||
.tiptap {
|
||||
--tiptap-object-bg: var(--base-100);
|
||||
--tiptap-object-fg: var(--base-content);
|
||||
--tiptap-object-bg: var(--neutral);
|
||||
--tiptap-object-fg: var(--neutral-content);
|
||||
--tiptap-active-bg: var(--primary);
|
||||
--tiptap-active-fg: var(--primary-content);
|
||||
}
|
||||
@@ -399,7 +395,7 @@ progress[value]::-webkit-progress-value {
|
||||
/* chat view */
|
||||
|
||||
.chat__compose {
|
||||
@apply cb cw fixed;
|
||||
@apply cb cw fixed z-compose;
|
||||
}
|
||||
|
||||
.chat__scroll-down {
|
||||
|
||||
@@ -1,37 +1,22 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import {randomInt, parseJson, fromPairs, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||
import {
|
||||
displayRelayUrl,
|
||||
getTagValue,
|
||||
getAddress,
|
||||
THREAD,
|
||||
MESSAGE,
|
||||
EVENT_TIME,
|
||||
COMMENT,
|
||||
} from "@welshman/util"
|
||||
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
||||
import {pubkey, signer, getThunkError} from "@welshman/app"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {
|
||||
alerts,
|
||||
getMembershipUrls,
|
||||
userMembership,
|
||||
NOTIFIER_PUBKEY,
|
||||
NOTIFIER_RELAY,
|
||||
} from "@app/core/state"
|
||||
import {loadAlertStatuses, requestRelayClaim} from "@app/core/requests"
|
||||
import {publishAlert, attemptAuth} from "@app/core/commands"
|
||||
import type {AlertParams} from "@app/core/commands"
|
||||
import {platform, platformName, canSendPushNotifications, getPushInfo} from "@app/util/push"
|
||||
import {alerts, userSpaceUrls} from "@app/core/state"
|
||||
import {requestRelayClaim} from "@app/core/requests"
|
||||
import {createAlert} from "@app/core/commands"
|
||||
import {canSendPushNotifications} from "@app/util/push"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
@@ -110,66 +95,20 @@
|
||||
|
||||
try {
|
||||
const claim = url ? await requestRelayClaim(url) : undefined
|
||||
const claims = claim ? {[url]: claim} : {}
|
||||
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url))
|
||||
const description = `for ${displayList(display)} on ${displayRelayUrl(url)}`
|
||||
const params: AlertParams = {feed, claims, description}
|
||||
|
||||
if (channel === "email") {
|
||||
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
|
||||
|
||||
params.description = `${cadence} alert ${description}, sent via email.`
|
||||
params.email = {
|
||||
cron,
|
||||
email,
|
||||
handler: [
|
||||
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
|
||||
"wss://relay.nostr.band/",
|
||||
"web",
|
||||
],
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// @ts-ignore
|
||||
params[platform] = await getPushInfo()
|
||||
params.description = `${platformName} push notification ${description}.`
|
||||
} catch (e: any) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: String(e),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't do this we'll get an event rejection
|
||||
await attemptAuth(NOTIFIER_RELAY)
|
||||
|
||||
const thunk = await publishAlert(params)
|
||||
const error = await getThunkError(thunk)
|
||||
const {error} = await createAlert({
|
||||
feed: makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url)),
|
||||
claims: claim ? {[url]: claim} : {},
|
||||
description: `for ${displayList(display)} on ${displayRelayUrl(url)}`,
|
||||
email: channel === "email" ? {cron, email} : undefined,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: `Failed to send your alert to the notification server (${error}).`,
|
||||
})
|
||||
pushToast({theme: "error", message: error})
|
||||
} else {
|
||||
pushToast({message: "Your alert has been successfully created!"})
|
||||
back()
|
||||
}
|
||||
|
||||
// Fetch our new status to make sure it's active
|
||||
const address = getAddress(thunk.event)
|
||||
const statusEvents = await loadAlertStatuses($pubkey!)
|
||||
const statusEvent = statusEvents.find(event => getTagValue("d", event.tags) === address)
|
||||
const statusTags = statusEvent
|
||||
? parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, statusEvent.content))
|
||||
: []
|
||||
const {status = "error", message = "Your alert was not activated"}: Record<string, string> =
|
||||
fromPairs(statusTags)
|
||||
|
||||
if (status === "error") {
|
||||
return pushToast({theme: "error", message})
|
||||
}
|
||||
|
||||
pushToast({message: "Your alert has been successfully created!"})
|
||||
back()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
@@ -187,6 +126,9 @@
|
||||
{#snippet title()}
|
||||
Add an Alert
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
Enable notifications to keep up to date on activity you care about.
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{#if canSendPushNotifications()}
|
||||
<FieldInline>
|
||||
@@ -232,7 +174,7 @@
|
||||
{#snippet input()}
|
||||
<select bind:value={url} class="select select-bordered">
|
||||
<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>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -262,12 +204,12 @@
|
||||
</FieldInline>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Confirm</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import type {Alert} from "@app/core/state"
|
||||
import {NOTIFIER_RELAY, NOTIFIER_PUBKEY} from "@app/core/state"
|
||||
import {publishDelete} from "@app/core/commands"
|
||||
import {deleteAlert} from "@app/core/commands"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
@@ -12,10 +11,7 @@
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const confirm = () => {
|
||||
const relays = [NOTIFIER_RELAY]
|
||||
const tags = [["p", NOTIFIER_PUBKEY]]
|
||||
|
||||
publishDelete({event: alert.event, relays, tags, protect: false})
|
||||
deleteAlert(alert)
|
||||
pushToast({message: "Your alert has been deleted!"})
|
||||
history.back()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {parseJson} from "@welshman/lib"
|
||||
import {displayFeeds} from "@welshman/feeds"
|
||||
import {getAddress, getTagValue, getTagValues} from "@welshman/util"
|
||||
import {getTagValue, getTagValues} from "@welshman/util"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AlertDelete from "@app/components/AlertDelete.svelte"
|
||||
import AlertStatus from "@app/components/AlertStatus.svelte"
|
||||
import type {Alert} from "@app/core/state"
|
||||
import {deriveAlertStatus} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
@@ -15,7 +16,6 @@
|
||||
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const status = deriveAlertStatus(getAddress(alert.event))
|
||||
const cron = $derived(getTagValue("cron", alert.tags))
|
||||
const channel = $derived(getTagValue("channel", alert.tags))
|
||||
const feeds = $derived(getTagValues("feed", alert.tags))
|
||||
@@ -34,36 +34,9 @@
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<Button class="py-1" onclick={startDelete}>
|
||||
<Icon icon="trash-bin-2" />
|
||||
<Icon icon={TrashBin2} />
|
||||
</Button>
|
||||
<div class="flex-inline gap-1">{description}</div>
|
||||
</div>
|
||||
{#if $status}
|
||||
{@const statusText = getTagValue("status", $status.tags) || "error"}
|
||||
{#if statusText === "ok"}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content px-3 py-1 text-sm"
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
Active
|
||||
</span>
|
||||
{:else if statusText === "pending"}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content border-yellow-500 px-3 py-1 text-sm text-yellow-500"
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
Pending
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
||||
</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||
data-tip="The notification server did not respond to your request.">
|
||||
Inactive
|
||||
</span>
|
||||
{/if}
|
||||
<AlertStatus {alert} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import {getAddress, getTagValue} from "@welshman/util"
|
||||
import type {Alert} from "@app/core/state"
|
||||
import {deriveAlertStatus} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
alert: Alert
|
||||
}
|
||||
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const status = deriveAlertStatus(getAddress(alert.event))
|
||||
</script>
|
||||
|
||||
{#if $status}
|
||||
{@const statusText = getTagValue("status", $status.tags) || "error"}
|
||||
{#if statusText === "ok"}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content px-3 py-1 text-sm"
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
Active
|
||||
</span>
|
||||
{:else if statusText === "pending"}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content border-yellow-500 px-3 py-1 text-sm text-yellow-500"
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
Pending
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
||||
</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||
data-tip="The notification server did not respond to your request.">
|
||||
Inactive
|
||||
</span>
|
||||
{/if}
|
||||
@@ -1,11 +1,26 @@
|
||||
<script lang="ts">
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import {sleep} from "@welshman/lib"
|
||||
import {getTagValue, getAddress} from "@welshman/util"
|
||||
import {isRelayFeed, findFeed} from "@welshman/feeds"
|
||||
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 Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||
import AlertItem from "@app/components/AlertItem.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {alerts} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {
|
||||
alerts,
|
||||
dmAlert,
|
||||
deriveAlertStatus,
|
||||
userInboxRelays,
|
||||
getAlertFeed,
|
||||
userSettingsValues,
|
||||
} from "@app/core/state"
|
||||
import {deleteAlert, createDmAlert} from "@app/core/commands"
|
||||
import {clearBadges} from "../util/notifications"
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
@@ -15,29 +30,126 @@
|
||||
|
||||
const {url = "", channel = "push", hideSpaceField = false}: Props = $props()
|
||||
|
||||
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
||||
const dmStatus = $derived($dmAlert ? deriveAlertStatus(getAddress($dmAlert.event)) : undefined)
|
||||
|
||||
const filteredAlerts = $derived(
|
||||
url ? $alerts.filter(a => getTagValue("feed", a.tags)?.includes(url)) : $alerts,
|
||||
$alerts.filter(alert => {
|
||||
const feed = getAlertFeed(alert)
|
||||
|
||||
// Skip non-feeds and DM alerts
|
||||
if (!feed || alert === $dmAlert) return false
|
||||
|
||||
// If we have a space url, only match feeds for this space
|
||||
if (url) return findFeed(feed, f => isRelayFeed(f) && f.includes(url))
|
||||
|
||||
return true
|
||||
}),
|
||||
)
|
||||
|
||||
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
||||
|
||||
const uncheckDmAlert = async (message: string) => {
|
||||
await sleep(100)
|
||||
|
||||
directMessagesNotificationToggle.checked = false
|
||||
pushToast({theme: "error", message})
|
||||
}
|
||||
|
||||
const onDirectMessagesNotificationToggle = async () => {
|
||||
if ($dmAlert) {
|
||||
deleteAlert($dmAlert)
|
||||
} else {
|
||||
if ($userInboxRelays.length === 0) {
|
||||
return uncheckDmAlert("Please set up your messaging relays before enabling alerts.")
|
||||
}
|
||||
|
||||
const {error} = await createDmAlert()
|
||||
|
||||
if (error) {
|
||||
return uncheckDmAlert(error)
|
||||
}
|
||||
|
||||
pushToast({message: "Your alert has been successfully created!"})
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon="inbox" />
|
||||
Alerts
|
||||
</strong>
|
||||
<Button class="btn btn-primary btn-sm" onclick={startAlert}>
|
||||
<Icon icon="add-circle" />
|
||||
Add Alert
|
||||
</Button>
|
||||
<div class="col-4">
|
||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon={Inbox} />
|
||||
Alerts
|
||||
</strong>
|
||||
<Button class="btn btn-primary btn-sm" onclick={startAlert}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Alert
|
||||
</Button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{#each filteredAlerts as alert (alert.event.id)}
|
||||
<AlertItem {alert} />
|
||||
{:else}
|
||||
<p class="text-center opacity-75 py-12">Nothing here yet!</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{#each filteredAlerts as alert (alert.event.id)}
|
||||
<AlertItem {alert} />
|
||||
{:else}
|
||||
<p class="text-center opacity-75 py-12">Nothing here yet!</p>
|
||||
{/each}
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||
<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">
|
||||
<p>Notify me about new direct messages</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:this={directMessagesNotificationToggle}
|
||||
checked={Boolean($dmAlert)}
|
||||
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>
|
||||
{#if $dmStatus}
|
||||
{@const status = getTagValue("status", $dmStatus.tags) || "error"}
|
||||
{#if status !== "ok"}
|
||||
<div class="alert alert-error border border-solid border-error bg-transparent text-error">
|
||||
<p>
|
||||
{getTagValue("message", $dmStatus.tags) ||
|
||||
"The notification server did not respond to your request."}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import Scanner from "@lib/components/Scanner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import CpuBolt from "@assets/icons/cpu-bolt.svg?dataurl"
|
||||
import QrCode from "@assets/icons/qr-code.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import InfoBunker from "@app/components/InfoBunker.svelte"
|
||||
import type {Nip46Controller} from "@app/util/nip46"
|
||||
@@ -33,10 +35,10 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="cpu" />
|
||||
<Icon icon={CpuBolt} />
|
||||
<input disabled={$loading} bind:value={$bunker} class="grow" placeholder="bunker://" />
|
||||
<Button onclick={toggleScanner}>
|
||||
<Icon icon="qr-code" />
|
||||
<Icon icon={QrCode} />
|
||||
</Button>
|
||||
</label>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.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 ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||
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 Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||
|
||||
const {
|
||||
url,
|
||||
event,
|
||||
showActivity = false,
|
||||
}: {
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: 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 shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
||||
|
||||
@@ -35,24 +38,27 @@
|
||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Event">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $pubkey}
|
||||
<li>
|
||||
<Button onclick={editEvent}>
|
||||
<Icon size={4} icon="pen" />
|
||||
Edit Event
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
</div>
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Event">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $pubkey}
|
||||
<li>
|
||||
<Button onclick={editEvent}>
|
||||
<Icon size={4} icon={Pen2} />
|
||||
Edit Event
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
const {url, h}: Props = $props()
|
||||
</script>
|
||||
|
||||
<CalendarEventForm {url}>
|
||||
<CalendarEventForm {url} {h}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
|
||||
@@ -8,13 +8,16 @@
|
||||
|
||||
const {event}: Props = $props()
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
const startDate = $derived(secondsToDate(parseInt(meta.start)))
|
||||
const start = $derived(parseInt(meta.start))
|
||||
</script>
|
||||
|
||||
<div
|
||||
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">
|
||||
<strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong>
|
||||
<span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||
<span class="text-xs opacity-75"
|
||||
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span>
|
||||
</div>
|
||||
{#if !isNaN(start)}
|
||||
{@const startDate = secondsToDate(start)}
|
||||
<div
|
||||
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">
|
||||
<strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong>
|
||||
<span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||
<span class="text-xs opacity-75"
|
||||
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {daysBetween} from "@lib/util"
|
||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||
import MapPoint from "@assets/icons/map-point.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -20,6 +23,7 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
h?: string
|
||||
header: Snippet
|
||||
initialValues?: {
|
||||
d: string
|
||||
@@ -31,7 +35,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const {url, header, initialValues}: Props = $props()
|
||||
const {url, h, header, initialValues}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -81,6 +85,10 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||
|
||||
pushToast({message: "Your event has been saved!"})
|
||||
@@ -131,7 +139,7 @@
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="gallery-send" />
|
||||
<Icon icon={GallerySend} />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -159,14 +167,14 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="map-point" />
|
||||
<Icon icon={MapPoint} />
|
||||
<input bind:value={location} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={$uploading}>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
formatTimestampAsTime,
|
||||
} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
type Props = {
|
||||
@@ -16,18 +17,20 @@
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
const start = $derived(parseInt(meta.start))
|
||||
const end = $derived(parseInt(meta.end))
|
||||
const startDateDisplay = $derived(formatTimestampAsDate(start))
|
||||
const endDateDisplay = $derived(formatTimestampAsDate(end))
|
||||
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-grow flex-wrap justify-between gap-2">
|
||||
<p class="text-xl">{meta.title || meta.name}</p>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon="clock-circle" size={4} />
|
||||
<span class="sm:hidden">{formatTimestampAsDate(start)}</span>
|
||||
{formatTimestampAsTime(start)} — {isSingleDay
|
||||
? formatTimestampAsTime(end)
|
||||
: formatTimestamp(end)}
|
||||
</div>
|
||||
{#if !isNaN(start) && !isNaN(end)}
|
||||
{@const startDateDisplay = formatTimestampAsDate(start)}
|
||||
{@const endDateDisplay = formatTimestampAsDate(end)}
|
||||
{@const isSingleDay = startDateDisplay === endDateDisplay}
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon={ClockCircle} size={4} />
|
||||
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
|
||||
{formatTimestampAsTime(start)} — {isSingleDay
|
||||
? formatTimestampAsTime(end)
|
||||
: formatTimestamp(end)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import {makeCalendarPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -12,13 +14,20 @@
|
||||
}
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const h = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}>
|
||||
<Link
|
||||
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
|
||||
href={makeCalendarPath(url, event.id)}>
|
||||
<CalendarEventHeader {event} />
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if h}
|
||||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<CalendarEventActions showActivity {url} {event} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {fromPairs} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||
import MapPoint from "@assets/icons/map-point.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
|
||||
@@ -15,12 +17,12 @@
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-1 text-sm opacity-75">
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon icon="user-circle" size={4} />
|
||||
<Icon icon={UserCircle} size={4} />
|
||||
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||
</span>
|
||||
{#if meta.location}
|
||||
<span class="flex items-start gap-1">
|
||||
<Icon icon="map-point" class="mt-[2px]" size={4} />
|
||||
<Icon icon={MapPoint} class="mt-[2px]" size={4} />
|
||||
<span class="break-words">{meta.location}</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {writable} from "svelte/store"
|
||||
import type {EventContent} from "@welshman/util"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
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="gallery-send" />
|
||||
{/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="plain" />
|
||||
</Button>
|
||||
</form>
|
||||
@@ -1,112 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {hash, now, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {thunks, deriveProfile, deriveProfileDisplay} from "@welshman/app"
|
||||
import {isMobile} from "@lib/html"
|
||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ChannelMessageZapButton from "@app/components/ChannelMessageZapButton.svelte"
|
||||
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
|
||||
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
|
||||
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
|
||||
import {colors, ENABLE_ZAPS} from "@app/core/state"
|
||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
replyTo?: (event: TrustedEvent) => void
|
||||
showPubkey?: boolean
|
||||
inert?: boolean
|
||||
}
|
||||
|
||||
const {url, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props()
|
||||
|
||||
const thunk = $thunks[event.id]
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const today = formatTimestampAsDate(now())
|
||||
const profile = deriveProfile(event.pubkey, [url])
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||
|
||||
const reply = () => replyTo!(event)
|
||||
|
||||
const onTap = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||
|
||||
const deleteReaction = async (event: TrustedEvent) =>
|
||||
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||
|
||||
const createReaction = async (template: EventContent) =>
|
||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||
</script>
|
||||
|
||||
<TapTarget
|
||||
data-event={event.id}
|
||||
onTap={inert ? null : onTap}
|
||||
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left">
|
||||
<div class="flex w-full gap-3 overflow-auto">
|
||||
{#if showPubkey}
|
||||
<Button onclick={openProfile} class="flex items-start">
|
||||
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} />
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="w-8 min-w-8 max-w-8"></div>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-grow pr-1">
|
||||
{#if showPubkey}
|
||||
<div class="flex items-center gap-2">
|
||||
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||
{$profileDisplay}
|
||||
</Button>
|
||||
<span class="text-xs opacity-50">
|
||||
{#if formatTimestampAsDate(event.created_at) === today}
|
||||
Today
|
||||
{:else}
|
||||
{formatTimestampAsDate(event.created_at)}
|
||||
{/if}
|
||||
at {formatTimestampAsTime(event.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-sm">
|
||||
<Content minimalQuote {event} {url} />
|
||||
{#if thunk}
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-2 ml-10 mt-1 pl-1">
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tooltip-right" />
|
||||
</div>
|
||||
{#if !isMobile}
|
||||
<button
|
||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}>
|
||||
{#if ENABLE_ZAPS}
|
||||
<ChannelMessageZapButton {url} {event} />
|
||||
{/if}
|
||||
<ChannelMessageEmojiButton {url} {event} />
|
||||
{#if replyTo}
|
||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||
<Icon icon="reply" size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
<ChannelMessageMenuButton {url} {event} />
|
||||
</button>
|
||||
{/if}
|
||||
</TapTarget>
|
||||
@@ -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}
|
||||
@@ -11,6 +11,7 @@
|
||||
MINUTE,
|
||||
sortBy,
|
||||
remove,
|
||||
enumerate,
|
||||
formatTimestampAsDate,
|
||||
} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
|
||||
@@ -30,7 +31,7 @@
|
||||
loadInboxRelaySelections,
|
||||
inboxRelaySelectionsByPubkey,
|
||||
} from "@welshman/app"
|
||||
import type {AbstractThunk} from "@welshman/app"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -49,7 +50,7 @@
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import {
|
||||
INDEXER_RELAYS,
|
||||
userSettingValues,
|
||||
userSettingsValues,
|
||||
deriveChat,
|
||||
splitChatId,
|
||||
PLATFORM_NAME,
|
||||
@@ -125,14 +126,13 @@
|
||||
|
||||
// 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
|
||||
const thunks: AbstractThunk[] = []
|
||||
for (let i = 0; i < templates.length; i++) {
|
||||
const template = templates[i]
|
||||
|
||||
thunks.push(
|
||||
await sendWrapped({pubkeys, template, delay: $userSettingValues.send_delay + ms(i)}),
|
||||
)
|
||||
}
|
||||
const thunks = Array.from(enumerate(templates)).map(([i, event]) =>
|
||||
sendWrapped({
|
||||
event,
|
||||
recipients: pubkeys,
|
||||
delay: $userSettingsValues.send_delay + ms(i),
|
||||
}),
|
||||
)
|
||||
|
||||
pushToast({
|
||||
timeout: 30_000,
|
||||
@@ -250,7 +250,7 @@
|
||||
<div
|
||||
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
||||
data-tip="{count} {label} not configured.">
|
||||
<Icon icon="danger" />
|
||||
<Icon icon={Danger} />
|
||||
{count}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -264,7 +264,7 @@
|
||||
<div class="py-12">
|
||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||
<p class="row-2 text-lg text-error">
|
||||
<Icon icon="danger" />
|
||||
<Icon icon={Danger} />
|
||||
Your inbox is not configured.
|
||||
</p>
|
||||
<p>
|
||||
@@ -277,7 +277,7 @@
|
||||
<div class="py-12">
|
||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||
<p class="row-2 text-lg text-error">
|
||||
<Icon icon="danger" />
|
||||
<Icon icon={Danger} />
|
||||
{missingInboxes.length}
|
||||
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
|
||||
</p>
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
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 {onSubmit}: Props = $props()
|
||||
|
||||
const autofocus = !isMobile
|
||||
|
||||
@@ -37,11 +38,11 @@
|
||||
}
|
||||
|
||||
const editor = makeEditor({
|
||||
url,
|
||||
autofocus,
|
||||
submit,
|
||||
uploading,
|
||||
aggressive: true,
|
||||
encryptFiles: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -54,7 +55,7 @@
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="gallery-send" />
|
||||
<Icon icon={GallerySend} />
|
||||
{/if}
|
||||
</Button>
|
||||
<div class="chat-editor flex-grow overflow-hidden">
|
||||
@@ -65,6 +66,6 @@
|
||||
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||
disabled={$uploading}
|
||||
onclick={submit}>
|
||||
<Icon icon="plain" />
|
||||
<Icon icon={Plane} />
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {displayProfileByPubkey} from "@welshman/app"
|
||||
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"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||
|
||||
const {
|
||||
verb,
|
||||
@@ -18,18 +19,13 @@
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
{#key event.id}
|
||||
<NoteContent
|
||||
{event}
|
||||
hideMediaAtDepth={0}
|
||||
minLength={100}
|
||||
maxLength={300}
|
||||
expandMode="disabled" />
|
||||
<NoteContentMinimal trimParent {event} />
|
||||
{/key}
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||
<Icon icon="close-circle" />
|
||||
<Icon icon={CloseCircle} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {WRAP} from "@welshman/util"
|
||||
import {repository} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {shouldUnwrap} from "@welshman/app"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/core/state"
|
||||
import {PLATFORM_NAME} from "@app/core/state"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
|
||||
const {next} = $props()
|
||||
@@ -17,22 +18,13 @@
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
const enableChat = async () => {
|
||||
canDecrypt.set(true)
|
||||
|
||||
for (const event of repository.query([{kinds: [WRAP]}])) {
|
||||
ensureUnwrapped(event)
|
||||
}
|
||||
|
||||
clearModals()
|
||||
goto(nextUrl)
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await enableChat()
|
||||
shouldUnwrap.set(true)
|
||||
clearModals()
|
||||
goto(nextUrl)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
@@ -60,12 +52,12 @@
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Enable Messages</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
<script lang="ts">
|
||||
import {waitForThunkCompletion} from "@welshman/app"
|
||||
import ChatSquare from "@assets/icons/chat-square.svg?dataurl"
|
||||
import Check from "@assets/icons/check.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ChatStart from "@app/components/ChatStart.svelte"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {dmAlert, userInboxRelays} from "@app/core/state"
|
||||
import {deleteAlert, createDmAlert} from "@app/core/commands"
|
||||
|
||||
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
||||
|
||||
@@ -11,15 +20,64 @@
|
||||
setChecked("/chat/*")
|
||||
history.back()
|
||||
}
|
||||
|
||||
const enableAlerts = async () => {
|
||||
if ($userInboxRelays.length === 0) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please set up your messaging relays before enabling alerts.",
|
||||
})
|
||||
}
|
||||
|
||||
enablingAlert = true
|
||||
|
||||
try {
|
||||
const {error} = await createDmAlert()
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
} finally {
|
||||
enablingAlert = false
|
||||
}
|
||||
}
|
||||
|
||||
const disableAlerts = async () => {
|
||||
disablingAlert = true
|
||||
|
||||
try {
|
||||
await waitForThunkCompletion(deleteAlert($dmAlert!))
|
||||
} finally {
|
||||
disablingAlert = false
|
||||
}
|
||||
}
|
||||
|
||||
let enablingAlert = $state(false)
|
||||
let disablingAlert = $state(false)
|
||||
</script>
|
||||
|
||||
<div class="col-2">
|
||||
<Button class="btn btn-primary" onclick={startChat}>
|
||||
<Icon size={4} icon="add-circle" />
|
||||
<Icon size={5} icon={ChatSquare} />
|
||||
Start chat
|
||||
</Button>
|
||||
<Button class="btn btn-neutral" onclick={markAsRead}>
|
||||
<Icon size={4} icon="check-circle" />
|
||||
<Icon size={5} icon={Check} />
|
||||
Mark all read
|
||||
</Button>
|
||||
{#if (!enablingAlert && $dmAlert) || disablingAlert}
|
||||
<Button class="btn btn-neutral" onclick={disableAlerts} disabled={disablingAlert}>
|
||||
{#if !disablingAlert}
|
||||
<Icon size={4} icon={BellOff} />
|
||||
{/if}
|
||||
<Spinner loading={disablingAlert}>Disable alerts</Spinner>
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral" onclick={enableAlerts} disabled={enablingAlert}>
|
||||
{#if !enablingAlert}
|
||||
<Icon size={4} icon={Bell} />
|
||||
{/if}
|
||||
<Spinner loading={enablingAlert}>Enable alerts</Spinner>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
import {type Instance} from "tippy.js"
|
||||
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {thunks, pubkey, deriveProfile, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
||||
import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
||||
import {isMobile} from "@lib/html"
|
||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
@@ -28,19 +29,18 @@
|
||||
|
||||
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
||||
|
||||
const thunk = $thunks[event.id]
|
||||
const isOwn = event.pubkey === $pubkey
|
||||
const profile = deriveProfile(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 deleteReaction = (event: TrustedEvent) =>
|
||||
sendWrapped({template: makeDelete({event, protect: false}), pubkeys})
|
||||
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
|
||||
|
||||
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})
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
class="opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}
|
||||
onclick={togglePopover}>
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
<Icon icon={MenuDots} size={4} />
|
||||
</button>
|
||||
</Tippy>
|
||||
{/if}
|
||||
@@ -99,8 +99,8 @@
|
||||
<div class="flex items-center gap-2">
|
||||
{#if !isOwn}
|
||||
<Button onclick={openProfile} class="flex items-center gap-1">
|
||||
<Avatar
|
||||
src={$profile?.picture}
|
||||
<ProfileCircle
|
||||
pubkey={event.pubkey}
|
||||
class="border border-solid border-base-content"
|
||||
size={4} />
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {sendWrapped} from "@welshman/app"
|
||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||
import {makeReaction} from "@app/core/commands"
|
||||
@@ -14,9 +15,12 @@
|
||||
const {event, pubkeys}: Props = $props()
|
||||
|
||||
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>
|
||||
|
||||
<EmojiButton {onEmoji} class="btn join-item btn-xs">
|
||||
<Icon icon="smile-circle" size={4} />
|
||||
<Icon icon={SmileCircle} size={4} />
|
||||
</EmojiButton>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
|
||||
const {event, pubkeys, popover, replyTo} = $props()
|
||||
|
||||
@@ -19,10 +21,10 @@
|
||||
<ChatMessageEmojiButton {event} {pubkeys} />
|
||||
{#if replyTo}
|
||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||
<Icon size={4} icon="reply" />
|
||||
<Icon size={4} icon={Reply} />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
||||
<Icon size={4} icon="code-2" />
|
||||
<Icon size={4} icon={Code2} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
import {makeReaction} from "@app/core/commands"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {clip} from "@app/util/toast"
|
||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
|
||||
type Props = {
|
||||
pubkeys: string[]
|
||||
@@ -20,7 +24,10 @@
|
||||
|
||||
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
||||
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)
|
||||
|
||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||
@@ -40,19 +47,19 @@
|
||||
|
||||
<div class="col-2">
|
||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||
<Icon size={4} icon="smile-circle" />
|
||||
<Icon size={4} icon={SmileCircle} />
|
||||
Send Reaction
|
||||
</Button>
|
||||
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||
<Icon size={4} icon="reply" />
|
||||
<Icon size={4} icon={Reply} />
|
||||
Send Reply
|
||||
</Button>
|
||||
<Button class="btn btn-neutral w-full" onclick={copyText}>
|
||||
<Icon size={4} icon="copy" />
|
||||
<Icon size={4} icon={Copy} />
|
||||
Copy Text
|
||||
</Button>
|
||||
<Button class="btn btn-neutral" onclick={showInfo}>
|
||||
<Icon size={4} icon="code-2" />
|
||||
<Icon size={4} icon={Code2} />
|
||||
Message Details
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
@@ -67,12 +69,12 @@
|
||||
</Field>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={pubkeys.length === 0}>
|
||||
Create Chat
|
||||
<Icon icon="alt-arrow-right" />
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
@@ -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-md" 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>
|
||||
@@ -18,8 +18,10 @@
|
||||
isAddress,
|
||||
isNewline,
|
||||
} from "@welshman/content"
|
||||
import type {Parsed} from "@welshman/content"
|
||||
import {preventDefault, stopPropagation} from "@lib/html"
|
||||
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"
|
||||
@@ -31,17 +33,15 @@
|
||||
import ContentQuote from "@app/components/ContentQuote.svelte"
|
||||
import ContentTopic from "@app/components/ContentTopic.svelte"
|
||||
import ContentMention from "@app/components/ContentMention.svelte"
|
||||
import {entityLink, userSettingValues} from "@app/core/state"
|
||||
import {entityLink, userSettingsValues} from "@app/core/state"
|
||||
|
||||
interface Props {
|
||||
event: any
|
||||
minLength?: number
|
||||
maxLength?: number
|
||||
showEntire?: boolean
|
||||
hideMediaAtDepth?: number
|
||||
expandMode?: string
|
||||
minimalQuote?: boolean
|
||||
depth?: number
|
||||
trimParent?: boolean
|
||||
url?: string
|
||||
}
|
||||
|
||||
@@ -50,10 +50,8 @@
|
||||
minLength = 500,
|
||||
maxLength = 700,
|
||||
showEntire = $bindable(false),
|
||||
hideMediaAtDepth = 1,
|
||||
expandMode = "block",
|
||||
minimalQuote = false,
|
||||
depth = 0,
|
||||
trimParent = false,
|
||||
url,
|
||||
}: Props = $props()
|
||||
|
||||
@@ -66,13 +64,13 @@
|
||||
const isBlock = (i: number) => {
|
||||
const parsed = fullContent[i]
|
||||
|
||||
if (!parsed || hideMediaAtDepth <= depth) return false
|
||||
if (!parsed) return false
|
||||
|
||||
if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) {
|
||||
if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i)) {
|
||||
if (isQuote(parsed) && isStartAndEnd(i)) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -94,25 +92,47 @@
|
||||
|
||||
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 = () => {
|
||||
warning = null
|
||||
}
|
||||
|
||||
let warning = $state(
|
||||
$userSettingValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
|
||||
$userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
|
||||
)
|
||||
|
||||
const shortContent = $derived(
|
||||
showEntire
|
||||
? fullContent
|
||||
: truncate(fullContent, {
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: hideMediaAtDepth <= depth ? 20 : 200,
|
||||
}),
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
if (!showEntire) {
|
||||
result = truncate(result, {
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: 200,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const hasEllipsis = $derived(shortContent.some(isEllipsis))
|
||||
const expandInline = $derived(hasEllipsis && expandMode === "inline")
|
||||
@@ -122,7 +142,7 @@
|
||||
<div class="relative">
|
||||
{#if warning}
|
||||
<div class="card2 card2-sm bg-alt row-2">
|
||||
<Icon icon="danger" />
|
||||
<Icon icon={Danger} />
|
||||
<p>
|
||||
This note has been flagged by the author as "{warning}".<br />
|
||||
<Button class="link" onclick={ignoreWarning}>Show anyway</Button>
|
||||
@@ -153,15 +173,9 @@
|
||||
{/if}
|
||||
{:else if isProfile(parsed)}
|
||||
<ContentMention value={parsed.value} {url} />
|
||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
||||
{:else if isQuote(parsed)}
|
||||
{#if isBlock(i)}
|
||||
<ContentQuote
|
||||
{depth}
|
||||
{url}
|
||||
{hideMediaAtDepth}
|
||||
value={parsed.value}
|
||||
{event}
|
||||
minimal={minimalQuote} />
|
||||
<ContentQuote {url} value={parsed.value} {event} />
|
||||
{:else}
|
||||
<Link
|
||||
external
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type {ParsedEmojiValue} from "@welshman/content"
|
||||
import {imgproxy} from "@app/core/state"
|
||||
|
||||
export let value: ParsedEmojiValue
|
||||
|
||||
@@ -8,10 +7,7 @@
|
||||
</script>
|
||||
|
||||
{#if value.url}
|
||||
<img
|
||||
{alt}
|
||||
src={imgproxy(value.url, {w: 24, h: 24})}
|
||||
class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
|
||||
<img {alt} src={value.url} class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
|
||||
{:else}
|
||||
{alt}
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 Link from "@lib/components/Link.svelte"
|
||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||
@@ -51,7 +51,7 @@
|
||||
<img
|
||||
alt="Link preview"
|
||||
onerror={onError}
|
||||
src={imgproxy(preview.image)}
|
||||
src={preview.image}
|
||||
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2 p-4">
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<script lang="ts">
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
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 Icon from "@lib/components/Icon.svelte"
|
||||
import {imgproxy} from "@app/core/state"
|
||||
|
||||
const {value, event, ...props} = $props()
|
||||
|
||||
@@ -13,18 +21,34 @@
|
||||
.map(tagsFromIMeta)
|
||||
.find(meta => getTagValue("url", meta) === url) || event.tags
|
||||
|
||||
const hash = getTagValue("x", meta)
|
||||
const key = getTagValue("decryption-key", meta)
|
||||
const nonce = getTagValue("decryption-nonce", meta)
|
||||
const algorithm = getTagValue("encryption-algorithm", meta)
|
||||
|
||||
const onError = () => {
|
||||
hasError = true
|
||||
const onError = async () => {
|
||||
// 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 src = $state(imgproxy(url))
|
||||
let src = $state("")
|
||||
|
||||
onMount(async () => {
|
||||
// If we have an encryption algorithm, fetch and decrypt
|
||||
if (algorithm === "aes-gcm" && key && nonce) {
|
||||
const response = await fetch(url)
|
||||
|
||||
@@ -32,8 +56,10 @@
|
||||
const ciphertext = new Uint8Array(await response.arrayBuffer())
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -44,9 +70,9 @@
|
||||
|
||||
{#if hasError}
|
||||
<a href={url} class="link-content whitespace-nowrap">
|
||||
<Icon icon="link-round" size={3} class="inline-block" />
|
||||
<Icon icon={LinkRound} size={3} class="inline-block" />
|
||||
{displayUrl(url)}
|
||||
</a>
|
||||
{:else}
|
||||
{:else if src}
|
||||
<img alt="" {src} onerror={onError} {...props} />
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {displayUrl} from "@welshman/lib"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||
@@ -16,12 +17,12 @@
|
||||
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
||||
<!-- Use a real link so people can copy the href -->
|
||||
<a href={url} class="link-content whitespace-nowrap" onclick={preventDefault(expand)}>
|
||||
<Icon icon="link-round" size={3} class="inline-block" />
|
||||
<Icon icon={LinkRound} size={3} class="inline-block" />
|
||||
{displayUrl(url)}
|
||||
</a>
|
||||
{:else}
|
||||
<Link external href={url} class="link-content whitespace-nowrap">
|
||||
<Icon icon="link-round" size={3} class="inline-block" />
|
||||
<Icon icon={LinkRound} size={3} class="inline-block" />
|
||||
{displayUrl(url)}
|
||||
</Link>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import type {ProfilePointer} from "@welshman/content"
|
||||
import {deriveProfileDisplay} from "@welshman/app"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
const {value, url}: Props = $props()
|
||||
|
||||
const display = deriveProfileDisplay(value.pubkey, removeNil([url]))
|
||||
const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
||||
</script>
|
||||
|
||||
@@ -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,20 +6,17 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.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 {goToEvent} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
value: any
|
||||
hideMediaAtDepth: number
|
||||
event: TrustedEvent
|
||||
depth: number
|
||||
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 idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
||||
@@ -43,17 +40,17 @@
|
||||
}
|
||||
</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 minimal && $quote.kind === MESSAGE}
|
||||
{#if $quote.kind === MESSAGE}
|
||||
<div
|
||||
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%);">
|
||||
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
|
||||
<NoteContentMinimal trimParent {url} event={$quote} />
|
||||
</div>
|
||||
{:else}
|
||||
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
|
||||
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
|
||||
<NoteContentMinimal {url} event={$quote} />
|
||||
</NoteCard>
|
||||
{/if}
|
||||
{:else}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {clip} from "@app/util/toast"
|
||||
@@ -9,6 +10,6 @@
|
||||
</script>
|
||||
|
||||
<Button onclick={copy} class="link-content">
|
||||
<Icon icon="bolt" size={3} class="inline-block translate-y-px" />
|
||||
<Icon icon={Bolt} size={3} class="inline-block translate-y-px" />
|
||||
{value.slice(0, 16)}...
|
||||
</Button>
|
||||
|
||||
@@ -1,46 +1,47 @@
|
||||
<script lang="ts">
|
||||
import {formatTimestamp} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.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 ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import {goToEvent} from "@app/util/routes"
|
||||
import {displayChannel} from "@app/core/state"
|
||||
import {displayRoom} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
h?: string
|
||||
events: TrustedEvent[]
|
||||
latest: TrustedEvent
|
||||
earliest: TrustedEvent
|
||||
participants: string[]
|
||||
}
|
||||
|
||||
const {url, room, events, latest, earliest, participants}: Props = $props()
|
||||
const {url, h, events, latest, earliest, participants}: Props = $props()
|
||||
</script>
|
||||
|
||||
<Button class="card2 bg-alt" onclick={() => goToEvent(earliest)}>
|
||||
<Button class="card2 bg-alt shadow-lg" onclick={() => goToEvent(earliest)}>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<ProfileCircle pubkey={earliest.pubkey} size={10} />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||
{#if room}
|
||||
{#if h}
|
||||
<span class="truncate font-medium text-blue-400">
|
||||
#{displayChannel(url, room)}
|
||||
#{displayRoom(url, h)}
|
||||
</span>
|
||||
<span class="opacity-50">•</span>
|
||||
{/if}
|
||||
<span class="text-nowrap">{formatTimestamp(earliest.created_at)}</span>
|
||||
</div>
|
||||
<Content minimalQuote minLength={100} maxLength={400} event={earliest} />
|
||||
<NoteContentMinimal event={earliest} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-13 flex items-center justify-between">
|
||||
<div class="flex gap-1">
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
<span class="text-sm opacity-70">
|
||||
{events.length}
|
||||
{events.length === 1 ? "message" : "messages"}
|
||||
@@ -66,7 +67,7 @@
|
||||
{formatTimestamp(latest.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<Content minimalQuote minLength={100} maxLength={400} event={latest} />
|
||||
<NoteContentMinimal event={latest} />
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import type {Instance} from "tippy.js"
|
||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -42,11 +45,11 @@
|
||||
<Button class="join rounded-full">
|
||||
{#if ENABLE_ZAPS && !hideZap}
|
||||
<ZapButton {url} {event} class="btn join-item btn-neutral btn-xs">
|
||||
<Icon icon="bolt" size={4} />
|
||||
<Icon icon={Bolt} size={4} />
|
||||
</ZapButton>
|
||||
{/if}
|
||||
<EmojiButton {onEmoji} class="btn join-item btn-neutral btn-xs">
|
||||
<Icon icon="smile-circle" size={4} />
|
||||
<Icon icon={SmileCircle} size={4} />
|
||||
</EmojiButton>
|
||||
<Tippy
|
||||
bind:popover
|
||||
@@ -54,7 +57,7 @@
|
||||
props={{url, noun, event, customActions, onClick: hidePopover}}
|
||||
params={{trigger: "manual", interactive: true}}>
|
||||
<Button class="btn join-item btn-neutral btn-xs" onclick={showPopover}>
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
<Icon icon={MenuDots} size={4} />
|
||||
</Button>
|
||||
</Tippy>
|
||||
</Button>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {repository} from "@welshman/app"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
|
||||
@@ -21,7 +22,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full">
|
||||
<Icon icon="reply" />
|
||||
<Icon icon={Reply} />
|
||||
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
|
||||
</div>
|
||||
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
import {LOCALE, secondsToDate} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import FileText from "@assets/icons/file-text.svg?dataurl"
|
||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -56,10 +59,10 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="file" />
|
||||
<Icon icon={FileText} />
|
||||
<input type="text" class="ellipsize min-w-0 grow" value={nevent1} />
|
||||
<Button onclick={copyLink} class="flex items-center">
|
||||
<Icon icon="copy" />
|
||||
<Icon icon={Copy} />
|
||||
</Button>
|
||||
</label>
|
||||
{/snippet}
|
||||
@@ -70,10 +73,10 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="user-circle" />
|
||||
<Icon icon={UserCircle} />
|
||||
<input type="text" class="ellipsize min-w-0 grow" value={npub1} />
|
||||
<Button onclick={copyPubkey} class="flex items-center">
|
||||
<Icon icon="copy" />
|
||||
<Icon icon={Copy} />
|
||||
</Button>
|
||||
</label>
|
||||
{/snippet}
|
||||
@@ -98,7 +101,7 @@
|
||||
<pre class="card2 card2-sm bg-alt overflow-auto text-xs"><code>{json}</code></pre>
|
||||
<p class="absolute right-2 top-2 flex flex-grow items-center justify-between">
|
||||
<Button onclick={copyJson} class="btn btn-neutral btn-sm flex items-center">
|
||||
<Icon icon="copy" /> Copy
|
||||
<Icon icon={Copy} /> Copy
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import type {Snippet} from "svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import type {TrustedEvent} 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 Icon from "@lib/components/Icon.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import EventReport from "@app/components/EventReport.svelte"
|
||||
import EventShare from "@app/components/EventShare.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import {hasNip29} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {makeSpaceChatPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
@@ -28,7 +36,14 @@
|
||||
|
||||
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})
|
||||
|
||||
@@ -39,18 +54,18 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}>
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md" bind:this={ul}>
|
||||
{#if isRoot}
|
||||
<li>
|
||||
<Button onclick={share}>
|
||||
<Icon size={4} icon="share-circle" />
|
||||
<Icon size={4} icon={ShareCircle} />
|
||||
Share to Chat
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
<li>
|
||||
<Button onclick={showInfo}>
|
||||
<Icon size={4} icon="code-2" />
|
||||
<Icon size={4} icon={Code2} />
|
||||
{noun} Details
|
||||
</Button>
|
||||
</li>
|
||||
@@ -58,14 +73,14 @@
|
||||
{#if event.pubkey === $pubkey}
|
||||
<li>
|
||||
<Button onclick={showDelete} class="text-error">
|
||||
<Icon size={4} icon="trash-bin-2" />
|
||||
<Icon size={4} icon={TrashBin2} />
|
||||
Delete {noun}
|
||||
</Button>
|
||||
</li>
|
||||
{:else}
|
||||
<li>
|
||||
<Button class="text-error" onclick={report}>
|
||||
<Icon size={4} icon="danger" />
|
||||
<Icon size={4} icon={Danger} />
|
||||
Report Content
|
||||
</Button>
|
||||
</li>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {writable} from "svelte/store"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import {fly} from "@lib/transition"
|
||||
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
@@ -81,7 +82,7 @@
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="paperclip" size={3} />
|
||||
<Icon icon={Paperclip} size={3} />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
@@ -78,12 +80,12 @@
|
||||
</Field>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Send Report</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
import {goto} from "$app/navigation"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.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 Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import {channelsByUrl} from "@app/core/state"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {roomsByUrl} from "@app/core/state"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
import {setKey} from "@lib/implicit"
|
||||
|
||||
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
|
||||
|
||||
@@ -20,8 +22,8 @@
|
||||
goto(makeRoomPath(url, selection), {replaceState: true})
|
||||
}
|
||||
|
||||
const toggleRoom = (room: string) => {
|
||||
selection = room === selection ? "" : room
|
||||
const toggleRoom = (h: string) => {
|
||||
selection = h === selection ? "" : h
|
||||
}
|
||||
|
||||
let selection = $state("")
|
||||
@@ -37,25 +39,25 @@
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
{#each $channelsByUrl.get(url) || [] as channel (channel.room)}
|
||||
{#each $roomsByUrl.get(url) || [] as room (room.h)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
class:btn-neutral={selection !== channel.room}
|
||||
class:btn-primary={selection === channel.room}
|
||||
onclick={() => toggleRoom(channel.room)}>
|
||||
#<ChannelName {...channel} />
|
||||
class:btn-neutral={selection !== room.h}
|
||||
class:btn-primary={selection === room.h}
|
||||
onclick={() => toggleRoom(room.h)}>
|
||||
#<RoomName {...room} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={!selection}>
|
||||
Share {noun}
|
||||
<Icon icon="alt-arrow-right" />
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<script lang="ts">
|
||||
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 ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {makeGoalPath} from "@app/util/routes"
|
||||
import {makeGoalPath, makeSpacePath} from "@app/util/routes"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
event: any
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
}
|
||||
|
||||
const {url, event, showActivity = false}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const path = makeGoalPath(url, event.id)
|
||||
const h = getTagValue("h", event.tags)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const deleteReaction = async (event: TrustedEvent) =>
|
||||
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||
@@ -26,13 +30,16 @@
|
||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
</div>
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import {makeEvent, ZAP_GOAL} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
@@ -15,7 +18,12 @@
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {canEnforceNip70} from "@app/core/commands"
|
||||
|
||||
const {url} = $props()
|
||||
type Props = {
|
||||
url: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -56,6 +64,10 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(ZAP_GOAL, {content, tags}),
|
||||
@@ -114,7 +126,7 @@
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="paperclip" size={3} />
|
||||
<Icon icon={Paperclip} size={3} />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -126,7 +138,7 @@
|
||||
{#snippet input()}
|
||||
<div class="flex flex-grow justify-end">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<Icon icon="bolt" />
|
||||
<Icon icon={Bolt} />
|
||||
<input bind:value={amount} type="number" class="w-28" />
|
||||
<p class="opacity-50">sats</p>
|
||||
</label>
|
||||
@@ -144,7 +156,7 @@
|
||||
</div>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary">Create Goal</Button>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import GoalActions from "@app/components/GoalActions.svelte"
|
||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import {makeGoalPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -16,9 +17,10 @@
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const summary = getTagValue("summary", event.tags)
|
||||
const h = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeGoalPath(url, event.id)}>
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-md" href={makeGoalPath(url, event.id)}>
|
||||
<p class="text-2xl">{event.content}</p>
|
||||
<Content
|
||||
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">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if h}
|
||||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<GoalActions showActivity {url} {event} />
|
||||
</div>
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
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 ZapButton from "@app/components/ZapButton.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
url?: string
|
||||
event: TrustedEvent
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
const {url, event, ...props}: Props = $props()
|
||||
|
||||
const zaps = deriveEventsMapped<Zap>(repository, {
|
||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||
@@ -26,7 +28,7 @@
|
||||
const daysOld = Math.ceil((now() - event.created_at) / DAY)
|
||||
</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>
|
||||
<p class="text-xl text-primary">{zapAmount} sats</p>
|
||||
@@ -43,7 +45,7 @@
|
||||
</div>
|
||||
<progress class="progress progress-primary" value={zapAmount} max={goalAmount}></progress>
|
||||
<ZapButton {url} {event} class="btn btn-primary lg:m-auto lg:px-20">
|
||||
<Icon icon="bolt" />
|
||||
<Icon icon={Bolt} />
|
||||
Contribute to this goal
|
||||
</ZapButton>
|
||||
</div>
|
||||
|
||||
@@ -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-2xl">
|
||||
<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,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {session} from "@welshman/app"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
@@ -27,8 +29,8 @@
|
||||
</p>
|
||||
<p>
|
||||
On <Link external href="https://nostr.com/">Nostr</Link>, <strong>you</strong> control your own
|
||||
identity and social data, through the magic of crytography. The basic idea is that you have a
|
||||
<strong>public key</strong>, which acts as your user id, and a
|
||||
identity and social data, through the magic of cryptography. The basic idea is that you have a
|
||||
<strong>public key</strong>, which acts as your user ID, and a
|
||||
<strong>private key</strong> which allows you to prove your identity.
|
||||
</p>
|
||||
{#if $session?.email}
|
||||
@@ -39,11 +41,11 @@
|
||||
<p>If you'd like to switch to self-custody, please click below to get started.</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button class="btn btn-primary" onclick={startEject}>
|
||||
<Icon icon="check-circle" />
|
||||
<Icon icon={CheckCircle} />
|
||||
I want to hold my own keys
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
|
||||
const back = () => history.back()
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>What are digital signatures?</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<p>
|
||||
Most online services ask their users to trust them that they're being honest, and they usually
|
||||
are. However, traditional social media platforms have the ability to <strong
|
||||
>create forged content</strong> that can appear to be genuinely authored, but which are actually
|
||||
counterfeit.
|
||||
</p>
|
||||
<p>
|
||||
On <Link external href="https://nostr.com/">Nostr</Link>, all your content is authenticated
|
||||
using <strong>digital signatures</strong>, which cryptographically tie a particular person to a
|
||||
given post or message.
|
||||
</p>
|
||||
<p>
|
||||
The result is that you don't normally have to trust service providers not to tamper with the
|
||||
information flowing through the network — instead, your client software can prove that a given
|
||||
piece of data is authentic.
|
||||
</p>
|
||||
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
||||
</div>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {deriveZapperForPubkey} from "@welshman/app"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
@@ -29,7 +30,7 @@
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
+35
-22
@@ -1,7 +1,11 @@
|
||||
<script lang="ts">
|
||||
import {randomId} from "@welshman/lib"
|
||||
import {preventDefault, stopPropagation} from "@lib/html"
|
||||
import {randomId, call} from "@welshman/lib"
|
||||
import {preventDefault, stopPropagation, compressFile} from "@lib/html"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {uploadFile} from "@app/core/commands"
|
||||
|
||||
interface Props {
|
||||
file?: File | undefined
|
||||
@@ -24,14 +28,14 @@
|
||||
active = false
|
||||
}
|
||||
|
||||
const onDrop = (e: any) => {
|
||||
const onDrop = async (e: any) => {
|
||||
active = false
|
||||
|
||||
file = e.dataTransfer.files[0]
|
||||
file = await compressFile(e.dataTransfer.files[0])
|
||||
}
|
||||
|
||||
const onChange = (e: any) => {
|
||||
file = e.target.files[0]
|
||||
const onChange = async (e: any) => {
|
||||
file = await compressFile(e.target.files[0])
|
||||
}
|
||||
|
||||
const onClear = () => {
|
||||
@@ -44,20 +48,29 @@
|
||||
let initialUrl = $state(url)
|
||||
|
||||
$effect(() => {
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
call(async () => {
|
||||
if (file) {
|
||||
const {result} = await uploadFile(file)
|
||||
|
||||
reader.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
url = reader.result as string
|
||||
},
|
||||
false,
|
||||
)
|
||||
reader.readAsDataURL(file)
|
||||
} else {
|
||||
url = initialUrl
|
||||
}
|
||||
if (result?.url) {
|
||||
url = result.url
|
||||
} else {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
url = reader.result as string
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
} else {
|
||||
url = initialUrl
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -84,14 +97,14 @@
|
||||
tabindex="-1"
|
||||
onmousedown={stopPropagation(onClear)}
|
||||
ontouchstart={stopPropagation(onClear)}>
|
||||
<Icon icon="close-circle" class="scale-150 !bg-base-300" />
|
||||
<Icon icon={CloseCircle} class="scale-150 !bg-base-300" />
|
||||
</span>
|
||||
{:else}
|
||||
<Icon icon="add-circle" class="scale-150 !bg-base-300" />
|
||||
<Icon icon={AddCircle} class="scale-150 !bg-base-300" />
|
||||
{/if}
|
||||
</div>
|
||||
{#if !url}
|
||||
<Icon icon="gallery-send" size={7} />
|
||||
<Icon icon={GallerySend} size={7} />
|
||||
{/if}
|
||||
</label>
|
||||
</form>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Login from "@assets/icons/login-3.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
@@ -21,9 +23,9 @@
|
||||
<p class="text-center">The chat app built for self-hosted communities.</p>
|
||||
</div>
|
||||
<Button onclick={logIn}>
|
||||
<CardButton class="!btn-primary">
|
||||
<CardButton class="btn-primary">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="login-2" size={7} /></div>
|
||||
<div><Icon icon={Login} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Log in</div>
|
||||
@@ -33,10 +35,10 @@
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Button>
|
||||
<Button onclick={signUp}>
|
||||
<Button onclick={signUp} class="btn-neutral">
|
||||
<CardButton>
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="add-circle" size={7} /></div>
|
||||
<div><Icon icon={AddCircle} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Create an account</div>
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
|
||||
import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
|
||||
import Widget from "@assets/icons/widget-2.svg?dataurl"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
|
||||
import Compass from "@assets/icons/compass-big.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -13,7 +17,6 @@
|
||||
import {pushModal, clearModals} from "@app/util/modal"
|
||||
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {loadUserData} from "@app/core/requests"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
|
||||
let signers: any[] = $state([])
|
||||
@@ -23,9 +26,7 @@
|
||||
|
||||
const signUp = () => pushModal(SignUp)
|
||||
|
||||
const onSuccess = async (session: Session, relays: string[] = []) => {
|
||||
await loadUserData(session.pubkey, relays)
|
||||
|
||||
const onSuccess = async (session: Session) => {
|
||||
addSession(session)
|
||||
pushToast({message: "Successfully logged in!"})
|
||||
setChecked("*")
|
||||
@@ -96,7 +97,7 @@
|
||||
{#if loading === "nip07"}
|
||||
<span class="loading loading-spinner mr-3"></span>
|
||||
{:else}
|
||||
<Icon icon="widget" />
|
||||
<Icon icon={Widget} />
|
||||
{/if}
|
||||
Log in with Extension
|
||||
</Button>
|
||||
@@ -116,7 +117,7 @@
|
||||
{#if loading === "password"}
|
||||
<span class="loading loading-spinner mr-3"></span>
|
||||
{:else}
|
||||
<Icon icon="key" />
|
||||
<Icon icon={Key} />
|
||||
{/if}
|
||||
Log in with Password
|
||||
</Button>
|
||||
@@ -125,7 +126,7 @@
|
||||
onclick={loginWithBunker}
|
||||
{disabled}
|
||||
class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}">
|
||||
<Icon icon="cpu" />
|
||||
<Icon icon={Cpu} />
|
||||
Log in with Remote Signer
|
||||
</Button>
|
||||
{#if BURROW_URL && hasSigner}
|
||||
@@ -133,7 +134,7 @@
|
||||
{#if loading === "password"}
|
||||
<span class="loading loading-spinner mr-3"></span>
|
||||
{:else}
|
||||
<Icon icon="key" />
|
||||
<Icon icon={Key} />
|
||||
{/if}
|
||||
Log in with Password
|
||||
</Button>
|
||||
@@ -144,7 +145,7 @@
|
||||
{disabled}
|
||||
href="https://nostrapps.com#signers"
|
||||
class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}">
|
||||
<Icon icon="compass" />
|
||||
<Icon icon={Compass} />
|
||||
Browse Signer Apps
|
||||
</Link>
|
||||
{/if}
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import BunkerConnect from "@app/components/BunkerConnect.svelte"
|
||||
import BunkerUrl from "@app/components/BunkerUrl.svelte"
|
||||
import {Nip46Controller} from "@app/util/nip46"
|
||||
import {loadUserData} from "@app/core/requests"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
@@ -30,8 +31,6 @@
|
||||
onNostrConnect: async (response: Nip46ResponseWithResult) => {
|
||||
const pubkey = await controller.broker.getPublicKey()
|
||||
|
||||
await loadUserData(pubkey)
|
||||
|
||||
loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS)
|
||||
setChecked("*")
|
||||
clearModals()
|
||||
@@ -46,13 +45,20 @@
|
||||
try {
|
||||
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl($bunker)
|
||||
|
||||
if (!signerPubkey || relays.length === 0) {
|
||||
if (!signerPubkey) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
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)
|
||||
|
||||
const {clientSecret} = controller
|
||||
@@ -65,8 +71,6 @@
|
||||
broker.cleanup()
|
||||
controller.stop()
|
||||
|
||||
await loadUserData(pubkey)
|
||||
|
||||
loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
|
||||
} else {
|
||||
return pushToast({
|
||||
@@ -89,6 +93,7 @@
|
||||
}
|
||||
|
||||
const selectConnect = () => {
|
||||
controller.loading.set(false)
|
||||
mode = "connect"
|
||||
}
|
||||
|
||||
@@ -133,13 +138,13 @@
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back} disabled={$loading}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
{#if mode === "bunker"}
|
||||
<Button type="submit" class="btn btn-primary" disabled={$loading || !$bunker}>
|
||||
<Spinner loading={$loading}>Next</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
{/if}
|
||||
</ModalFooter>
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
||||
import {loadUserData} from "@app/core/requests"
|
||||
import {clearModals, pushModal} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
@@ -92,8 +95,6 @@
|
||||
const pubkey = await broker.getPublicKey()
|
||||
const session = makeNip46Session(pubkey, clientSecret, response.event.pubkey, relays)
|
||||
|
||||
await loadUserData(pubkey)
|
||||
|
||||
addSession({...session, email})
|
||||
broker.cleanup()
|
||||
setChecked("*")
|
||||
@@ -121,7 +122,7 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="user-rounded" />
|
||||
<Icon icon={UserRounded} />
|
||||
<input bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
@@ -132,7 +133,7 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="key" />
|
||||
<Icon icon={Key} />
|
||||
<input bind:value={password} type="password" />
|
||||
</label>
|
||||
{/snippet}
|
||||
@@ -144,12 +145,12 @@
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
|
||||
<Spinner {loading}>Next</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {preventDefault} from "@lib/html"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -32,7 +33,7 @@
|
||||
<p class="text-center">Your local database will be cleared.</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<script lang="ts">
|
||||
import UserRounded from "@assets/icons/user-rounded.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 Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import Wallet from "@assets/icons/wallet.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -6,15 +14,18 @@
|
||||
import LogOut from "@app/components/LogOut.svelte"
|
||||
import {PLATFORM_NAME} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {theme} from "@app/util/theme"
|
||||
|
||||
const logout = () => pushModal(LogOut)
|
||||
|
||||
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
|
||||
</script>
|
||||
|
||||
<div class="column menu gap-2">
|
||||
<Link replaceState href="/settings/profile">
|
||||
<CardButton>
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="user-rounded" size={7} /></div>
|
||||
<div><Icon icon={UserRounded} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Profile</div>
|
||||
@@ -24,10 +35,36 @@
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Link replaceState href="/settings/relays">
|
||||
<CardButton>
|
||||
<Link replaceState href="/settings/alerts">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="server" size={7} /></div>
|
||||
<div><Icon icon={Bell} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Alerts</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Set up email digests and push notifications</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Link replaceState href="/settings/wallet">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Wallet} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Wallet</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Connect a bitcoin wallet for sending social tips</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Link replaceState href="/settings/relays">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Server} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Relays</div>
|
||||
@@ -37,10 +74,10 @@
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Link replaceState href="/settings">
|
||||
<CardButton>
|
||||
<Link replaceState href="/settings/content">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="settings" size={7} /></div>
|
||||
<div><Icon icon={Settings} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Settings</div>
|
||||
@@ -50,10 +87,23 @@
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Link replaceState href="/settings/about">
|
||||
<CardButton>
|
||||
<Button onclick={toggleTheme}>
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="code-2" size={7} /></div>
|
||||
<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">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Code2} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>About</div>
|
||||
@@ -64,6 +114,6 @@
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Button onclick={logout} class="btn btn-neutral">
|
||||
<Icon icon="exit" /> Log Out
|
||||
<Icon icon={Exit} /> Log Out
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,59 @@
|
||||
<script lang="ts">
|
||||
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 {fly} from "@lib/transition"
|
||||
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 LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import SquareTopDown from "@assets/icons/square-top-down.svg?dataurl"
|
||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Login from "@assets/icons/login-3.svg?dataurl"
|
||||
import History from "@assets/icons/history.svg?dataurl"
|
||||
import Tuning2 from "@assets/icons/tuning-2.svg?dataurl"
|
||||
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Popover from "@lib/components/Popover.svelte"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||
import SpaceDetail from "@app/components/SpaceDetail.svelte"
|
||||
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import ProfileList from "@app/components/ProfileList.svelte"
|
||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||
import Alerts from "@app/components/Alerts.svelte"
|
||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
||||
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
||||
import {
|
||||
ENABLE_ZAPS,
|
||||
userRoomsByUrl,
|
||||
hasMembershipUrl,
|
||||
memberships,
|
||||
CONTENT_KINDS,
|
||||
deriveSpaceMembers,
|
||||
deriveEventsForUrl,
|
||||
deriveUserRooms,
|
||||
deriveOtherRooms,
|
||||
userSpaceUrls,
|
||||
hasNip29,
|
||||
alerts,
|
||||
deriveUserCanCreateRoom,
|
||||
deriveUserIsSpaceAdmin,
|
||||
} from "@app/core/state"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {makeSpacePath, makeChatPath} from "@app/util/routes"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
@@ -40,7 +64,14 @@
|
||||
const calendarPath = makeSpacePath(url, "calendar")
|
||||
const userRooms = deriveUserRooms(url)
|
||||
const otherRooms = deriveOtherRooms(url)
|
||||
const members = deriveSpaceMembers(url)
|
||||
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
|
||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||
|
||||
const spaceKinds = derived(
|
||||
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
||||
$events => new Set($events.map(e => e.kind)),
|
||||
)
|
||||
|
||||
const openMenu = () => {
|
||||
showMenu = true
|
||||
@@ -50,13 +81,17 @@
|
||||
showMenu = !showMenu
|
||||
}
|
||||
|
||||
const showDetail = () => pushModal(SpaceDetail, {url}, {replaceState})
|
||||
|
||||
const showMembers = () =>
|
||||
pushModal(
|
||||
ProfileList,
|
||||
{url, pubkeys: members, title: `Members of`, subtitle: displayRelayUrl(url)},
|
||||
{url, pubkeys: $members, title: `Members of`, subtitle: displayRelayUrl(url)},
|
||||
{replaceState},
|
||||
)
|
||||
|
||||
const canCreateRoom = deriveUserCanCreateRoom(url)
|
||||
|
||||
const createInvite = () => pushModal(SpaceInvite, {url}, {replaceState})
|
||||
|
||||
const leaveSpace = () => pushModal(SpaceExit, {url}, {replaceState})
|
||||
@@ -76,10 +111,6 @@
|
||||
let replaceState = $state(false)
|
||||
let element: Element | undefined = $state()
|
||||
|
||||
const members = $derived(
|
||||
$memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey),
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
replaceState = Boolean(element?.closest(".drawer"))
|
||||
})
|
||||
@@ -88,38 +119,65 @@
|
||||
<div bind:this={element} class="flex h-full flex-col justify-between">
|
||||
<SecondaryNavSection>
|
||||
<div>
|
||||
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
|
||||
<strong class="ellipsize flex items-center gap-3">
|
||||
{displayRelayUrl(url)}
|
||||
</strong>
|
||||
<Icon icon="alt-arrow-down" />
|
||||
</SecondaryNavItem>
|
||||
<Button
|
||||
class="flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||
onclick={openMenu}>
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="ellipsize flex items-center gap-1">
|
||||
<RelayName {url} />
|
||||
</strong>
|
||||
<Icon icon={AltArrowDown} />
|
||||
</div>
|
||||
<span class="text-xs text-primary">{displayRelayUrl(url)}</span>
|
||||
</Button>
|
||||
{#if showMenu}
|
||||
<Popover hideOnClick onClose={toggleMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
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="user-rounded" />
|
||||
View Members ({members.length})
|
||||
</Button>
|
||||
</li>
|
||||
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
<li>
|
||||
<Button onclick={createInvite}>
|
||||
<Icon icon="link-round" />
|
||||
<Icon icon={LinkRound} />
|
||||
Create Invite
|
||||
</Button>
|
||||
</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
|
||||
<Icon icon={SquareTopDown} size={4} class="opacity-50" />
|
||||
</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">
|
||||
<Icon icon="exit" />
|
||||
<Icon icon={Exit} />
|
||||
Leave Space
|
||||
</Button>
|
||||
{:else}
|
||||
<Button onclick={joinSpace} class="bg-primary text-primary-content">
|
||||
<Icon icon="login-2" />
|
||||
<Icon icon={Login} />
|
||||
Join Space
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -128,37 +186,50 @@
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto">
|
||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url)}>
|
||||
<Icon icon="home-smile" /> Home
|
||||
</SecondaryNavItem>
|
||||
{#if ENABLE_ZAPS}
|
||||
<div class="flex max-h-[calc(100vh-250px)] min-h-0 flex-col gap-1 overflow-auto">
|
||||
{#if hasNip29($relay)}
|
||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
||||
<Icon icon={History} /> Recent Activity
|
||||
</SecondaryNavItem>
|
||||
{:else}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={chatPath}
|
||||
notification={$notifications.has(chatPath)}>
|
||||
<Icon icon={ChatRound} /> Chat
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={goalsPath}
|
||||
notification={$notifications.has(goalsPath)}>
|
||||
<Icon icon="star-fall-minimalistic-2" /> Goals
|
||||
<Icon icon={StarFallMinimalistic} /> Goals
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(THREAD)}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={threadsPath}
|
||||
notification={$notifications.has(threadsPath)}>
|
||||
<Icon icon={NotesMinimalistic} /> Threads
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(EVENT_TIME)}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={calendarPath}
|
||||
notification={$notifications.has(calendarPath)}>
|
||||
<Icon icon={CalendarMinimalistic} /> Calendar
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={threadsPath}
|
||||
notification={$notifications.has(threadsPath)}>
|
||||
<Icon icon="notes-minimalistic" /> Threads
|
||||
</SecondaryNavItem>
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={calendarPath}
|
||||
notification={$notifications.has(calendarPath)}>
|
||||
<Icon icon="calendar-minimalistic" /> Calendar
|
||||
</SecondaryNavItem>
|
||||
{#if hasNip29($relay)}
|
||||
{#if $userRooms.length > 0}
|
||||
<div class="h-2"></div>
|
||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||
{/if}
|
||||
{#each $userRooms as room, i (room)}
|
||||
<MenuSpaceRoomItem {replaceState} notify {url} {room} />
|
||||
{#each $userRooms as h, i (h)}
|
||||
<MenuSpaceRoomItem {replaceState} notify {url} {h} />
|
||||
{/each}
|
||||
{#if $otherRooms.length > 0}
|
||||
<div class="h-2"></div>
|
||||
@@ -170,27 +241,25 @@
|
||||
{/if}
|
||||
</SecondaryNavHeader>
|
||||
{/if}
|
||||
{#each $otherRooms as room, i (room)}
|
||||
<MenuSpaceRoomItem {replaceState} {url} {room} />
|
||||
{#each $otherRooms as h, i (h)}
|
||||
<MenuSpaceRoomItem {replaceState} {url} {h} />
|
||||
{/each}
|
||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
||||
<Icon icon="add-circle" />
|
||||
Create room
|
||||
</SecondaryNavItem>
|
||||
{:else}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={chatPath}
|
||||
notification={$notifications.has(chatPath)}>
|
||||
<Icon icon="chat-round" /> Chat
|
||||
</SecondaryNavItem>
|
||||
{#if $canCreateRoom}
|
||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
||||
<Icon icon={AddCircle} />
|
||||
Create room
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</SecondaryNavSection>
|
||||
<div class="p-4">
|
||||
<button class="btn btn-neutral btn-sm w-full" onclick={manageAlerts}>
|
||||
<Icon icon="bell" />
|
||||
<div class="flex flex-col gap-2 p-4">
|
||||
<Button class="btn btn-neutral btn-sm" onclick={showDetail}>
|
||||
<SocketStatusIndicator {url} />
|
||||
</Button>
|
||||
<Button class="btn btn-neutral btn-sm" onclick={manageAlerts}>
|
||||
<Icon icon={Bell} />
|
||||
Manage Alerts
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
<script lang="ts">
|
||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {pushDrawer} from "@app/util/modal"
|
||||
import {deriveSocketStatus} from "@app/core/state"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const path = makeSpacePath(url)
|
||||
const path = makeSpacePath(url) + ":mobile"
|
||||
|
||||
const status = deriveSocketStatus(url)
|
||||
|
||||
const openMenu = () => pushDrawer(MenuSpace, {url})
|
||||
</script>
|
||||
|
||||
<Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
|
||||
<Icon icon="menu-dots" />
|
||||
{#if $notifications.has(path)}
|
||||
<Icon icon={MenuDots} />
|
||||
{#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>
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
import {deriveChannel} from "@app/core/state"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
room: any
|
||||
h: any
|
||||
notify?: boolean
|
||||
replaceState?: boolean
|
||||
}
|
||||
|
||||
const {url, room, notify = false, replaceState = false}: Props = $props()
|
||||
const {url, h, notify = false, replaceState = false}: Props = $props()
|
||||
|
||||
const path = makeRoomPath(url, room)
|
||||
const channel = deriveChannel(url, room)
|
||||
const path = makeRoomPath(url, h)
|
||||
</script>
|
||||
|
||||
<SecondaryNavItem
|
||||
href={path}
|
||||
{replaceState}
|
||||
notification={notify ? $notifications.has(path) : false}>
|
||||
{#if $channel?.closed || $channel?.private}
|
||||
<Icon icon="lock" size={4} />
|
||||
{:else}
|
||||
<Icon icon="hashtag" />
|
||||
{/if}
|
||||
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||
<ChannelName {url} {room} />
|
||||
</div>
|
||||
<RoomNameWithImage {url} {h} />
|
||||
</SecondaryNavItem>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<script lang="ts">
|
||||
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>
|
||||
{#snippet icon()}
|
||||
<div><Icon icon="login-2" 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,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
@@ -13,9 +13,9 @@
|
||||
</script>
|
||||
|
||||
<Link replaceState href={path}>
|
||||
<CardButton>
|
||||
<CardButton class="btn-neutral shadow-md">
|
||||
{#snippet icon()}
|
||||
<div><SpaceAvatar {url} /></div>
|
||||
<RelayIcon {url} size={12} />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div class="flex gap-1">
|
||||
|
||||
@@ -1,31 +1,54 @@
|
||||
<script lang="ts">
|
||||
import {page} from "$app/stores"
|
||||
import {onMount, mount, unmount, createRawSnippet} from "svelte"
|
||||
import Drawer from "@lib/components/Drawer.svelte"
|
||||
import Dialog from "@lib/components/Dialog.svelte"
|
||||
import {modals, clearModals} from "@app/util/modal"
|
||||
import {modal, clearModals} from "@app/util/modal"
|
||||
|
||||
const onKeyDown = (e: any) => {
|
||||
if (e.code === "Escape" && e.target === document.body) {
|
||||
const closeModals = () => {
|
||||
if ($modal && !$modal.options.noEscape) {
|
||||
clearModals()
|
||||
}
|
||||
}
|
||||
|
||||
const hash = $derived($page.url.hash.slice(1))
|
||||
const modal = $derived($modals[hash])
|
||||
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>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
|
||||
{#if modal?.options?.drawer}
|
||||
<Drawer onClose={clearModals} {...modal.options}>
|
||||
{#key modal.id}
|
||||
<modal.component {...modal.props} />
|
||||
{/key}
|
||||
</Drawer>
|
||||
{:else if modal}
|
||||
<Dialog onClose={clearModals} {...modal.options}>
|
||||
{#key modal.id}
|
||||
<modal.component {...modal.props} />
|
||||
{/key}
|
||||
</Dialog>
|
||||
{/if}
|
||||
<div bind:this={element}></div>
|
||||
|
||||
@@ -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>
|
||||
@@ -8,6 +8,7 @@
|
||||
import {Router} from "@welshman/router"
|
||||
import {userMutes} from "@welshman/app"
|
||||
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 Profile from "@app/components/Profile.svelte"
|
||||
@@ -44,7 +45,7 @@
|
||||
{#if muted}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="row-2 relative">
|
||||
<Icon icon="danger" class="mt-1" />
|
||||
<Icon icon={Danger} class="mt-1" />
|
||||
<p>You have muted this person.</p>
|
||||
</div>
|
||||
<Button class="link ml-8" onclick={ignoreMute}>Show anyway</Button>
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
<script lang="ts">
|
||||
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 CalendarEventDate from "@app/components/CalendarEventDate.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
|
||||
const props: ComponentProps<typeof Content> = $props()
|
||||
</script>
|
||||
|
||||
{#if props.event.kind === EVENT_TIME}
|
||||
<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>
|
||||
<NoteContentEventTime {...props} />
|
||||
{:else if props.event.kind === THREAD}
|
||||
<NoteContentThread {...props} />
|
||||
{:else if props.event.kind === ZAP_GOAL}
|
||||
<NoteContentGoal {...props} />
|
||||
{:else}
|
||||
<Content {...props} />
|
||||
{/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>
|
||||
@@ -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>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
@@ -32,7 +33,7 @@
|
||||
<div class="flex w-full justify-between gap-2">
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right">
|
||||
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
||||
<Icon icon="smile-circle" size={4} />
|
||||
<Icon icon={SmileCircle} size={4} />
|
||||
</EmojiButton>
|
||||
</ReactionSummary>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
@@ -49,7 +51,7 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="user-rounded" />
|
||||
<Icon icon={UserRounded} />
|
||||
<input readonly value={email} class="grow" />
|
||||
</label>
|
||||
{/snippet}
|
||||
@@ -60,7 +62,7 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="key" />
|
||||
<Icon icon={Key} />
|
||||
<input bind:value={password} class="grow" type="password" />
|
||||
</label>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {postJson, sleep} from "@welshman/lib"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
@@ -55,7 +57,7 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="user-rounded" />
|
||||
<Icon icon={UserRounded} />
|
||||
<input bind:value={email} class="grow" />
|
||||
</label>
|
||||
{/snippet}
|
||||
@@ -65,7 +67,7 @@
|
||||
</FieldInline>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Profile from "@app/components/Profile.svelte"
|
||||
@@ -17,18 +18,18 @@
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl">
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||
<div class="flex justify-between">
|
||||
<Profile {pubkey} {url} />
|
||||
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex">
|
||||
<Icon icon="user-circle" />
|
||||
<Icon icon={UserCircle} />
|
||||
View Profile
|
||||
</Button>
|
||||
</div>
|
||||
<ProfileInfo {pubkey} {url} />
|
||||
<ProfileBadges {pubkey} {url} />
|
||||
<Button onclick={openProfile} class="btn btn-primary sm:hidden">
|
||||
<Icon icon="user-circle" />
|
||||
<Icon icon={UserCircle} />
|
||||
View Profile
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,17 +3,23 @@
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
import {splitAt} from "@welshman/lib"
|
||||
import {userProfile} from "@welshman/app"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import {userProfile, shouldUnwrap} from "@welshman/app"
|
||||
import Widget from "@assets/icons/widget.svg?dataurl"
|
||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||
import MenuSpaces from "@app/components/MenuSpaces.svelte"
|
||||
import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte"
|
||||
import MenuSettings from "@app/components/MenuSettings.svelte"
|
||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||
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 {makeSpacePath} from "@app/util/routes"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
@@ -24,15 +30,11 @@
|
||||
|
||||
const {children}: Props = $props()
|
||||
|
||||
const addSpace = () => pushModal(SpaceAdd)
|
||||
|
||||
const showSpacesMenu = () => (spaceUrls.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd))
|
||||
|
||||
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
|
||||
|
||||
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 path = makeSpacePath(url)
|
||||
@@ -45,9 +47,8 @@
|
||||
const itemHeight = 56
|
||||
const navPadding = 6 * itemHeight
|
||||
const itemLimit = $derived((windowHeight - navPadding) / itemHeight)
|
||||
const spaceUrls = $derived(Array.from($userRoomsByUrl.keys()))
|
||||
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, spaceUrls))
|
||||
const anySpaceNotifications = $derived(spaceUrls.some(hasNotification))
|
||||
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, $userSpaceUrls))
|
||||
const anySpaceNotifications = $derived($userSpaceUrls.some(hasNotification))
|
||||
const otherSpaceNotifications = $derived(secondarySpaceUrls.some(hasNotification))
|
||||
</script>
|
||||
|
||||
@@ -55,13 +56,13 @@
|
||||
|
||||
<div
|
||||
class="ml-sai mt-sai mb-sai relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
||||
<div class="flex h-full flex-col justify-between">
|
||||
<div class="flex h-full flex-col" class:justify-between={PLATFORM_RELAYS.length === 0}>
|
||||
<div>
|
||||
{#each PLATFORM_RELAYS as url (url)}
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{:else}
|
||||
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
|
||||
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" />
|
||||
</PrimaryNavItem>
|
||||
<Divider />
|
||||
{#each primarySpaceUrls as url (url)}
|
||||
@@ -73,31 +74,34 @@
|
||||
class="tooltip-right"
|
||||
onclick={showOtherSpacesMenu}
|
||||
notification={otherSpaceNotifications}>
|
||||
<Avatar icon="widget" class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Other Spaces" src={Widget} />
|
||||
</PrimaryNavItem>
|
||||
{/if}
|
||||
<PrimaryNavItem title="Add Space" onclick={addSpace} class="tooltip-right">
|
||||
<Avatar icon="add-square" class="!h-10 !w-10" />
|
||||
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
|
||||
<ImageIcon alt="Add a Space" src={Compass} size={7} />
|
||||
</PrimaryNavItem>
|
||||
{/each}
|
||||
</div>
|
||||
{#if PLATFORM_RELAYS.length > 0}
|
||||
<Divider />
|
||||
{/if}
|
||||
<div>
|
||||
<PrimaryNavItem
|
||||
title="Settings"
|
||||
href="/settings/profile"
|
||||
prefix="/settings"
|
||||
class="tooltip-right">
|
||||
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Settings" src={$userProfile?.picture || UserRounded} class="rounded-full" />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem
|
||||
title="Messages"
|
||||
onclick={openChat}
|
||||
class="tooltip-right"
|
||||
notification={$notifications.has("/chat")}>
|
||||
<Avatar icon="letter" class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Messages" src={Letter} size={7} />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
||||
<Avatar icon="magnifer" class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Search" src={Magnifier} size={7} />
|
||||
</PrimaryNavItem>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,27 +114,28 @@
|
||||
<div
|
||||
class="border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
||||
<div class="content-padding-x content-sizing flex justify-between px-2">
|
||||
<div class="flex gap-2 sm:gap-8">
|
||||
<div class="flex gap-2 sm:gap-6">
|
||||
<PrimaryNavItem title="Home" href="/home">
|
||||
<Avatar icon="home-smile" class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Home" src={HomeSmile} size={7} />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem
|
||||
title="Messages"
|
||||
onclick={openChat}
|
||||
notification={$notifications.has("/chat")}>
|
||||
<Avatar icon="letter" class="!h-10 !w-10" />
|
||||
<ImageIcon alt="Messages" src={Letter} size={7} />
|
||||
</PrimaryNavItem>
|
||||
{#if PLATFORM_RELAYS.length !== 1}
|
||||
<PrimaryNavItem
|
||||
title="Spaces"
|
||||
onclick={showSpacesMenu}
|
||||
notification={anySpaceNotifications}>
|
||||
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
|
||||
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
|
||||
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={7} />
|
||||
</PrimaryNavItem>
|
||||
{/if}
|
||||
</div>
|
||||
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
|
||||
<Avatar icon="settings" src={$userProfile?.picture} class="!h-10 !w-10" />
|
||||
<ImageIcon
|
||||
alt="Settings"
|
||||
src={$userProfile?.picture || Settings}
|
||||
size={7}
|
||||
class="rounded-full" />
|
||||
</PrimaryNavItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||
import {makeSpacePath, goToSpace} from "@app/util/routes"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const path = makeSpacePath(url)
|
||||
const onClick = () => goToSpace(url)
|
||||
</script>
|
||||
|
||||
<PrimaryNavItem
|
||||
onclick={onClick}
|
||||
title={displayRelayUrl(url)}
|
||||
href={path}
|
||||
class="tooltip-right"
|
||||
notification={$notifications.has(path)}>
|
||||
<SpaceAvatar {url} />
|
||||
notification={$notifications.has(makeSpacePath(url))}>
|
||||
<RelayIcon {url} size={10} class="rounded-full" />
|
||||
</PrimaryNavItem>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
<script lang="ts">
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import {displayPubkey} from "@welshman/util"
|
||||
import {
|
||||
deriveHandleForPubkey,
|
||||
displayHandle,
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
} from "@welshman/app"
|
||||
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import WotScore from "@app/components/WotScore.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {clip} from "@app/util/toast"
|
||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||
|
||||
type Props = {
|
||||
pubkey: string
|
||||
@@ -25,8 +21,7 @@
|
||||
|
||||
const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props()
|
||||
|
||||
const relays = removeNil([url])
|
||||
const profile = deriveProfile(pubkey, relays)
|
||||
const relays = removeUndefined([url])
|
||||
const profileDisplay = deriveProfileDisplay(pubkey, relays)
|
||||
const handle = deriveHandleForPubkey(pubkey)
|
||||
|
||||
@@ -37,7 +32,7 @@
|
||||
|
||||
<div class="flex max-w-full items-start gap-3">
|
||||
<Button onclick={openProfile} class="py-1">
|
||||
<Avatar src={$profile?.picture} size={avatarSize} />
|
||||
<ProfileCircle {pubkey} size={avatarSize} />
|
||||
</Button>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -55,7 +50,7 @@
|
||||
<div class="flex items-center gap-1 overflow-hidden text-ellipsis text-xs opacity-60">
|
||||
{displayPubkey(pubkey)}
|
||||
<Button onclick={copyPubkey} class="pt-1">
|
||||
<Icon size={3} icon="copy" />
|
||||
<Icon size={3} icon={Copy} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {deriveEvents} from "@welshman/store"
|
||||
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 Button from "@lib/components/Button.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 {pushModal} from "@app/util/modal"
|
||||
|
||||
@@ -21,8 +25,8 @@
|
||||
const {pubkey, url}: Props = $props()
|
||||
const filters: Filter[] = [{authors: [pubkey], limit: 1}]
|
||||
const events = deriveEvents(repository, {filters})
|
||||
const membership = $derived($membershipsByPubkey.get(pubkey))
|
||||
const relays = $derived(getRelayTags(getListTags(membership)))
|
||||
const selections = deriveGroupSelections(pubkey)
|
||||
const spaceUrls = $derived(getSpaceUrlsFromGroupSelections($selections))
|
||||
|
||||
const viewEvent = () => goToEvent($events[0]!)
|
||||
|
||||
@@ -36,7 +40,7 @@
|
||||
load({
|
||||
filters: [
|
||||
{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(),
|
||||
})
|
||||
@@ -49,10 +53,10 @@
|
||||
Last active {formatTimestampRelative($events[0].created_at)}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if relays.length > 0}
|
||||
{#if spaceUrls.length > 0}
|
||||
<Button onclick={openSpaces} class="badge badge-neutral">
|
||||
{relays.length}
|
||||
{relays.length === 1 ? "space" : "spaces"}
|
||||
{spaceUrls.length}
|
||||
{spaceUrls.length === 1 ? "space" : "spaces"}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
<script lang="ts">
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import cx from "classnames"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
|
||||
type Props = {
|
||||
pubkey: string
|
||||
class?: string
|
||||
size?: number
|
||||
url?: string
|
||||
} & Record<string, any>
|
||||
}
|
||||
|
||||
const {pubkey, url, ...props}: Props = $props()
|
||||
const {pubkey, url, size = 7, ...props}: Props = $props()
|
||||
|
||||
const profile = deriveProfile(pubkey, removeNil([url]))
|
||||
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||
</script>
|
||||
|
||||
<Avatar src={$profile?.picture} icon="user-circle" {...props} />
|
||||
<ImageIcon
|
||||
{size}
|
||||
class={cx(props.class, "rounded-full")}
|
||||
src={$profile?.picture || UserRounded}
|
||||
alt="Profile picture" />
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script lang="ts">
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
|
||||
const {...props} = $props()
|
||||
type Props = {
|
||||
pubkeys: string[]
|
||||
size?: number
|
||||
}
|
||||
|
||||
const {pubkeys, size = 7}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex pr-3">
|
||||
{#each props.pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
|
||||
{#each pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
|
||||
<div class="z-feature -mr-3 inline-block">
|
||||
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {...props} />
|
||||
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
DELETE,
|
||||
isReplaceable,
|
||||
getAddress,
|
||||
getRelaysFromList,
|
||||
} from "@welshman/util"
|
||||
import {pubkey, userRelaySelections, publishThunk, repository} from "@welshman/app"
|
||||
import {pubkey, publishThunk, repository} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -18,7 +19,7 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {logout} from "@app/core/commands"
|
||||
import {INDEXER_RELAYS, PLATFORM_NAME, userMembership, getMembershipUrls} from "@app/core/state"
|
||||
import {INDEXER_RELAYS, PLATFORM_NAME, userSpaceUrls, userWriteRelays} from "@app/core/state"
|
||||
|
||||
let progress: number | undefined = $state(undefined)
|
||||
let confirmText = $state("")
|
||||
@@ -39,11 +40,7 @@
|
||||
const profileEvent = makeEvent(PROFILE, createProfile({name: "[deleted]"}))
|
||||
const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
|
||||
const denominator = chunks.length + 2
|
||||
const relays = uniq([
|
||||
...INDEXER_RELAYS,
|
||||
...getRelaysFromList($userRelaySelections),
|
||||
...getMembershipUrls($userMembership),
|
||||
])
|
||||
const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls])
|
||||
|
||||
let step = 0
|
||||
|
||||
@@ -136,12 +133,12 @@
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-error" disabled={showProgress || !confirmOk}>
|
||||
<Spinner loading={progress !== undefined}>Confirm</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user