Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75905e4652 | |||
| d07b9cde5f | |||
| d8a9cc5a7e | |||
| 863d11352f | |||
| b4cc770cdf | |||
| 901e56a625 | |||
| 479fed34f7 | |||
| 81d7b08aed | |||
| a582b1ea73 | |||
| 1c0b2a09df | |||
| 3a42a1b560 | |||
| db203bf00d | |||
| ffb36af734 | |||
| b399fa8dcc | |||
| 5bba5959f7 | |||
| 2ad65e394e | |||
| 345b20bf5d | |||
| b9fb251b32 | |||
| dd9a9c0df2 | |||
| 115b5f9fbe | |||
| 3ad7dcfeb4 | |||
| 60d107aed2 | |||
| 08d8d45ecb | |||
| c40e8ce1a7 | |||
| 993bf8d2e6 | |||
| c3c65c3970 | |||
| a5b868cd56 | |||
| 8fcc56a408 | |||
| c8dfbc936b | |||
| f1e76a1ed1 | |||
| 6ecc3e6770 | |||
| b05c408977 | |||
| e484c3cb00 | |||
| 69d0e11ba4 |
@@ -3,4 +3,6 @@
|
|||||||
--ignore-dir=build
|
--ignore-dir=build
|
||||||
--ignore-dir=ios/DerivedData
|
--ignore-dir=ios/DerivedData
|
||||||
--ignore-dir=ios/App/App/public
|
--ignore-dir=ios/App/App/public
|
||||||
|
--ignore-file=match:.svg
|
||||||
|
--ignore-file=match:package-lock.json
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||||
VITE_BURROW_URL=
|
VITE_BURROW_URL=
|
||||||
VITE_PLATFORM_URL=https://flotilla.social
|
VITE_PLATFORM_URL=https://flotilla.social
|
||||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ out/
|
|||||||
.gradle/
|
.gradle/
|
||||||
local.properties
|
local.properties
|
||||||
proguard/
|
proguard/
|
||||||
|
google-services.json
|
||||||
|
GoogleService-Info.plist
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@@ -1,5 +1,34 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.0.0
|
||||||
|
|
||||||
|
* Add alerts via Anchor
|
||||||
|
|
||||||
|
# 0.2.12
|
||||||
|
|
||||||
|
* Fix keyboard covering chat input
|
||||||
|
* Fix thread replies
|
||||||
|
* Make error reporting and analytics optional
|
||||||
|
* Replace long press with tap target
|
||||||
|
* Fix time input
|
||||||
|
* Fix nevent hints for url-specific stuff
|
||||||
|
* Fix confirm and reactions on mobile
|
||||||
|
* Add reply to chat on mobile
|
||||||
|
* Fix profile suggestions
|
||||||
|
|
||||||
|
# 0.2.11
|
||||||
|
|
||||||
|
* Add in-app signup flow on ios
|
||||||
|
* Add profile deletion
|
||||||
|
|
||||||
|
# 0.2.10
|
||||||
|
|
||||||
|
* Improve space discovery
|
||||||
|
|
||||||
|
# 0.2.9
|
||||||
|
|
||||||
|
* Add NIP 01 signup flow on mobile
|
||||||
|
|
||||||
# 0.2.8
|
# 0.2.8
|
||||||
|
|
||||||
* Show spinner when joining a room
|
* Show spinner when joining a room
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 9
|
versionCode 13
|
||||||
versionName "0.2.7"
|
versionName "0.2.13"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ android {
|
|||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
|
implementation project(':capacitor-keyboard')
|
||||||
implementation project(':nostr-signer-capacitor-plugin')
|
implementation project(':nostr-signer-capacitor-plugin')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/title_activity_main"
|
android:label="@string/title_activity_main"
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|||||||
@@ -5,5 +5,8 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
|||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
|
include ':capacitor-keyboard'
|
||||||
|
project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
|
||||||
|
|
||||||
include ':nostr-signer-capacitor-plugin'
|
include ':nostr-signer-capacitor-plugin'
|
||||||
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/nostr-signer-capacitor-plugin/android')
|
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/nostr-signer-capacitor-plugin/android')
|
||||||
|
|||||||
+5
-1
@@ -10,7 +10,11 @@ const config: CapacitorConfig = {
|
|||||||
plugins: {
|
plugins: {
|
||||||
SplashScreen: {
|
SplashScreen: {
|
||||||
androidSplashResourceName: "splash"
|
androidSplashResourceName: "splash"
|
||||||
}
|
},
|
||||||
|
Keyboard: {
|
||||||
|
style: "DARK",
|
||||||
|
resizeOnFullScreen: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
||||||
// server: {
|
// server: {
|
||||||
|
|||||||
@@ -351,14 +351,14 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.2.8;
|
MARKETING_VERSION = 0.2.13;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -376,14 +376,14 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.2.8;
|
MARKETING_VERSION = 0.2.13;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ def capacitor_pods
|
|||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||||
|
pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
|
||||||
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/nostr-signer-capacitor-plugin'
|
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/nostr-signer-capacitor-plugin'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Generated
+81
-85
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.8",
|
"version": "0.2.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.8",
|
"version": "0.2.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^7.0.0",
|
"@capacitor/android": "^7.0.0",
|
||||||
"@capacitor/app": "^7.0.0",
|
"@capacitor/app": "^7.0.0",
|
||||||
"@capacitor/cli": "^7.0.0",
|
"@capacitor/cli": "^7.0.0",
|
||||||
"@capacitor/core": "^7.0.1",
|
"@capacitor/core": "^7.0.1",
|
||||||
"@capacitor/ios": "^7.0.0",
|
"@capacitor/ios": "^7.0.0",
|
||||||
|
"@capacitor/keyboard": "^7.0.0",
|
||||||
"@noble/curves": "^1.5.0",
|
"@noble/curves": "^1.5.0",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
@@ -21,16 +22,16 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
"@welshman/app": "~0.0.42",
|
"@welshman/app": "^0.0.43",
|
||||||
"@welshman/content": "~0.0.18",
|
"@welshman/content": "^0.1.0",
|
||||||
"@welshman/dvm": "~0.0.14",
|
"@welshman/dvm": "^0.0.15",
|
||||||
"@welshman/editor": "~0.0.15",
|
"@welshman/editor": "^0.1.0",
|
||||||
"@welshman/feeds": "~0.0.30",
|
"@welshman/feeds": "^0.1.0",
|
||||||
"@welshman/lib": "~0.0.41",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/net": "~0.0.47",
|
"@welshman/net": "^0.0.49",
|
||||||
"@welshman/signer": "~0.0.20",
|
"@welshman/signer": "^0.1.0",
|
||||||
"@welshman/store": "~0.0.16",
|
"@welshman/store": "^0.1.0",
|
||||||
"@welshman/util": "~0.0.61",
|
"@welshman/util": "^0.1.0",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"date-picker-svelte": "^2.13.0",
|
"date-picker-svelte": "^2.13.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
@@ -2090,6 +2091,15 @@
|
|||||||
"@capacitor/core": "^7.0.0"
|
"@capacitor/core": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@capacitor/keyboard": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tqwy8wG+sx4UqiFCX4Q+bFw6uKgG7BiHKAPpeefoIgoEB8H8Jf3xZNZoVPnJIMuPsCdSvuyHXZbJXH9IEEirGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -4448,9 +4458,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.14",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
|
||||||
"integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==",
|
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -4712,19 +4722,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/app": {
|
"node_modules/@welshman/app": {
|
||||||
"version": "0.0.42",
|
"version": "0.0.43",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/app/-/app-0.0.42.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/app/-/app-0.0.43.tgz",
|
||||||
"integrity": "sha512-+yV2VZ1r/BVhaLfZyyXe6RNMAbj1Z5jFlvy9K14Z3oTRix9ugvFgs78nLgXbt/lyLvafOit0c3RoGyQcixEb3Q==",
|
"integrity": "sha512-cW/0h48d18m8eyajfE+ZNUEKyehR2NY5w4jWZjHwEHjUlj6izTTIYLhcLxAGZoge/raEpWhvKkI2CeiX+3PheA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@welshman/dvm": "~0.0.13",
|
"@welshman/dvm": "^0.0.15",
|
||||||
"@welshman/feeds": "~0.0.30",
|
"@welshman/feeds": "^0.1.0",
|
||||||
"@welshman/lib": "~0.0.39",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/net": "~0.0.46",
|
"@welshman/net": "^0.0.49",
|
||||||
"@welshman/signer": "~0.0.19",
|
"@welshman/signer": "^0.1.0",
|
||||||
"@welshman/store": "~0.0.15",
|
"@welshman/store": "^0.1.0",
|
||||||
"@welshman/util": "~0.0.59",
|
"@welshman/util": "^0.1.0",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^4.2.18",
|
||||||
@@ -4766,13 +4776,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/content": {
|
"node_modules/@welshman/content": {
|
||||||
"version": "0.0.18",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.1.0.tgz",
|
||||||
"integrity": "sha512-7LHs9xKStrkaet9VY1PWSEUWrdIaIThIo+ByN6lF3nRZwPTExrBy4rPXnEa5roVAAwgmlhXw3zTkfGP15V6joQ==",
|
"integrity": "sha512-l+r3JgBf6raPcwsAsNiM3N4Ms0X88uKPMuPltQLOMv0whaDCUVpu/w7llQBX6fH7v9RgSq0imgkUCWw9puYNlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^7.0.2",
|
"@braintree/sanitize-url": "^7.0.2",
|
||||||
"@welshman/lib": "~0.0.40",
|
"@welshman/lib": "^0.1.0",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4780,22 +4790,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/dvm": {
|
"node_modules/@welshman/dvm": {
|
||||||
"version": "0.0.14",
|
"version": "0.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/dvm/-/dvm-0.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/dvm/-/dvm-0.0.15.tgz",
|
||||||
"integrity": "sha512-C7nJ3Z3QQv5ZRVxH57rqM/z7m9ljDAaAPCjhZdnO/MXzkGdy6AfczSiXK8IXTe9q4dYyEJ7kADo7UVfwES/t5Q==",
|
"integrity": "sha512-XYdQBsbMIYX0ympQdq3KiacnoDYqXhQ4m+7zVROOO4rF9swht7az46T12Sga046eLUbFTK2f45qFAPEwcmhDHg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/net": "~0.0.46",
|
"@welshman/net": "^0.0.49",
|
||||||
"@welshman/util": "~0.0.58",
|
"@welshman/util": "^0.1.0",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/editor": {
|
"node_modules/@welshman/editor": {
|
||||||
"version": "0.0.15",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.1.0.tgz",
|
||||||
"integrity": "sha512-Eg3alzv+cjCXtr6oEItRqoRSD4DTllt3c2JyJTxpF/KNiy8XHHMeUSpVFgph3+pAt5jwyl6b1feKPEwpShgqHw==",
|
"integrity": "sha512-gqbkjWhyb37tbDwd1gYPc/KWd+kpF6V0YQi7apoBIPGqiprYuzzrkhUe27DWgWDvCik4GB3Ef2Gy9vmQDRXpcg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/core": "^2.11.5",
|
"@tiptap/core": "^2.11.5",
|
||||||
@@ -4811,41 +4821,27 @@
|
|||||||
"@tiptap/extension-text": "^2.11.5",
|
"@tiptap/extension-text": "^2.11.5",
|
||||||
"@tiptap/pm": "^2.11.5",
|
"@tiptap/pm": "^2.11.5",
|
||||||
"@tiptap/suggestion": "^2.11.5",
|
"@tiptap/suggestion": "^2.11.5",
|
||||||
"@welshman/lib": "~0.0.40",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/util": "^0.0.60",
|
"@welshman/util": "^0.1.0",
|
||||||
"nostr-editor": "^0.0.4-pre.13",
|
"nostr-editor": "^0.0.4-pre.13",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.10.4",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/editor/node_modules/@welshman/util": {
|
|
||||||
"version": "0.0.60",
|
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.60.tgz",
|
|
||||||
"integrity": "sha512-kqZgYnrwxKx0JTDZnTSaQYc2ev7E9ZjNDy5MclX36d5T/qPUspmwksAOodFJY9kJoJd49bf1omAmBTgnFJfeHw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/ws": "^8.5.13",
|
|
||||||
"@welshman/lib": "~0.0.37",
|
|
||||||
"nostr-tools": "^2.7.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@welshman/feeds": {
|
"node_modules/@welshman/feeds": {
|
||||||
"version": "0.0.30",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.0.30.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.1.0.tgz",
|
||||||
"integrity": "sha512-Zcex2uJVeYM55zDI1Dhb5I41lYGD4BURWl95nbFaWbbMYDwoAFIS2cPXBsaGNrITzsz8qByvRs2RnplrmZwSzA==",
|
"integrity": "sha512-89N1Ibcyzbs7VFljSWe/lpg/hLPq2/EKk5xegiZ7Sn8+X0Mqn7+8V3HUBEbPk+ZICLSIwEsksrFkWgFyeKLoSQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/util": "~0.0.54"
|
"@welshman/util": "^0.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/lib": {
|
"node_modules/@welshman/lib": {
|
||||||
"version": "0.0.41",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.41.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.1.0.tgz",
|
||||||
"integrity": "sha512-FMJVoPZw8Vi1fd2/ulwqlBS1tvjkFAm9lg+Dz5SXItXxrNC06YMRTjGjInCBEkArrvNGPUjchzSFDNmbH0fxHQ==",
|
"integrity": "sha512-U3hKLigTOP62/jkJbQboZ4P1wSZae16xdFVC3CXT9lk3aRkI7g6pAfzCnungCbeX26X/HgRUs/IqRRMI+tz/Pg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/base": "^1.1.6",
|
"@scure/base": "^1.1.6",
|
||||||
@@ -4854,28 +4850,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/net": {
|
"node_modules/@welshman/net": {
|
||||||
"version": "0.0.47",
|
"version": "0.0.49",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.47.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.49.tgz",
|
||||||
"integrity": "sha512-/mIr+QyLH+RlD16rsPDTIW250lOm5eNaLO6dhZw8dMKznMhVtSWe/X/lJZOXmexzbB2z7WYZVN5x5TggZROyxA==",
|
"integrity": "sha512-DvsBh+MGIZtRd08itpxM8H40tNB2CuEx1ayydnIjk/bX9J2SgfjoFcMhr0mJ8PdSYvwmfRV95T4gF19ni8ANeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.40",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/util": "~0.0.59",
|
"@welshman/util": "^0.1.0",
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/signer": {
|
"node_modules/@welshman/signer": {
|
||||||
"version": "0.0.20",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/signer/-/signer-0.0.20.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/signer/-/signer-0.1.0.tgz",
|
||||||
"integrity": "sha512-t7ulAMtx+b1NedTzs91D/2WTdM0OzTQHsv15qtXQoIyqrdhJ7QQsMHkDdFUkyPK8CBxAWO8kVorbDM8DbVyeCQ==",
|
"integrity": "sha512-9pLpYsAcriBqQROiJlnnncyCHcneDS2iD+gA5SXv1lHxKRMm3yePx5vjlUj1vJ4+JMvNiutQeTnqOc+gJ31MoA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/curves": "^1.7.0",
|
"@noble/curves": "^1.7.0",
|
||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/net": "~0.0.46",
|
"@welshman/net": "^0.0.49",
|
||||||
"@welshman/util": "~0.0.58",
|
"@welshman/util": "^0.1.0",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4886,13 +4882,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/store": {
|
"node_modules/@welshman/store": {
|
||||||
"version": "0.0.16",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.1.0.tgz",
|
||||||
"integrity": "sha512-hIcvcBnmE2jbEyl44iw2qRVPq66CQsVevCMK8NlKHN5LrRydbZQDrU1v/1uWV43FoGr2jjIawUiJUsm8+GlP+Q==",
|
"integrity": "sha512-i0AD8Y4OuuxdQvxmKgziNbiAZ+WDjheY5Z/DGfBTKPWrITOqA5vmWY601WYUJe0HgleXcgp0dpjFXRdX7bVIvQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/util": "~0.0.59",
|
"@welshman/util": "^0.1.0",
|
||||||
"svelte": "^4.2.18"
|
"svelte": "^4.2.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4931,13 +4927,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/util": {
|
"node_modules/@welshman/util": {
|
||||||
"version": "0.0.61",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.61.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.1.0.tgz",
|
||||||
"integrity": "sha512-+l4YX01msAtnyylzpIFIAYubvnBLyr9hGx3iRO5LS3OPv/yUDOeyYJseWDqorkIiN5BRT7PCgnWJdlQP71ZtAw==",
|
"integrity": "sha512-acXG0+HoFPT/zDwFO0zBWt9TrCHD0IYNJNN67CNwmv9yicVskDsFs8L65uoSEDgCOjp9eQnIMXf20s8a/XO3Ng==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/ws": "^8.5.13",
|
"@types/ws": "^8.5.13",
|
||||||
"@welshman/lib": "~0.0.40",
|
"@welshman/lib": "^0.1.0",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -15240,9 +15236,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
|
||||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
+12
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.8",
|
"version": "0.2.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"@capacitor/cli": "^7.0.0",
|
"@capacitor/cli": "^7.0.0",
|
||||||
"@capacitor/core": "^7.0.1",
|
"@capacitor/core": "^7.0.1",
|
||||||
"@capacitor/ios": "^7.0.0",
|
"@capacitor/ios": "^7.0.0",
|
||||||
|
"@capacitor/keyboard": "^7.0.0",
|
||||||
"@noble/curves": "^1.5.0",
|
"@noble/curves": "^1.5.0",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
@@ -50,16 +51,16 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
"@welshman/app": "~0.0.42",
|
"@welshman/app": "^0.0.43",
|
||||||
"@welshman/content": "~0.0.18",
|
"@welshman/content": "^0.1.0",
|
||||||
"@welshman/dvm": "~0.0.14",
|
"@welshman/dvm": "^0.0.15",
|
||||||
"@welshman/editor": "~0.0.15",
|
"@welshman/editor": "^0.1.0",
|
||||||
"@welshman/feeds": "~0.0.30",
|
"@welshman/feeds": "^0.1.0",
|
||||||
"@welshman/lib": "~0.0.41",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/net": "~0.0.47",
|
"@welshman/net": "^0.0.49",
|
||||||
"@welshman/signer": "~0.0.20",
|
"@welshman/signer": "^0.1.0",
|
||||||
"@welshman/store": "~0.0.16",
|
"@welshman/store": "^0.1.0",
|
||||||
"@welshman/util": "~0.0.61",
|
"@welshman/util": "^0.1.0",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"date-picker-svelte": "^2.13.0",
|
"date-picker-svelte": "^2.13.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
|||||||
+32
@@ -1,3 +1,5 @@
|
|||||||
|
@import "@welshman/editor/index.css";
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@@ -323,3 +325,33 @@ emoji-picker {
|
|||||||
--input-font-color: var(--base-content);
|
--input-font-color: var(--base-content);
|
||||||
--outline-color: var(--base-100);
|
--outline-color: var(--base-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* progress */
|
||||||
|
|
||||||
|
progress[value]::-webkit-progress-value {
|
||||||
|
transition: width 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* content width for fixed elements */
|
||||||
|
|
||||||
|
.cw {
|
||||||
|
@apply w-full md:w-[calc(100%-18.5rem)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* chat view */
|
||||||
|
|
||||||
|
.chat__page-bar {
|
||||||
|
@apply sait cw !fixed top-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__messages {
|
||||||
|
@apply saib cw fixed top-12 flex h-[calc(100%-6rem)] flex-col-reverse overflow-y-auto overflow-x-hidden md:h-[calc(100%-2.5rem)];
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__compose {
|
||||||
|
@apply saib cw fixed bottom-14 md:bottom-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__scroll-down {
|
||||||
|
@apply saib fixed bottom-28 right-4 md:bottom-16;
|
||||||
|
}
|
||||||
|
|||||||
+3
-1
@@ -2,7 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||||
<meta name="theme-color" content="{ACCENT}" />
|
<meta name="theme-color" content="{ACCENT}" />
|
||||||
<meta name="description" content="{DESCRIPTION}" />
|
<meta name="description" content="{DESCRIPTION}" />
|
||||||
<meta name="og:url" content="{URL}" />
|
<meta name="og:url" content="{URL}" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint prefer-rest-params: 0 */
|
/* eslint prefer-rest-params: 0 */
|
||||||
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
|
import {getSetting} from "@app/state"
|
||||||
|
|
||||||
const w = window as any
|
const w = window as any
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ w.plausible =
|
|||||||
|
|
||||||
export const setupAnalytics = () => {
|
export const setupAnalytics = () => {
|
||||||
page.subscribe($page => {
|
page.subscribe($page => {
|
||||||
if ($page.route) {
|
if ($page.route && getSetting("report_usage")) {
|
||||||
w.plausible("pageview", {u: $page.route.id})
|
w.plausible("pageview", {u: $page.route.id})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+40
-52
@@ -1,6 +1,6 @@
|
|||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
import {ctx, randomId, uniq, equals} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
DELETE,
|
DELETE,
|
||||||
REPORT,
|
REPORT,
|
||||||
@@ -26,12 +26,11 @@ import {
|
|||||||
getTag,
|
getTag,
|
||||||
getListTags,
|
getListTags,
|
||||||
getRelayTags,
|
getRelayTags,
|
||||||
isShareableRelayUrl,
|
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
toNostrURI,
|
toNostrURI,
|
||||||
|
unionFilters,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, EventContent, EventTemplate, List} from "@welshman/util"
|
import type {TrustedEvent, Filter, EventContent, EventTemplate} from "@welshman/util"
|
||||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
|
||||||
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
||||||
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
||||||
import {
|
import {
|
||||||
@@ -40,13 +39,9 @@ import {
|
|||||||
repository,
|
repository,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
publishThunks,
|
publishThunks,
|
||||||
loadProfile,
|
|
||||||
loadInboxRelaySelections,
|
|
||||||
profilesByPubkey,
|
profilesByPubkey,
|
||||||
relaySelectionsByPubkey,
|
relaySelectionsByPubkey,
|
||||||
getWriteRelayUrls,
|
getWriteRelayUrls,
|
||||||
loadFollows,
|
|
||||||
loadMutes,
|
|
||||||
tagEvent,
|
tagEvent,
|
||||||
tagEventForReaction,
|
tagEventForReaction,
|
||||||
getRelayUrls,
|
getRelayUrls,
|
||||||
@@ -67,11 +62,12 @@ import {
|
|||||||
userMembership,
|
userMembership,
|
||||||
INDEXER_RELAYS,
|
INDEXER_RELAYS,
|
||||||
NIP46_PERMS,
|
NIP46_PERMS,
|
||||||
loadMembership,
|
ALERT,
|
||||||
loadSettings,
|
NOTIFIER_PUBKEY,
|
||||||
getDefaultPubkeys,
|
NOTIFIER_RELAY,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
|
import {loadUserData} from "@app/requests"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -161,47 +157,6 @@ export const logout = async () => {
|
|||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loaders
|
|
||||||
|
|
||||||
export const loadUserData = (
|
|
||||||
pubkey: string,
|
|
||||||
request: Partial<SubscribeRequestWithHandlers> = {},
|
|
||||||
) => {
|
|
||||||
const promise = Promise.race([
|
|
||||||
sleep(3000),
|
|
||||||
Promise.all([
|
|
||||||
loadInboxRelaySelections(pubkey, request),
|
|
||||||
loadMembership(pubkey, request),
|
|
||||||
loadSettings(pubkey, request),
|
|
||||||
loadProfile(pubkey, request),
|
|
||||||
loadFollows(pubkey, request),
|
|
||||||
loadMutes(pubkey, request),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
|
|
||||||
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
|
|
||||||
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
|
|
||||||
promise.then(async () => {
|
|
||||||
for (const pubkeys of chunk(50, getDefaultPubkeys())) {
|
|
||||||
const relays = sample(1, INDEXER_RELAYS)
|
|
||||||
|
|
||||||
await sleep(1000)
|
|
||||||
|
|
||||||
for (const pubkey of pubkeys) {
|
|
||||||
loadMembership(pubkey, {relays})
|
|
||||||
loadProfile(pubkey, {relays})
|
|
||||||
loadFollows(pubkey, {relays})
|
|
||||||
loadMutes(pubkey, {relays})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
export const discoverRelays = (lists: List[]) =>
|
|
||||||
Promise.all(uniq(lists.flatMap(getRelayUrls)).filter(isShareableRelayUrl).map(loadRelay))
|
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
|
|
||||||
export const broadcastUserData = async (relays: string[]) => {
|
export const broadcastUserData = async (relays: string[]) => {
|
||||||
@@ -376,6 +331,7 @@ export const checkRelayConnection = async (url: string) => {
|
|||||||
const connection = ctx.net.pool.get(url)
|
const connection = ctx.net.pool.get(url)
|
||||||
|
|
||||||
await connection.socket.open()
|
await connection.socket.open()
|
||||||
|
await connection.socket.wait(3000)
|
||||||
|
|
||||||
if (connection.socket.status !== SocketStatus.Open) {
|
if (connection.socket.status !== SocketStatus.Open) {
|
||||||
return `Failed to connect`
|
return `Failed to connect`
|
||||||
@@ -504,3 +460,35 @@ export const makeComment = ({event, content, tags = []}: CommentParams) =>
|
|||||||
|
|
||||||
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||||
publishThunk({event: makeComment(params), relays})
|
publishThunk({event: makeComment(params), relays})
|
||||||
|
|
||||||
|
export type AlertParams = {
|
||||||
|
cron: string
|
||||||
|
email: string
|
||||||
|
relay: string
|
||||||
|
handler: string
|
||||||
|
filters: Filter[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeAlert = async ({cron, email, handler, relay, filters}: AlertParams) =>
|
||||||
|
createEvent(ALERT, {
|
||||||
|
content: await signer
|
||||||
|
.get()
|
||||||
|
.nip44.encrypt(
|
||||||
|
NOTIFIER_PUBKEY,
|
||||||
|
JSON.stringify([
|
||||||
|
["cron", cron],
|
||||||
|
["email", email],
|
||||||
|
["relay", relay],
|
||||||
|
["handler", handler],
|
||||||
|
["channel", "email"],
|
||||||
|
...unionFilters(filters).map(filter => ["filter", JSON.stringify(filter)]),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
tags: [
|
||||||
|
["d", randomId()],
|
||||||
|
["p", NOTIFIER_PUBKEY],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const publishAlert = async (params: AlertParams) =>
|
||||||
|
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import {randomInt} from "@welshman/lib"
|
||||||
|
import {displayRelayUrl, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
||||||
|
import type {Filter} from "@welshman/util"
|
||||||
|
import {pubkey} from "@welshman/app"
|
||||||
|
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 {getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state"
|
||||||
|
import {loadAlertStatuses} from "@app/requests"
|
||||||
|
import {publishAlert} from "@app/commands"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
const handler = Capacitor.isNativePlatform()
|
||||||
|
? "https://app.flotilla.social"
|
||||||
|
: window.location.origin
|
||||||
|
|
||||||
|
const timezone = new Date()
|
||||||
|
.toString()
|
||||||
|
.match(/GMT[^\s]+/)![0]
|
||||||
|
.slice(3)
|
||||||
|
const timezoneOffset = parseInt(timezone) / 100
|
||||||
|
const minute = randomInt(0, 59)
|
||||||
|
const hour = (17 - timezoneOffset) % 24
|
||||||
|
const WEEKLY = `0 ${minute} ${hour} * * 1`
|
||||||
|
const DAILY = `0 ${minute} ${hour} * * *`
|
||||||
|
|
||||||
|
let loading = false
|
||||||
|
let cron = WEEKLY
|
||||||
|
let email = ""
|
||||||
|
let relay = ""
|
||||||
|
let notifyThreads = true
|
||||||
|
let notifyCalendar = true
|
||||||
|
let notifyChat = false
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!email.includes("@")) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide an email address",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relay) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please select a space",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notifyThreads && !notifyCalendar && !notifyChat) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please select something to be notified about",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters: Filter[] = []
|
||||||
|
|
||||||
|
if (notifyThreads) {
|
||||||
|
filters.push({kinds: [THREAD]})
|
||||||
|
filters.push({kinds: [COMMENT], "#k": [String(THREAD)]})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyCalendar) {
|
||||||
|
filters.push({kinds: [EVENT_TIME]})
|
||||||
|
filters.push({kinds: [COMMENT], "#k": [String(EVENT_TIME)]})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyChat) {
|
||||||
|
filters.push({kinds: [MESSAGE], "#h": getMembershipRoomsByUrl(relay, $userMembership)})
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await publishAlert({cron, email, relay, handler, filters})
|
||||||
|
await loadAlertStatuses($pubkey!)
|
||||||
|
|
||||||
|
pushToast({message: "Your alert has been successfully created!"})
|
||||||
|
back()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
Add an Alert
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Email Address*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<input placeholder="email@example.com" bind:value={email} />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Frequency*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select bind:value={cron} class="select select-bordered">
|
||||||
|
<option value={WEEKLY}>Weekly</option>
|
||||||
|
<option value={DAILY}>Daily</option>
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Space*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select bind:value={relay} class="select select-bordered">
|
||||||
|
<option value="" disabled selected>Choose a space URL</option>
|
||||||
|
{#each getMembershipUrls($userMembership) as url (url)}
|
||||||
|
<option value={url}>{displayRelayUrl(url)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Notifications*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex items-center justify-end gap-4">
|
||||||
|
<span class="flex gap-3">
|
||||||
|
<input type="checkbox" class="checkbox" bind:checked={notifyThreads} />
|
||||||
|
Threads
|
||||||
|
</span>
|
||||||
|
<span class="flex gap-3">
|
||||||
|
<input type="checkbox" class="checkbox" bind:checked={notifyCalendar} />
|
||||||
|
Calendar
|
||||||
|
</span>
|
||||||
|
<span class="flex gap-3">
|
||||||
|
<input type="checkbox" class="checkbox" bind:checked={notifyChat} />
|
||||||
|
Chat
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Confirm</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import type {Alert} from "@app/state"
|
||||||
|
import {NOTIFIER_RELAY} from "@app/state"
|
||||||
|
import {publishDelete} from "@app/commands"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
alert: Alert
|
||||||
|
}
|
||||||
|
|
||||||
|
const {alert}: Props = $props()
|
||||||
|
|
||||||
|
const confirm = () => {
|
||||||
|
publishDelete({event: alert.event, relays: [NOTIFIER_RELAY]})
|
||||||
|
pushToast({message: "Your alert has been deleted!"})
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Confirm {confirm} message="You'll no longer receive messages for this alert." />
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {parseJson, nthEq} from "@welshman/lib"
|
||||||
|
import {
|
||||||
|
getAddress,
|
||||||
|
getTagValue,
|
||||||
|
getTagValues,
|
||||||
|
displayRelayUrl,
|
||||||
|
EVENT_TIME,
|
||||||
|
MESSAGE,
|
||||||
|
THREAD,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import {displayList} from "@lib/util"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import AlertDelete from "@app/components/AlertDelete.svelte"
|
||||||
|
import type {Alert} from "@app/state"
|
||||||
|
import {alertStatuses} from "@app/state"
|
||||||
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
alert: Alert
|
||||||
|
}
|
||||||
|
|
||||||
|
const {alert}: Props = $props()
|
||||||
|
|
||||||
|
const address = $derived(getAddress(alert.event))
|
||||||
|
const status = $derived($alertStatuses.find(s => s.event.tags.some(nthEq(1, address))))
|
||||||
|
const cron = $derived(getTagValue("cron", alert.tags))
|
||||||
|
const channel = $derived(getTagValue("channel", alert.tags))
|
||||||
|
const relay = $derived(getTagValue("relay", alert.tags)!)
|
||||||
|
const filters = $derived(getTagValues("filter", alert.tags).map(parseJson))
|
||||||
|
const types = $derived.by(() => {
|
||||||
|
const t: string[] = []
|
||||||
|
|
||||||
|
if (filters.some(f => f.kinds?.includes(THREAD))) t.push("threads")
|
||||||
|
if (filters.some(f => f.kinds?.includes(EVENT_TIME))) t.push("calendar events")
|
||||||
|
if (filters.some(f => f.kinds?.includes(MESSAGE))) t.push("chat")
|
||||||
|
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
|
||||||
|
const startDelete = () => pushModal(AlertDelete, {alert})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<Button class="py-1" onclick={startDelete}>
|
||||||
|
<Icon icon="trash-bin-2" />
|
||||||
|
</Button>
|
||||||
|
<div class="flex-inline gap-1">
|
||||||
|
{cron?.endsWith("1") ? "Weekly" : "Daily"} alert for
|
||||||
|
{displayList(types)} on
|
||||||
|
<Link class="link" href={makeSpacePath(relay)}>
|
||||||
|
{displayRelayUrl(relay)}
|
||||||
|
</Link>, sent via {channel}.
|
||||||
|
</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}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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/modal"
|
||||||
|
import {alerts} from "@app/state"
|
||||||
|
|
||||||
|
const startAlert = () => pushModal(AlertAdd)
|
||||||
|
</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>
|
||||||
|
<div class="col-4">
|
||||||
|
{#each $alerts as alert (alert.event.id)}
|
||||||
|
<AlertItem {alert} />
|
||||||
|
{:else}
|
||||||
|
<p class="text-center opacity-75 py-12">No alerts found</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
formatTimestampAsTime,
|
formatTimestampAsTime,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
|
||||||
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
|
const onTap = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
@@ -60,9 +60,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LongPress
|
<TapTarget
|
||||||
data-event={event.id}
|
data-event={event.id}
|
||||||
onLongPress={inert ? null : onLongPress}
|
onTap={inert ? null : onTap}
|
||||||
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left">
|
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left">
|
||||||
<div class="flex w-full gap-3 overflow-auto">
|
<div class="flex w-full gap-3 overflow-auto">
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
@@ -99,15 +99,17 @@
|
|||||||
<div class="row-2 ml-10 mt-1">
|
<div class="row-2 ml-10 mt-1">
|
||||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
||||||
</div>
|
</div>
|
||||||
<button
|
{#if !isMobile}
|
||||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
<button
|
||||||
class:group-hover:opacity-100={!isMobile}>
|
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||||
<ChannelMessageEmojiButton {url} {room} {event} />
|
class:group-hover:opacity-100={!isMobile}>
|
||||||
{#if replyTo}
|
<ChannelMessageEmojiButton {url} {room} {event} />
|
||||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
{#if replyTo}
|
||||||
<Icon icon="reply" size={4} />
|
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||||
</Button>
|
<Icon icon="reply" size={4} />
|
||||||
{/if}
|
</Button>
|
||||||
<ChannelMessageMenuButton {url} {event} />
|
{/if}
|
||||||
</button>
|
<ChannelMessageMenuButton {url} {event} />
|
||||||
</LongPress>
|
</button>
|
||||||
|
{/if}
|
||||||
|
</TapTarget>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import EventReport from "@app/components/EventReport.svelte"
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const {url, event, onClick} = $props()
|
const {url, event, onClick} = $props()
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {url, event})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDelete = () => {
|
const showDelete = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(ConfirmDelete, {url, event})
|
pushModal(EventDeleteConfirm, {url, event})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {publishReaction} from "@app/commands"
|
import {publishReaction} from "@app/commands"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const {url, event, reply} = $props()
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
reply: () => void
|
||||||
|
}
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) => {
|
const {url, event, reply}: Props = $props()
|
||||||
|
|
||||||
|
const onEmoji = ((event: TrustedEvent, url: string, emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
publishReaction({event, relays: [url], content: emoji.unicode})
|
publishReaction({event, relays: [url], content: emoji.unicode})
|
||||||
}
|
}).bind(undefined, event, url)
|
||||||
|
|
||||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||||
|
|
||||||
@@ -23,9 +30,9 @@
|
|||||||
reply()
|
reply()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
|
const showInfo = () => pushModal(EventInfo, {url, event}, {replaceState: true})
|
||||||
|
|
||||||
const showDelete = () => pushModal(ConfirmDelete, {url, event})
|
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
|
|||||||
+117
-100
@@ -42,8 +42,6 @@
|
|||||||
const others = remove($pubkey!, pubkeys)
|
const others = remove($pubkey!, pubkeys)
|
||||||
const missingInboxes = $derived(pubkeys.filter(pk => !$inboxRelaySelectionsByPubkey.has(pk)))
|
const missingInboxes = $derived(pubkeys.filter(pk => !$inboxRelaySelectionsByPubkey.has(pk)))
|
||||||
|
|
||||||
const assertEvent = (e: any) => e as TrustedEvent
|
|
||||||
|
|
||||||
const showMembers = () =>
|
const showMembers = () =>
|
||||||
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
||||||
|
|
||||||
@@ -72,6 +70,8 @@
|
|||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let compose: ChatCompose | undefined = $state()
|
let compose: ChatCompose | undefined = $state()
|
||||||
let parent: TrustedEvent | undefined = $state()
|
let parent: TrustedEvent | undefined = $state()
|
||||||
|
let chatCompose: HTMLElement | undefined = $state()
|
||||||
|
let dynamicPadding: HTMLElement | undefined = $state()
|
||||||
|
|
||||||
const elements = $derived.by(() => {
|
const elements = $derived.by(() => {
|
||||||
const elements = []
|
const elements = []
|
||||||
@@ -106,6 +106,16 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Don't use loadInboxRelaySelection because we want to force reload
|
// Don't use loadInboxRelaySelection because we want to force reload
|
||||||
load({filters: [{kinds: [INBOX_RELAYS], authors: others}]})
|
load({filters: [{kinds: [INBOX_RELAYS], authors: others}]})
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
dynamicPadding!.style.minHeight = `${chatCompose!.offsetHeight}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(chatCompose!)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(chatCompose!)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -113,106 +123,113 @@
|
|||||||
}, 5000)
|
}, 5000)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative flex h-full w-full flex-col">
|
{#if others.length > 0}
|
||||||
{#if others.length > 0}
|
<PageBar class="chat__page-bar">
|
||||||
<PageBar>
|
{#snippet title()}
|
||||||
{#snippet title()}
|
<div class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
||||||
<div class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
{#if others.length === 1}
|
||||||
{#if others.length === 1}
|
{@const pubkey = others[0]}
|
||||||
{@const pubkey = others[0]}
|
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
||||||
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
<Button onclick={onClick} class="row-2">
|
||||||
<Button onclick={onClick} class="row-2">
|
<ProfileCircle {pubkey} size={5} />
|
||||||
<ProfileCircle {pubkey} size={5} />
|
<ProfileName {pubkey} />
|
||||||
<ProfileName {pubkey} />
|
</Button>
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<ProfileCircles pubkeys={others} size={5} />
|
|
||||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
<ProfileName pubkey={others[0]} />
|
|
||||||
and
|
|
||||||
{#if others.length === 2}
|
|
||||||
<ProfileName pubkey={others[1]} />
|
|
||||||
{:else}
|
|
||||||
{others.length - 1}
|
|
||||||
{others.length > 2 ? "others" : "other"}
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{#if others.length > 2}
|
|
||||||
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
|
|
||||||
>Show all members</Button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet action()}
|
|
||||||
<div>
|
|
||||||
{#if remove($pubkey, missingInboxes).length > 0}
|
|
||||||
{@const count = remove($pubkey, missingInboxes).length}
|
|
||||||
{@const label = count > 1 ? "inboxes are" : "inbox is"}
|
|
||||||
<div
|
|
||||||
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
|
||||||
data-tip="{count} {label} not configured.">
|
|
||||||
<Icon icon="danger" />
|
|
||||||
{count}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</PageBar>
|
|
||||||
{/if}
|
|
||||||
<div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2">
|
|
||||||
{#if missingInboxes.includes($pubkey!)}
|
|
||||||
<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" />
|
|
||||||
Your inbox is not configured.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
|
||||||
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your
|
|
||||||
inbox.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if missingInboxes.length > 0}
|
|
||||||
<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" />
|
|
||||||
{missingInboxes.length}
|
|
||||||
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
|
|
||||||
sure everyone in this conversation has set up their inbox relays.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#each elements as { type, id, value, showPubkey } (id)}
|
|
||||||
{#if type === "date"}
|
|
||||||
<Divider>{value}</Divider>
|
|
||||||
{:else}
|
|
||||||
<ChatMessage event={assertEvent(value)} {pubkeys} {showPubkey} {replyTo} />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
<p
|
|
||||||
class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
|
||||||
<Spinner {loading}>
|
|
||||||
{#if loading}
|
|
||||||
Looking for messages...
|
|
||||||
{:else}
|
{:else}
|
||||||
End of message history
|
<div class="flex items-center gap-2">
|
||||||
|
<ProfileCircles pubkeys={others} size={5} />
|
||||||
|
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<ProfileName pubkey={others[0]} />
|
||||||
|
and
|
||||||
|
{#if others.length === 2}
|
||||||
|
<ProfileName pubkey={others[1]} />
|
||||||
|
{:else}
|
||||||
|
{others.length - 1}
|
||||||
|
{others.length > 2 ? "others" : "other"}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{#if others.length > 2}
|
||||||
|
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
|
||||||
|
>Show all members</Button>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Spinner>
|
</div>
|
||||||
{@render info?.()}
|
{/snippet}
|
||||||
</p>
|
{#snippet action()}
|
||||||
</div>
|
<div>
|
||||||
{#if parent}
|
{#if remove($pubkey, missingInboxes).length > 0}
|
||||||
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
{@const count = remove($pubkey, missingInboxes).length}
|
||||||
|
{@const label = count > 1 ? "inboxes are" : "inbox is"}
|
||||||
|
<div
|
||||||
|
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
||||||
|
data-tip="{count} {label} not configured.">
|
||||||
|
<Icon icon="danger" />
|
||||||
|
{count}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</PageBar>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="chat__messages scroll-container">
|
||||||
|
<div bind:this={dynamicPadding}></div>
|
||||||
|
{#if missingInboxes.includes($pubkey!)}
|
||||||
|
<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" />
|
||||||
|
Your inbox is not configured.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
||||||
|
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your inbox.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if missingInboxes.length > 0}
|
||||||
|
<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" />
|
||||||
|
{missingInboxes.length}
|
||||||
|
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
|
||||||
|
sure everyone in this conversation has set up their inbox relays.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#each elements as { type, id, value, showPubkey } (id)}
|
||||||
|
{#if type === "date"}
|
||||||
|
<Divider>{value}</Divider>
|
||||||
|
{:else}
|
||||||
|
<ChatMessage
|
||||||
|
event={$state.snapshot(value as TrustedEvent)}
|
||||||
|
{pubkeys}
|
||||||
|
{showPubkey}
|
||||||
|
{replyTo} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
||||||
|
<Spinner {loading}>
|
||||||
|
{#if loading}
|
||||||
|
Looking for messages...
|
||||||
|
{:else}
|
||||||
|
End of message history
|
||||||
|
{/if}
|
||||||
|
</Spinner>
|
||||||
|
{@render info?.()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
||||||
|
<div>
|
||||||
|
{#if parent}
|
||||||
|
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<ChatCompose bind:this={compose} {onSubmit} />
|
<ChatCompose bind:this={compose} {onSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
@@ -27,12 +27,12 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
replyTo?: any
|
replyTo: (event: TrustedEvent) => void
|
||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
showPubkey?: boolean
|
showPubkey?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, replyTo = undefined, pubkeys, showPubkey = false}: Props = $props()
|
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
const thunk = $thunks[event.id]
|
||||||
const isOwn = event.pubkey === $pubkey
|
const isOwn = event.pubkey === $pubkey
|
||||||
@@ -40,6 +40,8 @@
|
|||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||||
|
|
||||||
|
const reply = () => replyTo(event)
|
||||||
|
|
||||||
const onReactionClick = async (content: string, events: TrustedEvent[]) => {
|
const onReactionClick = async (content: string, events: TrustedEvent[]) => {
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||||
const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content})
|
const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content})
|
||||||
@@ -49,7 +51,7 @@
|
|||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys})
|
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
|
||||||
|
|
||||||
const togglePopover = () => {
|
const togglePopover = () => {
|
||||||
if (popoverIsVisible) {
|
if (popoverIsVisible) {
|
||||||
@@ -72,32 +74,34 @@
|
|||||||
class:chat-start={!isOwn}
|
class:chat-start={!isOwn}
|
||||||
class:flex-row-reverse={!isOwn}
|
class:flex-row-reverse={!isOwn}
|
||||||
class:chat-end={isOwn}>
|
class:chat-end={isOwn}>
|
||||||
<Tippy
|
{#if !isMobile}
|
||||||
bind:popover
|
<Tippy
|
||||||
component={ChatMessageMenu}
|
bind:popover
|
||||||
props={{event, pubkeys, popover, replyTo}}
|
component={ChatMessageMenu}
|
||||||
params={{
|
props={{event, pubkeys, popover, replyTo}}
|
||||||
interactive: true,
|
params={{
|
||||||
trigger: "manual",
|
interactive: true,
|
||||||
onShow() {
|
trigger: "manual",
|
||||||
popoverIsVisible = true
|
onShow() {
|
||||||
},
|
popoverIsVisible = true
|
||||||
onHidden() {
|
},
|
||||||
popoverIsVisible = false
|
onHidden() {
|
||||||
},
|
popoverIsVisible = false
|
||||||
}}>
|
},
|
||||||
<button
|
}}>
|
||||||
type="button"
|
<button
|
||||||
class="opacity-0 transition-all"
|
type="button"
|
||||||
class:group-hover:opacity-100={!isMobile}
|
class="opacity-0 transition-all"
|
||||||
onclick={togglePopover}>
|
class:group-hover:opacity-100={!isMobile}
|
||||||
<Icon icon="menu-dots" size={4} />
|
onclick={togglePopover}>
|
||||||
</button>
|
<Icon icon="menu-dots" size={4} />
|
||||||
</Tippy>
|
</button>
|
||||||
|
</Tippy>
|
||||||
|
{/if}
|
||||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||||
<LongPress
|
<TapTarget
|
||||||
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
||||||
onLongPress={showMobileMenu}>
|
onTap={showMobileMenu}>
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if !isOwn}
|
{#if !isOwn}
|
||||||
@@ -120,7 +124,7 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<Content showEntire {event} />
|
<Content showEntire {event} />
|
||||||
</div>
|
</div>
|
||||||
</LongPress>
|
</TapTarget>
|
||||||
<div class="row-2 z-feature -mt-1 ml-4">
|
<div class="row-2 z-feature -mt-1 ml-4">
|
||||||
<ReactionSummary {event} {onReactionClick} noTooltip />
|
<ReactionSummary {event} {onReactionClick} noTooltip />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||||
@@ -8,15 +9,26 @@
|
|||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {clip} from "@app/toast"
|
import {clip} from "@app/toast"
|
||||||
|
|
||||||
const {event, pubkeys} = $props()
|
type Props = {
|
||||||
|
pubkeys: string[]
|
||||||
const onEmoji = (emoji: NativeEmoji) => {
|
event: TrustedEvent
|
||||||
history.back()
|
reply: () => void
|
||||||
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {event, pubkeys, reply}: Props = $props()
|
||||||
|
|
||||||
|
const onEmoji = ((event: TrustedEvent, emoji: NativeEmoji) => {
|
||||||
|
history.back()
|
||||||
|
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
||||||
|
}).bind(undefined, event)
|
||||||
|
|
||||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||||
|
|
||||||
|
const sendReply = () => {
|
||||||
|
history.back()
|
||||||
|
reply()
|
||||||
|
}
|
||||||
|
|
||||||
const copyText = () => {
|
const copyText = () => {
|
||||||
history.back()
|
history.back()
|
||||||
clip(event.content)
|
clip(event.content)
|
||||||
@@ -30,6 +42,10 @@
|
|||||||
<Icon size={4} icon="smile-circle" />
|
<Icon size={4} icon="smile-circle" />
|
||||||
Send Reaction
|
Send Reaction
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||||
|
<Icon size={4} icon="reply" />
|
||||||
|
Send Reply
|
||||||
|
</Button>
|
||||||
<Button class="btn btn-neutral w-full" onclick={copyText}>
|
<Button class="btn btn-neutral w-full" onclick={copyText}>
|
||||||
<Icon size={4} icon="copy" />
|
<Icon size={4} icon="copy" />
|
||||||
Copy Text
|
Copy Text
|
||||||
|
|||||||
+8
-4
@@ -1,14 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import {publishDelete} from "@app/commands"
|
import {publishDelete} from "@app/commands"
|
||||||
import {clearModals} from "@app/modal"
|
import {clearModals} from "@app/modal"
|
||||||
|
|
||||||
const {url, event} = $props()
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
const confirm = async () => {
|
const confirm = async () => {
|
||||||
const snapshot = $state.snapshot(event)
|
await publishDelete({event, relays: [url]})
|
||||||
|
|
||||||
await publishDelete({event: snapshot, relays: [url]})
|
|
||||||
|
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
import {ctx} from "@welshman/lib"
|
import {ctx} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import {clip} from "@app/toast"
|
import {clip} from "@app/toast"
|
||||||
|
|
||||||
const {event} = $props()
|
type Props = {
|
||||||
|
url?: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
const relays = ctx.app.router.Event(event).getUrls()
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const relays = url ? [url] : ctx.app.router.Event(event).getUrls()
|
||||||
const nevent1 = nip19.neventEncode({...event, relays})
|
const nevent1 = nip19.neventEncode({...event, relays})
|
||||||
const npub1 = nip19.npubEncode(event.pubkey)
|
const npub1 = nip19.npubEncode(event.pubkey)
|
||||||
const json = JSON.stringify(event, null, 2)
|
const json = JSON.stringify(event, null, 2)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import EventReport from "@app/components/EventReport.svelte"
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import EventShare from "@app/components/EventShare.svelte"
|
import EventShare from "@app/components/EventShare.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {url, event})
|
||||||
}
|
}
|
||||||
|
|
||||||
const share = () => {
|
const share = () => {
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
const showDelete = () => {
|
const showDelete = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(ConfirmDelete, {url, event})
|
pushModal(EventDeleteConfirm, {url, event})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal, clearModals} from "@app/modal"
|
||||||
import {PLATFORM_NAME, BURROW_URL} from "@app/state"
|
import {PLATFORM_NAME, BURROW_URL} from "@app/state"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {loadUserData} from "@app/commands"
|
import {loadUserData} from "@app/requests"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
|
|
||||||
let signers: any[] = $state([])
|
let signers: any[] = $state([])
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import QRCode from "@app/components/QRCode.svelte"
|
import QRCode from "@app/components/QRCode.svelte"
|
||||||
import InfoBunker from "@app/components/InfoBunker.svelte"
|
import InfoBunker from "@app/components/InfoBunker.svelte"
|
||||||
import {loginWithNip46, loadUserData} from "@app/commands"
|
import {loginWithNip46} from "@app/commands"
|
||||||
|
import {loadUserData} from "@app/requests"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal, clearModals} from "@app/modal"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
||||||
import {loadUserData} from "@app/commands"
|
import {loadUserData} from "@app/requests"
|
||||||
import {clearModals, pushModal} from "@app/modal"
|
import {clearModals, pushModal} from "@app/modal"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let modal: any = $state()
|
let modal: any = $state.raw()
|
||||||
const hash = $derived($page.url.hash.slice(1))
|
const hash = $derived($page.url.hash.slice(1))
|
||||||
const hashIsValid = $derived(Boolean($modals[hash]))
|
const hashIsValid = $derived(Boolean($modals[hash]))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {chunk, sleep, uniq} from "@welshman/lib"
|
||||||
|
import {
|
||||||
|
createEvent,
|
||||||
|
createProfile,
|
||||||
|
PROFILE,
|
||||||
|
DELETE,
|
||||||
|
isReplaceable,
|
||||||
|
getAddress,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import {pubkey, userRelaySelections, publishThunk, getRelayUrls, repository} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
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 {pushToast} from "@app/toast"
|
||||||
|
import {logout} from "@app/commands"
|
||||||
|
import {INDEXER_RELAYS, PLATFORM_NAME, userMembership, getMembershipUrls} from "@app/state"
|
||||||
|
|
||||||
|
let progress: number | undefined = $state(undefined)
|
||||||
|
let confirmText = $state("")
|
||||||
|
|
||||||
|
const CONFIRM_TEXT = "permanently delete my nostr account"
|
||||||
|
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
|
||||||
|
const showProgress = $derived(progress !== undefined)
|
||||||
|
|
||||||
|
const deleteProfile = async () => {
|
||||||
|
if (!confirmOk) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please type your confirmation into the text box.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}]))
|
||||||
|
const profileEvent = createEvent(PROFILE, createProfile({name: "[deleted]"}))
|
||||||
|
const vanishEvent = createEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
|
||||||
|
const denominator = chunks.length + 2
|
||||||
|
const relays = uniq([
|
||||||
|
...INDEXER_RELAYS,
|
||||||
|
...getRelayUrls($userRelaySelections),
|
||||||
|
...getMembershipUrls($userMembership),
|
||||||
|
])
|
||||||
|
|
||||||
|
let step = 0
|
||||||
|
|
||||||
|
const incrementProgress = async () => {
|
||||||
|
progress = ++step / denominator
|
||||||
|
|
||||||
|
return sleep(800)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, blank out their profile in case relays don't support deletion by address
|
||||||
|
await publishThunk({relays, event: profileEvent})
|
||||||
|
|
||||||
|
await incrementProgress()
|
||||||
|
|
||||||
|
// Next, send a "right to vanish" event to all relays
|
||||||
|
await publishThunk({relays, event: vanishEvent})
|
||||||
|
|
||||||
|
await incrementProgress()
|
||||||
|
|
||||||
|
// Finally, send deletion requests for all known events in case relays don't support right to vanish
|
||||||
|
for (const events of chunks) {
|
||||||
|
const tags: string[][] = []
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
tags.push(["e", event.id])
|
||||||
|
|
||||||
|
if (isReplaceable(event)) {
|
||||||
|
tags.push(["a", getAddress(event)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await publishThunk({relays, event: createEvent(DELETE, {tags})})
|
||||||
|
|
||||||
|
await incrementProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let them see that progress is complete
|
||||||
|
await sleep(2000)
|
||||||
|
|
||||||
|
// Goodbye forever!
|
||||||
|
await logout()
|
||||||
|
|
||||||
|
window.location.href = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
progress = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteProfile()
|
||||||
|
} catch (e) {
|
||||||
|
progress = undefined
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(confirm)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
Delete your account
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
From the Nostr network
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
{#if showProgress}
|
||||||
|
<p>
|
||||||
|
We are currently sending deletion requests to your relay selections and space hosts. Please
|
||||||
|
wait while we complete this process. Once we're done, you'll be automatically logged out.
|
||||||
|
</p>
|
||||||
|
<progress class="progress progress-primary w-full" value={progress! * 100} max="100"></progress>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
This will delete your nostr account everywhere, not just on {PLATFORM_NAME}.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To confirm, please type "{CONFIRM_TEXT}" into the text box below. This action can't be undone.
|
||||||
|
</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<input bind:value={confirmText} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
<p>
|
||||||
|
<strong>Note:</strong> not all relays may honor your request for deletion. If you find that your
|
||||||
|
content continues to be available, please contact the offending relays directly.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-error" disabled={showProgress || !confirmOk}>
|
||||||
|
<Spinner loading={progress !== undefined}>Confirm</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {ctx} from "@welshman/lib"
|
import {ctx} from "@welshman/lib"
|
||||||
|
import type {Profile} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
createEvent,
|
createEvent,
|
||||||
makeProfile,
|
makeProfile,
|
||||||
@@ -8,76 +9,31 @@
|
|||||||
isPublishedProfile,
|
isPublishedProfile,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {pubkey, profilesByPubkey, publishThunk} from "@welshman/app"
|
import {pubkey, profilesByPubkey, publishThunk} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||||
import InfoHandle from "@app/components/InfoHandle.svelte"
|
import {clearModals} from "@app/modal"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
const values = $state({...($profilesByPubkey.get($pubkey!) || makeProfile())})
|
const initialValues = {...($profilesByPubkey.get($pubkey!) || makeProfile())}
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const saveEdit = () => {
|
const onsubmit = (profile: Profile) => {
|
||||||
const relays = ctx.app.router.FromUser().getUrls()
|
const relays = ctx.app.router.FromUser().getUrls()
|
||||||
const template = isPublishedProfile(values)
|
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
|
||||||
? editProfile($state.snapshot(values))
|
|
||||||
: createProfile($state.snapshot(values))
|
|
||||||
const event = createEvent(template.kind, template)
|
const event = createEvent(template.kind, template)
|
||||||
|
|
||||||
publishThunk({event, relays})
|
publishThunk({event, relays})
|
||||||
pushToast({message: "Your profile has been updated!"})
|
pushToast({message: "Your profile has been updated!"})
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|
||||||
let file: File | undefined = $state()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="col-4" onsubmit={preventDefault(saveEdit)}>
|
<ProfileEditForm {initialValues} {onsubmit}>
|
||||||
<div class="flex justify-center py-2">
|
{#snippet footer()}
|
||||||
<InputProfilePicture bind:file bind:url={values.picture} />
|
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
||||||
</div>
|
<Button class="btn btn-neutral" onclick={back}>Discard Changes</Button>
|
||||||
<Field>
|
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
||||||
{#snippet label()}
|
</div>
|
||||||
<p>Username</p>
|
{/snippet}
|
||||||
{/snippet}
|
</ProfileEditForm>
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon="user-circle" />
|
|
||||||
<input bind:value={values.name} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>About You</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<textarea class="textarea textarea-bordered leading-4" rows="3" bind:value={values.about}>
|
|
||||||
</textarea>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Nostr Address</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon="map-point" />
|
|
||||||
<input bind:value={values.nip05} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<p>
|
|
||||||
<Button class="link" onclick={() => pushModal(InfoHandle)}>What is a nostr address?</Button>
|
|
||||||
</p>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
|
||||||
<Button class="btn btn-neutral" onclick={back}>Discard Changes</Button>
|
|
||||||
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
|
import type {Profile} from "@welshman/util"
|
||||||
|
import {makeProfile} from "@welshman/util"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
||||||
|
import InfoHandle from "@app/components/InfoHandle.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialValues?: Profile
|
||||||
|
onsubmit: (profile: Profile) => void
|
||||||
|
hideAddress?: boolean
|
||||||
|
footer: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
const {initialValues = makeProfile(), hideAddress, onsubmit, footer}: Props = $props()
|
||||||
|
|
||||||
|
const values = $state(initialValues)
|
||||||
|
|
||||||
|
const submit = () => onsubmit($state.snapshot(values))
|
||||||
|
|
||||||
|
let file: File | undefined = $state()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="col-4" onsubmit={preventDefault(submit)}>
|
||||||
|
<div class="flex justify-center py-2">
|
||||||
|
<InputProfilePicture bind:file bind:url={values.picture} />
|
||||||
|
</div>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Username</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="user-circle" />
|
||||||
|
<input bind:value={values.name} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
What would you like people to call you?
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>About You</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<textarea class="textarea textarea-bordered leading-4" rows="3" bind:value={values.about}
|
||||||
|
></textarea>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
Give a brief introduction to why you're here.
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{#if !hideAddress}
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Nostr Address</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="map-point" />
|
||||||
|
<input bind:value={values.nip05} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>
|
||||||
|
<Button class="link" onclick={() => pushModal(InfoHandle)}
|
||||||
|
>What is a nostr address?</Button>
|
||||||
|
</p>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{/if}
|
||||||
|
{@render footer()}
|
||||||
|
</form>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
import {postJson} from "@welshman/lib"
|
import {postJson} from "@welshman/lib"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -8,16 +9,22 @@
|
|||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||||
|
import SignUpKey from "@app/components/SignUpKey.svelte"
|
||||||
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {BURROW_URL, PLATFORM_NAME} from "@app/state"
|
import {BURROW_URL, PLATFORM_NAME, PLATFORM_ACCENT} from "@app/state"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
const ac = window.location.origin
|
const params = new URLSearchParams({
|
||||||
|
an: PLATFORM_NAME,
|
||||||
|
ac: window.location.origin,
|
||||||
|
at: isMobile ? "android" : "web",
|
||||||
|
aa: PLATFORM_ACCENT.slice(1),
|
||||||
|
am: "dark",
|
||||||
|
asf: "yes",
|
||||||
|
})
|
||||||
|
|
||||||
const at = isMobile ? "android" : "web"
|
const nstart = `https://start.njump.me/?${params.toString()}`
|
||||||
|
|
||||||
const nstart = `https://start.njump.me/?an=Flotilla&at=${at}&ac=${ac}`
|
|
||||||
|
|
||||||
const login = () => pushModal(LogIn)
|
const login = () => pushModal(LogIn)
|
||||||
|
|
||||||
@@ -37,18 +44,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signup = () => {
|
const usePassword = () => {
|
||||||
if (BURROW_URL) {
|
if (BURROW_URL) {
|
||||||
signupPassword()
|
signupPassword()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useKey = () => pushModal(SignUpKey)
|
||||||
|
|
||||||
let email = $state("")
|
let email = $state("")
|
||||||
let password = $state("")
|
let password = $state("")
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(signup)}>
|
<form class="column gap-4" onsubmit={preventDefault(usePassword)}>
|
||||||
<h1 class="heading">Sign up with Nostr</h1>
|
<h1 class="heading">Sign up with Nostr</h1>
|
||||||
<p class="m-auto max-w-sm text-center">
|
<p class="m-auto max-w-sm text-center">
|
||||||
{PLATFORM_NAME} is built using the
|
{PLATFORM_NAME} is built using the
|
||||||
@@ -89,10 +98,17 @@
|
|||||||
</p>
|
</p>
|
||||||
<Divider>Or</Divider>
|
<Divider>Or</Divider>
|
||||||
{/if}
|
{/if}
|
||||||
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
{#if Capacitor.isNativePlatform()}
|
||||||
<Icon icon="square-share-line" />
|
<Button onclick={useKey} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||||
Get going on nstart
|
<Icon icon="key" />
|
||||||
</a>
|
Generate a key
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||||
|
<Icon icon="square-share-line" />
|
||||||
|
Create an account on Nstart
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
<Button class="link" onclick={login}>Log in instead</Button>
|
<Button class="link" onclick={login}>Log in instead</Button>
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {encrypt} from "nostr-tools/nip49"
|
||||||
|
import {hexToBytes} from "@noble/hashes/utils"
|
||||||
|
import {makeSecret, getPubkey} from "@welshman/signer"
|
||||||
|
import {preventDefault, downloadText} from "@lib/html"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import SignUpKeyConfirm from "@app/components/SignUpKeyConfirm.svelte"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const secret = makeSecret()
|
||||||
|
|
||||||
|
const pubkey = getPubkey(secret)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
if (password.length < 12) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Passwords must be at least 12 characters long.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ncryptsec = encrypt(hexToBytes(secret), password)
|
||||||
|
|
||||||
|
downloadText("Nostr Secret Key.txt", ncryptsec)
|
||||||
|
|
||||||
|
pushModal(SignUpKeyConfirm, {secret, pubkey, ncryptsec})
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = ""
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Welcome to Nostr!</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
<Link external href="https://nostr.com/" class="link">Nostr</Link> is way to build social apps that
|
||||||
|
talk to each other. Users own their social identity instead of renting it from a tech company, and
|
||||||
|
can take it with them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This means that instead of using a password to log in, you generate a <strong
|
||||||
|
>secret key</strong>
|
||||||
|
which gives you full control over your account.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Keeping this key safe is very important, so we encourage you to download an encrypted copy. To
|
||||||
|
do this, go ahead and fill in the password you'd like to use to secure your key below.
|
||||||
|
</p>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
Password*
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="key" />
|
||||||
|
<input bind:value={password} class="grow" type="password" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>Passwords should be at least 12 characters long. Write this down!</p>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" type="submit">
|
||||||
|
Download my key
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {preventDefault, copyToClipboard} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secret: string
|
||||||
|
pubkey: string
|
||||||
|
ncryptsec: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {secret, pubkey, ncryptsec}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
copyToClipboard(ncryptsec)
|
||||||
|
pushToast({message: "Your secret key has been copied to your clipboard!"})
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
pushModal(SignUpProfile, {secret, pubkey})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Download your key</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
Great! We've encrypted your secret key and saved it to your device. If that didn't work, or if
|
||||||
|
you'd rather save your key somewhere else, you can find the encrypted version below:
|
||||||
|
</p>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
Encrypted Secret Key
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="key" />
|
||||||
|
<input value={ncryptsec} class="ellipsize grow" />
|
||||||
|
<Button onclick={copy} class="flex items-center">
|
||||||
|
<Icon icon="copy" />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" type="submit">
|
||||||
|
Fill out your profile
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Profile} from "@welshman/util"
|
||||||
|
import {PROFILE, createProfile, createEvent} from "@welshman/util"
|
||||||
|
import {addSession, publishThunk} from "@welshman/app"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||||
|
import {INDEXER_RELAYS} from "@app/state"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secret: string
|
||||||
|
pubkey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {secret, pubkey}: Props = $props()
|
||||||
|
|
||||||
|
const onsubmit = (profile: Profile) => {
|
||||||
|
const event = createEvent(PROFILE, createProfile(profile))
|
||||||
|
|
||||||
|
addSession({method: "nip01", secret, pubkey})
|
||||||
|
publishThunk({event, relays: INDEXER_RELAYS})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProfileEditForm hideAddress {onsubmit}>
|
||||||
|
{#snippet footer()}
|
||||||
|
<Button type="submit" class="btn btn-primary">Create Account</Button>
|
||||||
|
{/snippet}
|
||||||
|
</ProfileEditForm>
|
||||||
@@ -1,36 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {ctx, sleep} from "@welshman/lib"
|
import {ctx, sleep} from "@welshman/lib"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import SpaceVisitConfirm, {confirmSpaceVisit} from "@app/components/SpaceVisitConfirm.svelte"
|
||||||
import {attemptRelayAccess} from "@app/commands"
|
import {attemptRelayAccess} from "@app/commands"
|
||||||
import {makeSpacePath} from "@app/routes"
|
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
const path = makeSpacePath(url)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const confirm = () => goto(path, {replaceState: true})
|
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (!error && ctx.net.pool.get(url).stats.lastAuth === 0) {
|
if (!error && ctx.net.pool.get(url).stats.lastAuth === 0) {
|
||||||
pushModal(Confirm, {
|
pushModal(SpaceVisitConfirm, {url}, {replaceState: true})
|
||||||
confirm,
|
|
||||||
message: `This space does not appear to limit who can post to it. This can result
|
|
||||||
in a large amount of spam or other objectionable content. Continue?`,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
confirm()
|
confirmSpaceVisit(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {ctx, tryCatch} from "@welshman/lib"
|
import {ctx, tryCatch} from "@welshman/lib"
|
||||||
import {isRelayUrl, normalizeRelayUrl} from "@welshman/util"
|
import {isRelayUrl, normalizeRelayUrl} from "@welshman/util"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
@@ -7,27 +6,16 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import InfoRelay from "@app/components/InfoRelay.svelte"
|
import InfoRelay from "@app/components/InfoRelay.svelte"
|
||||||
|
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {addSpaceMembership, attemptRelayAccess} from "@app/commands"
|
import {attemptRelayAccess} from "@app/commands"
|
||||||
import {makeSpacePath} from "@app/routes"
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const confirm = async (url: string) => {
|
|
||||||
await addSpaceMembership(url)
|
|
||||||
|
|
||||||
goto(makeSpacePath(url), {replaceState: true})
|
|
||||||
|
|
||||||
pushToast({
|
|
||||||
message: "Welcome to the space!",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinRelay = async (invite: string) => {
|
const joinRelay = async (invite: string) => {
|
||||||
const [raw, claim] = invite.split("|")
|
const [raw, claim] = invite.split("|")
|
||||||
const url = normalizeRelayUrl(raw)
|
const url = normalizeRelayUrl(raw)
|
||||||
@@ -40,13 +28,9 @@
|
|||||||
const connection = ctx.net.pool.get(url)
|
const connection = ctx.net.pool.get(url)
|
||||||
|
|
||||||
if (connection.stats.lastAuth === 0) {
|
if (connection.stats.lastAuth === 0) {
|
||||||
pushModal(Confirm, {
|
pushModal(SpaceJoinConfirm, {url}, {replaceState: true})
|
||||||
confirm: () => confirm(url),
|
|
||||||
message: `This space does not appear to limit who can post to it. This can result
|
|
||||||
in a large amount of spam or other objectionable content. Continue?`,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
await confirm(url)
|
await confirmSpaceJoin(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script module lang="ts">
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
import {addSpaceMembership} from "@app/commands"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
export const confirmSpaceJoin = async (url: string) => {
|
||||||
|
await addSpaceMembership(url)
|
||||||
|
|
||||||
|
goto(makeSpacePath(url), {replaceState: true})
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
message: "Welcome to the space!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const confirm = () => confirmSpaceJoin(url)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Confirm
|
||||||
|
{confirm}
|
||||||
|
message="This space does not appear to limit who can post to it. This can result in a large amount of spam or other objectionable content. Continue?" />
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script module lang="ts">
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
|
||||||
|
export const confirmSpaceVisit = (url: string) => {
|
||||||
|
goto(makeSpacePath(url), {replaceState: true})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const confirm = () => confirmSpaceVisit(url)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Confirm
|
||||||
|
{confirm}
|
||||||
|
message="This space does not appear to limit who can post to it. This can result in a large amount of spam or other objectionable content. Continue?" />
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import "@welshman/editor/index.css"
|
|
||||||
|
|
||||||
import {mount} from "svelte"
|
import {mount} from "svelte"
|
||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
|
|||||||
+94
-6
@@ -1,5 +1,19 @@
|
|||||||
import {get, writable} from "svelte/store"
|
import {get, writable} from "svelte/store"
|
||||||
import {partition, shuffle, int, YEAR, MONTH, insert, sortBy, assoc, now} from "@welshman/lib"
|
import {
|
||||||
|
partition,
|
||||||
|
chunk,
|
||||||
|
sample,
|
||||||
|
sleep,
|
||||||
|
shuffle,
|
||||||
|
uniq,
|
||||||
|
int,
|
||||||
|
YEAR,
|
||||||
|
MONTH,
|
||||||
|
insert,
|
||||||
|
sortBy,
|
||||||
|
assoc,
|
||||||
|
now,
|
||||||
|
} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
MESSAGE,
|
MESSAGE,
|
||||||
DELETE,
|
DELETE,
|
||||||
@@ -9,10 +23,11 @@ import {
|
|||||||
matchFilters,
|
matchFilters,
|
||||||
getTagValues,
|
getTagValues,
|
||||||
getTagValue,
|
getTagValue,
|
||||||
|
isShareableRelayUrl,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
import type {TrustedEvent, Filter, List} from "@welshman/util"
|
||||||
import {feedFromFilters, makeRelayFeed, makeIntersectionFeed} from "@welshman/feeds"
|
import {feedFromFilters, makeRelayFeed, makeIntersectionFeed} from "@welshman/feeds"
|
||||||
import type {Subscription} from "@welshman/net"
|
import type {Subscription, SubscribeRequestWithHandlers} from "@welshman/net"
|
||||||
import type {AppSyncOpts, Thunk} from "@welshman/app"
|
import type {AppSyncOpts, Thunk} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
subscribe,
|
subscribe,
|
||||||
@@ -22,10 +37,26 @@ import {
|
|||||||
hasNegentropy,
|
hasNegentropy,
|
||||||
thunkWorker,
|
thunkWorker,
|
||||||
createFeedController,
|
createFeedController,
|
||||||
|
loadRelay,
|
||||||
|
loadMutes,
|
||||||
|
loadFollows,
|
||||||
|
loadProfile,
|
||||||
|
loadInboxRelaySelections,
|
||||||
|
getRelayUrls,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
import {daysBetween} from "@lib/util"
|
import {daysBetween} from "@lib/util"
|
||||||
import {userRoomsByUrl, getUrlsForEvent} from "@app/state"
|
import {
|
||||||
|
ALERT,
|
||||||
|
ALERT_STATUS,
|
||||||
|
NOTIFIER_RELAY,
|
||||||
|
INDEXER_RELAYS,
|
||||||
|
getDefaultPubkeys,
|
||||||
|
userRoomsByUrl,
|
||||||
|
getUrlsForEvent,
|
||||||
|
loadMembership,
|
||||||
|
loadSettings,
|
||||||
|
} from "@app/state"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -280,6 +311,20 @@ export const makeCalendarFeed = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Domain specific
|
||||||
|
|
||||||
|
export const loadAlerts = (pubkey: string) =>
|
||||||
|
load({
|
||||||
|
relays: [NOTIFIER_RELAY],
|
||||||
|
filters: [{kinds: [ALERT], authors: [pubkey]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const loadAlertStatuses = (pubkey: string) =>
|
||||||
|
load({
|
||||||
|
relays: [NOTIFIER_RELAY],
|
||||||
|
filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}],
|
||||||
|
})
|
||||||
|
|
||||||
// Application requests
|
// Application requests
|
||||||
|
|
||||||
export const listenForNotifications = () => {
|
export const listenForNotifications = () => {
|
||||||
@@ -294,7 +339,9 @@ export const listenForNotifications = () => {
|
|||||||
relays: [url],
|
relays: [url],
|
||||||
filters: [
|
filters: [
|
||||||
{kinds: [THREAD], limit: 1},
|
{kinds: [THREAD], limit: 1},
|
||||||
|
{kinds: [EVENT_TIME], limit: 1},
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
|
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
|
||||||
|
{kinds: [COMMENT], "#K": [String(EVENT_TIME)], limit: 1},
|
||||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
|
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -303,8 +350,8 @@ export const listenForNotifications = () => {
|
|||||||
subscribe({
|
subscribe({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
filters: [
|
filters: [
|
||||||
{kinds: [THREAD], since: now()},
|
{kinds: [THREAD, EVENT_TIME], since: now()},
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
|
{kinds: [COMMENT], "#K": [String(THREAD), String(EVENT_TIME)], since: now()},
|
||||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -317,3 +364,44 @@ export const listenForNotifications = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadUserData = (
|
||||||
|
pubkey: string,
|
||||||
|
request: Partial<SubscribeRequestWithHandlers> = {},
|
||||||
|
) => {
|
||||||
|
const promise = Promise.race([
|
||||||
|
sleep(3000),
|
||||||
|
Promise.all([
|
||||||
|
loadInboxRelaySelections(pubkey, request),
|
||||||
|
loadMembership(pubkey, request),
|
||||||
|
loadSettings(pubkey, request),
|
||||||
|
loadProfile(pubkey, request),
|
||||||
|
loadFollows(pubkey, request),
|
||||||
|
loadMutes(pubkey, request),
|
||||||
|
loadAlertStatuses(pubkey),
|
||||||
|
loadAlerts(pubkey),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
|
||||||
|
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
|
||||||
|
promise.then(async () => {
|
||||||
|
for (const pubkeys of chunk(50, getDefaultPubkeys())) {
|
||||||
|
const relays = sample(1, INDEXER_RELAYS)
|
||||||
|
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
for (const pubkey of pubkeys) {
|
||||||
|
loadMembership(pubkey, {relays})
|
||||||
|
loadProfile(pubkey, {relays})
|
||||||
|
loadFollows(pubkey, {relays})
|
||||||
|
loadMutes(pubkey, {relays})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export const discoverRelays = (lists: List[]) =>
|
||||||
|
Promise.all(uniq(lists.flatMap(getRelayUrls)).filter(isShareableRelayUrl).map(loadRelay))
|
||||||
|
|||||||
+55
-7
@@ -41,7 +41,7 @@ import {
|
|||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
|
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
|
||||||
import {Nip59} from "@welshman/signer"
|
import {Nip59, decrypt} from "@welshman/signer"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
repository,
|
repository,
|
||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
ensurePlaintext,
|
ensurePlaintext,
|
||||||
thunks,
|
thunks,
|
||||||
walkThunks,
|
walkThunks,
|
||||||
|
signer,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {Thunk, Relay} from "@welshman/app"
|
import type {Thunk, Relay} from "@welshman/app"
|
||||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||||
@@ -73,6 +74,15 @@ export const GENERAL = "_"
|
|||||||
|
|
||||||
export const PROTECTED = ["-"]
|
export const PROTECTED = ["-"]
|
||||||
|
|
||||||
|
export const ALERT = 32830
|
||||||
|
|
||||||
|
export const ALERT_STATUS = 32831
|
||||||
|
|
||||||
|
export const NOTIFIER_PUBKEY = "27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df"
|
||||||
|
|
||||||
|
// export const NOTIFIER_RELAY = 'wss://notifier.flotilla.social/'
|
||||||
|
export const NOTIFIER_RELAY = "ws://localhost:4738/"
|
||||||
|
|
||||||
export const INDEXER_RELAYS = [
|
export const INDEXER_RELAYS = [
|
||||||
"wss://purplepag.es/",
|
"wss://purplepag.es/",
|
||||||
"wss://relay.damus.io/",
|
"wss://relay.damus.io/",
|
||||||
@@ -295,6 +305,8 @@ export type Settings = {
|
|||||||
values: {
|
values: {
|
||||||
show_media: boolean
|
show_media: boolean
|
||||||
hide_sensitive: boolean
|
hide_sensitive: boolean
|
||||||
|
report_usage: boolean
|
||||||
|
report_errors: boolean
|
||||||
send_delay: number
|
send_delay: number
|
||||||
upload_type: "nip96" | "blossom"
|
upload_type: "nip96" | "blossom"
|
||||||
nip96_urls: string[]
|
nip96_urls: string[]
|
||||||
@@ -305,6 +317,8 @@ export type Settings = {
|
|||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
show_media: true,
|
show_media: true,
|
||||||
hide_sensitive: true,
|
hide_sensitive: true,
|
||||||
|
report_usage: true,
|
||||||
|
report_errors: false,
|
||||||
send_delay: 3000,
|
send_delay: 3000,
|
||||||
upload_type: "nip96",
|
upload_type: "nip96",
|
||||||
nip96_urls: ["https://nostr.build"],
|
nip96_urls: ["https://nostr.build"],
|
||||||
@@ -332,6 +346,40 @@ export const {
|
|||||||
load({...request, filters: [{kinds: [SETTINGS], authors: [pubkey]}]}),
|
load({...request, filters: [{kinds: [SETTINGS], authors: [pubkey]}]}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Alerts
|
||||||
|
|
||||||
|
export type Alert = {
|
||||||
|
event: TrustedEvent
|
||||||
|
tags: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const alerts = deriveEventsMapped<Alert>(repository, {
|
||||||
|
filters: [{kinds: [ALERT]}],
|
||||||
|
itemToEvent: item => item.event,
|
||||||
|
eventToItem: async event => {
|
||||||
|
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
||||||
|
|
||||||
|
return {event, tags}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Alert Statuses
|
||||||
|
|
||||||
|
export type AlertStatus = {
|
||||||
|
event: TrustedEvent
|
||||||
|
tags: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
|
||||||
|
filters: [{kinds: [ALERT_STATUS]}],
|
||||||
|
itemToEvent: item => item.event,
|
||||||
|
eventToItem: async event => {
|
||||||
|
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
||||||
|
|
||||||
|
return {event, tags}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Membership
|
// Membership
|
||||||
|
|
||||||
export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
||||||
@@ -345,18 +393,18 @@ export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
|||||||
export const getMembershipUrls = (list?: List) => {
|
export const getMembershipUrls = (list?: List) => {
|
||||||
const tags = getListTags(list)
|
const tags = getListTags(list)
|
||||||
|
|
||||||
return sort(uniq([...getRelayTagValues(tags), ...getGroupTags(tags).map(nth(2))]))
|
return sort(
|
||||||
|
uniq([...getRelayTagValues(tags), ...getGroupTags(tags).map(nth(2))]).map(url =>
|
||||||
|
normalizeRelayUrl(url),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMembershipRooms = (list?: List) =>
|
export const getMembershipRooms = (list?: List) =>
|
||||||
getGroupTags(getListTags(list)).map(([_, room, url, name = ""]) => ({url, room, name}))
|
getGroupTags(getListTags(list)).map(([_, room, url, name = ""]) => ({url, room, name}))
|
||||||
|
|
||||||
export const getMembershipRoomsByUrl = (url: string, list?: List) =>
|
export const getMembershipRoomsByUrl = (url: string, list?: List) =>
|
||||||
sort(
|
sort(getGroupTags(getListTags(list)).filter(nthEq(2, url)).map(nth(1)))
|
||||||
getGroupTags(getListTags(list))
|
|
||||||
.filter(t => t[2] === url)
|
|
||||||
.map(nth(1)),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const memberships = deriveEventsMapped<PublishedList>(repository, {
|
export const memberships = deriveEventsMapped<PublishedList>(repository, {
|
||||||
filters: [{kinds: [GROUPS]}],
|
filters: [{kinds: [GROUPS]}],
|
||||||
|
|||||||
+8
-1
@@ -1,10 +1,17 @@
|
|||||||
import * as Sentry from "@sentry/browser"
|
import * as Sentry from "@sentry/browser"
|
||||||
|
import {getSetting} from "@app/state"
|
||||||
|
|
||||||
export const setupTracking = () => {
|
export const setupTracking = () => {
|
||||||
if (import.meta.env.VITE_GLITCHTIP_API_KEY) {
|
if (import.meta.env.VITE_GLITCHTIP_API_KEY) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: import.meta.env.VITE_GLITCHTIP_API_KEY,
|
dsn: import.meta.env.VITE_GLITCHTIP_API_KEY,
|
||||||
tracesSampleRate: 0.01,
|
beforeSend(event: any) {
|
||||||
|
if (!getSetting("report_errors")) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
},
|
||||||
integrations(integrations) {
|
integrations(integrations) {
|
||||||
return integrations.filter(integration => integration.name !== "Breadcrumbs")
|
return integrations.filter(integration => integration.name !== "Breadcrumbs")
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<div>{subtitle}</div>
|
<div>{subtitle}</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>{message}</p>
|
<p class="text-center">{message}</p>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
|
|||||||
@@ -12,11 +12,7 @@
|
|||||||
|
|
||||||
const pad = (n: number) => ("00" + String(n)).slice(-2)
|
const pad = (n: number) => ("00" + String(n)).slice(-2)
|
||||||
|
|
||||||
const getTime = (d: Date, inheritMinutes: boolean) => {
|
const getTime = (d: Date) => `${pad(d.getHours())}:${minutes}`
|
||||||
const minutes = inheritMinutes ? pad(d.getMinutes()) : "00"
|
|
||||||
|
|
||||||
return `${pad(d.getHours())}:${minutes}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const setTime = (d: Date, time: string) => {
|
const setTime = (d: Date, time: string) => {
|
||||||
const [hours, minutes] = time.split(":").map(x => parseInt(x))
|
const [hours, minutes] = time.split(":").map(x => parseInt(x))
|
||||||
@@ -29,6 +25,7 @@
|
|||||||
|
|
||||||
const onTimeChange = () => {
|
const onTimeChange = () => {
|
||||||
if (time) {
|
if (time) {
|
||||||
|
minutes = time.slice(-2)
|
||||||
date = setTime(date || new Date(), time)
|
date = setTime(date || new Date(), time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,12 +39,13 @@
|
|||||||
|
|
||||||
let date: Date | undefined = $state()
|
let date: Date | undefined = $state()
|
||||||
let time: string | undefined = $state()
|
let time: string | undefined = $state()
|
||||||
|
let minutes: string = $state("00")
|
||||||
let element: HTMLElement
|
let element: HTMLElement
|
||||||
|
|
||||||
// Sync date to time and value
|
// Sync date to time and value
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (date) {
|
if (date) {
|
||||||
time = getTime(date, false)
|
time = getTime(date)
|
||||||
value = dateToSeconds(date)
|
value = dateToSeconds(date)
|
||||||
} else {
|
} else {
|
||||||
value = undefined
|
value = undefined
|
||||||
@@ -57,7 +55,7 @@
|
|||||||
// Sync updates to value to date/time
|
// Sync updates to value to date/time
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const derivedDate = value ? secondsToDate(value) : undefined
|
const derivedDate = value ? secondsToDate(value) : undefined
|
||||||
const derivedTime = derivedDate ? getTime(derivedDate, true) : undefined
|
const derivedTime = derivedDate ? getTime(derivedDate) : undefined
|
||||||
|
|
||||||
date = derivedDate
|
date = derivedDate
|
||||||
time = derivedTime
|
time = derivedTime
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
const {children, onLongPress, ...restProps} = $props()
|
|
||||||
|
|
||||||
const ontouchstart = (event: any) => {
|
|
||||||
touch = event.touches[0]
|
|
||||||
timeout = setTimeout(onLongPress, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ontouchmove = (event: any) => {
|
|
||||||
const curTouch = event.touches[0]
|
|
||||||
|
|
||||||
if (Math.abs(curTouch.clientX - touch.clientX) > 30) {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(curTouch.clientY - touch.clientY) > 30) {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ontouchend = () => clearTimeout(timeout)
|
|
||||||
|
|
||||||
let touch: Touch
|
|
||||||
let timeout: any
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div role="button" tabindex="0" {ontouchstart} {ontouchmove} {ontouchend} {...restProps}>
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
const {...props}: Props = $props()
|
const {...props}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative z-feature mx-2 rounded-xl pt-4 {props.class}">
|
<div class="relative z-feature rounded-xl px-2 pt-2 {props.class}">
|
||||||
<div
|
<div
|
||||||
class="flex min-h-12 items-center justify-between gap-4 rounded-xl bg-base-100 px-4 shadow-xl">
|
class="flex min-h-12 items-center justify-between gap-4 rounded-xl bg-base-100 px-4 shadow-xl">
|
||||||
<div class="flex items-center gap-4">
|
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
|
||||||
{@render props.icon?.()}
|
{@render props.icon?.()}
|
||||||
{@render props.title?.()}
|
{@render props.title?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div transition:fly|local={{duration: 200}} class="tiptap-suggestions">
|
<div transition:fly|local={{duration: 200}} class="tiptap-suggestions">
|
||||||
<div class="tiptap-suggestions__content">
|
<div class="tiptap-suggestions__content max-h-[40vh]">
|
||||||
{#if $term && allowCreate && !items.includes($term)}
|
{#if $term && allowCreate && !items.includes($term)}
|
||||||
<button
|
<button
|
||||||
class="tiptap-suggestions__create"
|
class="tiptap-suggestions__create"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {isMobile} from "@lib/html"
|
||||||
|
|
||||||
|
const {children, onTap, ...restProps} = $props()
|
||||||
|
|
||||||
|
const onclick = (event: MouseEvent) => {
|
||||||
|
if (isMobile) {
|
||||||
|
onTap(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div role="button" tabindex="0" {onclick} {...restProps}>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
@@ -76,3 +76,16 @@ export const createScroller = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isMobile = "ontouchstart" in document.documentElement
|
export const isMobile = "ontouchstart" in document.documentElement
|
||||||
|
|
||||||
|
export const downloadText = (filename: string, text: string) => {
|
||||||
|
const blob = new Blob([text], {type: "text/plain"})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,8 +67,8 @@
|
|||||||
import {nsecDecode} from "@lib/util"
|
import {nsecDecode} from "@lib/util"
|
||||||
import {theme} from "@app/theme"
|
import {theme} from "@app/theme"
|
||||||
import {INDEXER_RELAYS, userMembership, ensureUnwrapped, canDecrypt} from "@app/state"
|
import {INDEXER_RELAYS, userMembership, ensureUnwrapped, canDecrypt} from "@app/state"
|
||||||
import {loadUserData, loginWithNip46} from "@app/commands"
|
import {loadUserData, listenForNotifications} from "@app/requests"
|
||||||
import {listenForNotifications} from "@app/requests"
|
import {loginWithNip46} from "@app/commands"
|
||||||
import * as commands from "@app/commands"
|
import * as commands from "@app/commands"
|
||||||
import * as requests from "@app/requests"
|
import * as requests from "@app/requests"
|
||||||
import * as notifications from "@app/notifications"
|
import * as notifications from "@app/notifications"
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {addToMapKey, dec, gt} from "@welshman/lib"
|
import {addToMapKey, dec, gt} from "@welshman/lib"
|
||||||
import type {Relay} from "@welshman/app"
|
import type {Relay} from "@welshman/app"
|
||||||
import {relays, createSearch} from "@welshman/app"
|
import {relays, createSearch, loadRelay, loadRelaySelections} from "@welshman/app"
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Page from "@lib/components/Page.svelte"
|
import Page from "@lib/components/Page.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -14,15 +15,26 @@
|
|||||||
import SpaceCheck from "@app/components/SpaceCheck.svelte"
|
import SpaceCheck from "@app/components/SpaceCheck.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import {
|
import {
|
||||||
memberships,
|
|
||||||
membershipByPubkey,
|
membershipByPubkey,
|
||||||
getMembershipUrls,
|
getMembershipUrls,
|
||||||
|
loadMembership,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
getDefaultPubkeys,
|
getDefaultPubkeys,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {discoverRelays} from "@app/commands"
|
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const discoverRelays = () =>
|
||||||
|
Promise.all(
|
||||||
|
getDefaultPubkeys().map(async pubkey => {
|
||||||
|
await loadRelaySelections(pubkey)
|
||||||
|
|
||||||
|
const membership = await loadMembership(pubkey)
|
||||||
|
const urls = getMembershipUrls(membership)
|
||||||
|
|
||||||
|
await Promise.all(urls.map(url => loadRelay(url)))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const wotGraph = $derived.by(() => {
|
const wotGraph = $derived.by(() => {
|
||||||
const scores = new Map<string, Set<string>>()
|
const scores = new Map<string, Set<string>>()
|
||||||
|
|
||||||
@@ -36,20 +48,23 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
const relaySearch = $derived(
|
const relaySearch = $derived(
|
||||||
createSearch($relays, {
|
createSearch(
|
||||||
getValue: (relay: Relay) => relay.url,
|
$relays.filter(r => wotGraph.has(r.url)),
|
||||||
sortFn: ({score, item}) => {
|
{
|
||||||
if (score && score > 0.1) return -score!
|
getValue: (relay: Relay) => relay.url,
|
||||||
|
sortFn: ({score, item}) => {
|
||||||
|
if (score && score > 0.1) return -score!
|
||||||
|
|
||||||
const wotScore = wotGraph.get(item.url)?.size || 0
|
const wotScore = wotGraph.get(item.url)?.size || 0
|
||||||
|
|
||||||
return score ? dec(score) * wotScore : -wotScore
|
return score ? dec(score) * wotScore : -wotScore
|
||||||
|
},
|
||||||
|
fuseOptions: {
|
||||||
|
keys: ["url", "name", {name: "description", weight: 0.3}],
|
||||||
|
shouldSort: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fuseOptions: {
|
),
|
||||||
keys: ["url", "name", {name: "description", weight: 0.3}],
|
|
||||||
shouldSort: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const openSpace = (url: string) => pushModal(SpaceCheck, {url})
|
const openSpace = (url: string) => pushModal(SpaceCheck, {url})
|
||||||
@@ -128,9 +143,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
{#await discoverRelays($memberships)}
|
{#await discoverRelays()}
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center py-20" out:fly>
|
||||||
<Spinner loading>Loading more relays...</Spinner>
|
<Spinner loading>Looking for spaces...</Spinner>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !PLATFORM_RELAY}
|
{#if !PLATFORM_RELAY}
|
||||||
<div class="hero min-h-screen">
|
<div class="hero min-h-screen overflow-auto pb-8">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="column content gap-4">
|
<div class="column content gap-4">
|
||||||
<h1 class="text-center text-5xl">Welcome to</h1>
|
<h1 class="text-center text-5xl">Welcome to</h1>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<form class="content column gap-4" {onsubmit}>
|
<form class="content column gap-4" {onsubmit}>
|
||||||
<div class="card2 bg-alt col-4 shadow-xl">
|
<div class="card2 bg-alt col-4 shadow-xl">
|
||||||
<p class="text-lg">Content Settings</p>
|
<strong class="text-lg">Content Settings</strong>
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Hide sensitive content?</p>
|
<p>Hide sensitive content?</p>
|
||||||
@@ -77,7 +77,37 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
<p class="text-lg">Editor Settings</p>
|
<strong class="text-lg">Privacy Settings</strong>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Report errors?</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-primary"
|
||||||
|
bind:checked={settings.report_errors} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>
|
||||||
|
Allow {PLATFORM_NAME} to send error reports to help improve the app.
|
||||||
|
</p>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Report usage?</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.report_usage} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>
|
||||||
|
Allow {PLATFORM_NAME} to collect anonymous usage data.
|
||||||
|
</p>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<strong class="text-lg">Editor Settings</strong>
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Send Delay</p>
|
<p>Send Delay</p>
|
||||||
@@ -119,7 +149,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet info()}
|
{#snippet info()}
|
||||||
<p>Choose a media server type and url for files you upload to flotilla.</p>
|
<p>Choose a media server type and url for files you upload to {PLATFORM_NAME}.</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
import {hexToBytes} from "@noble/hashes/utils"
|
import {hexToBytes} from "@noble/hashes/utils"
|
||||||
import {displayPubkey, displayProfile} from "@welshman/util"
|
import {displayPubkey, displayProfile} from "@welshman/util"
|
||||||
import {pubkey, session, displayNip05, deriveProfile} from "@welshman/app"
|
import {pubkey, session, displayNip05, deriveProfile} from "@welshman/app"
|
||||||
|
import {slideAndFade} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
||||||
|
import ProfileDelete from "@app/components/ProfileDelete.svelte"
|
||||||
import InfoKeys from "@app/components/InfoKeys.svelte"
|
import InfoKeys from "@app/components/InfoKeys.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
@@ -25,6 +27,10 @@
|
|||||||
const startEdit = () => pushModal(ProfileEdit)
|
const startEdit = () => pushModal(ProfileEdit)
|
||||||
|
|
||||||
const startEject = () => pushModal(InfoKeys)
|
const startEject = () => pushModal(InfoKeys)
|
||||||
|
|
||||||
|
const startDelete = () => pushModal(ProfileDelete)
|
||||||
|
|
||||||
|
let showAdvanced = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="content column gap-4">
|
<div class="content column gap-4">
|
||||||
@@ -77,7 +83,10 @@
|
|||||||
<div class="card2 bg-alt col-4 shadow-xl">
|
<div class="card2 bg-alt col-4 shadow-xl">
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Public Key</p>
|
<p class="flex items-center gap-3">
|
||||||
|
<Icon icon="key" />
|
||||||
|
Public Key
|
||||||
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<label class="input input-bordered flex w-full items-center justify-between gap-2">
|
<label class="input input-bordered flex w-full items-center justify-between gap-2">
|
||||||
@@ -100,7 +109,10 @@
|
|||||||
{#if $session?.method === "nip01"}
|
{#if $session?.method === "nip01"}
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Private Key</p>
|
<p class="flex items-center gap-3">
|
||||||
|
<Icon icon="key" />
|
||||||
|
Private Key
|
||||||
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
@@ -117,4 +129,27 @@
|
|||||||
</FieldInline>
|
</FieldInline>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card2 bg-alt shadow-xl">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<strong class="flex items-center gap-3">
|
||||||
|
<Icon icon="settings" />
|
||||||
|
Advanced
|
||||||
|
</strong>
|
||||||
|
<Button onclick={() => (showAdvanced = !showAdvanced)}>
|
||||||
|
{#if showAdvanced}
|
||||||
|
<Icon icon="alt-arrow-down" />
|
||||||
|
{:else}
|
||||||
|
<Icon icon="alt-arrow-up" />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{#if showAdvanced}
|
||||||
|
<div transition:slideAndFade class="flex flex-col gap-2 pt-4">
|
||||||
|
<Button class="btn btn-outline btn-error" onclick={startDelete}>
|
||||||
|
<Icon icon="trash-bin-2" />
|
||||||
|
Delete your profile
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
import RelayItem from "@app/components/RelayItem.svelte"
|
import RelayItem from "@app/components/RelayItem.svelte"
|
||||||
import RelayAdd from "@app/components/RelayAdd.svelte"
|
import RelayAdd from "@app/components/RelayAdd.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {setRelayPolicy, discoverRelays, setInboxRelayPolicy} from "@app/commands"
|
import {discoverRelays} from "@app/requests"
|
||||||
|
import {setRelayPolicy, setInboxRelayPolicy} from "@app/commands"
|
||||||
|
|
||||||
const readRelayUrls = derived(userRelaySelections, getReadRelayUrls)
|
const readRelayUrls = derived(userRelaySelections, getReadRelayUrls)
|
||||||
const writeRelayUrls = derived(userRelaySelections, getWriteRelayUrls)
|
const writeRelayUrls = derived(userRelaySelections, getWriteRelayUrls)
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<Divider>Your Rooms</Divider>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<Link href={threadsPath} class="btn btn-primary">
|
<Link href={threadsPath} class="btn btn-primary">
|
||||||
<div class="relative flex items-center gap-2">
|
<div class="relative flex items-center gap-2">
|
||||||
@@ -167,6 +168,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Link>
|
</Link>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Divider>Other Rooms</Divider>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
{#each $otherRooms as room (room)}
|
{#each $otherRooms as room (room)}
|
||||||
<Link href={makeRoomPath(url, room)} class="btn btn-neutral">
|
<Link href={makeRoomPath(url, room)} class="btn btn-neutral">
|
||||||
<div class="relative flex min-w-0 items-center gap-2 overflow-hidden text-nowrap">
|
<div class="relative flex min-w-0 items-center gap-2 overflow-hidden text-nowrap">
|
||||||
|
|||||||
@@ -46,8 +46,6 @@
|
|||||||
const filter = {kinds: [MESSAGE], "#h": [room]}
|
const filter = {kinds: [MESSAGE], "#h": [room]}
|
||||||
const relay = deriveRelay(url)
|
const relay = deriveRelay(url)
|
||||||
|
|
||||||
const assertEvent = (e: any) => e as TrustedEvent
|
|
||||||
|
|
||||||
const joinRoom = async () => {
|
const joinRoom = async () => {
|
||||||
if (hasNip29($relay)) {
|
if (hasNip29($relay)) {
|
||||||
joiningRoom = true
|
joiningRoom = true
|
||||||
@@ -136,6 +134,8 @@
|
|||||||
let parent: TrustedEvent | undefined = $state()
|
let parent: TrustedEvent | undefined = $state()
|
||||||
let element: HTMLElement | undefined = $state()
|
let element: HTMLElement | undefined = $state()
|
||||||
let newMessages: HTMLElement | undefined = $state()
|
let newMessages: HTMLElement | undefined = $state()
|
||||||
|
let chatCompose: HTMLElement | undefined = $state()
|
||||||
|
let dynamicPadding: HTMLElement | undefined = $state()
|
||||||
let newMessagesSeen = false
|
let newMessagesSeen = false
|
||||||
let showFixedNewMessages = $state(false)
|
let showFixedNewMessages = $state(false)
|
||||||
let showScrollButton = $state(false)
|
let showScrollButton = $state(false)
|
||||||
@@ -210,6 +210,16 @@
|
|||||||
loadingEvents = false
|
loadingEvents = false
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
dynamicPadding!.style.minHeight = `${chatCompose!.offsetHeight}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(chatCompose!)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(chatCompose!)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@@ -218,84 +228,80 @@
|
|||||||
// Sveltekit calls onDestroy at the beginning of the page load for some reason
|
// Sveltekit calls onDestroy at the beginning of the page load for some reason
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setChecked($page.url.pathname)
|
setChecked($page.url.pathname)
|
||||||
}, 300)
|
}, 800)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative flex h-full flex-col">
|
<PageBar class="chat__page-bar">
|
||||||
<PageBar>
|
{#snippet icon()}
|
||||||
{#snippet icon()}
|
<div class="center">
|
||||||
<div class="center">
|
<Icon icon="hashtag" />
|
||||||
<Icon icon="hashtag" />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title()}
|
|
||||||
<strong>
|
|
||||||
<ChannelName {url} {room} />
|
|
||||||
</strong>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet action()}
|
|
||||||
<div class="row-2">
|
|
||||||
{#if room !== GENERAL}
|
|
||||||
{#if $userRoomsByUrl.get(url)?.has(room)}
|
|
||||||
<Button class="btn btn-neutral btn-sm" onclick={leaveRoom}>
|
|
||||||
<Icon icon="arrows-a-logout-2" />
|
|
||||||
Leave Room
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<Button class="btn btn-neutral btn-sm" disabled={joiningRoom} onclick={joinRoom}>
|
|
||||||
{#if joiningRoom}
|
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
|
||||||
{:else}
|
|
||||||
<Icon icon="login-2" />
|
|
||||||
{/if}
|
|
||||||
Join Room
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
<MenuSpaceButton {url} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</PageBar>
|
|
||||||
<div
|
|
||||||
class="scroll-container -mt-2 flex flex-grow flex-col-reverse overflow-y-auto overflow-x-hidden py-2"
|
|
||||||
onscroll={onScroll}
|
|
||||||
bind:this={element}>
|
|
||||||
{#each elements as { type, id, value, showPubkey } (id)}
|
|
||||||
{#if type === "new-messages"}
|
|
||||||
<div
|
|
||||||
bind:this={newMessages}
|
|
||||||
class="flex items-center py-2 text-xs transition-colors"
|
|
||||||
class:opacity-0={showFixedNewMessages}>
|
|
||||||
<div class="h-px flex-grow bg-primary"></div>
|
|
||||||
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
|
|
||||||
<div class="h-px flex-grow bg-primary"></div>
|
|
||||||
</div>
|
|
||||||
{:else if type === "date"}
|
|
||||||
<Divider>{value}</Divider>
|
|
||||||
{:else}
|
|
||||||
<div in:slide class:-mt-1={!showPubkey}>
|
|
||||||
<ChannelMessage {url} {room} {replyTo} event={assertEvent(value)} {showPubkey} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
<p class="flex h-10 items-center justify-center py-20">
|
|
||||||
{#if loadingEvents}
|
|
||||||
<Spinner loading={loadingEvents}>Looking for messages...</Spinner>
|
|
||||||
{:else}
|
|
||||||
<Spinner>End of message history</Spinner>
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{#if showFixedNewMessages}
|
|
||||||
<div class="relative z-feature flex justify-center">
|
|
||||||
<div transition:fly={{duration: 200}} class="fixed top-12">
|
|
||||||
<Button class="btn btn-primary btn-xs rounded-full" onclick={scrollToNewMessages}>
|
|
||||||
New Messages
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
|
<strong class="ellipsize">
|
||||||
|
<ChannelName {url} {room} />
|
||||||
|
</strong>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet action()}
|
||||||
|
<div class="row-2">
|
||||||
|
{#if room !== GENERAL}
|
||||||
|
{#if $userRoomsByUrl.get(url)?.has(room)}
|
||||||
|
<Button class="btn btn-neutral btn-sm" onclick={leaveRoom}>
|
||||||
|
<Icon icon="arrows-a-logout-2" />
|
||||||
|
Leave Room
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button class="btn btn-neutral btn-sm" disabled={joiningRoom} onclick={joinRoom}>
|
||||||
|
{#if joiningRoom}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon="login-2" />
|
||||||
|
{/if}
|
||||||
|
Join Room
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
<MenuSpaceButton {url} />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</PageBar>
|
||||||
|
|
||||||
|
<div class="chat__messages scroll-container" onscroll={onScroll} bind:this={element}>
|
||||||
|
<div bind:this={dynamicPadding}></div>
|
||||||
|
{#each elements as { type, id, value, showPubkey } (id)}
|
||||||
|
{#if type === "new-messages"}
|
||||||
|
<div
|
||||||
|
bind:this={newMessages}
|
||||||
|
class="flex items-center py-2 text-xs transition-colors"
|
||||||
|
class:opacity-0={showFixedNewMessages}>
|
||||||
|
<div class="h-px flex-grow bg-primary"></div>
|
||||||
|
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
|
||||||
|
<div class="h-px flex-grow bg-primary"></div>
|
||||||
|
</div>
|
||||||
|
{:else if type === "date"}
|
||||||
|
<Divider>{value}</Divider>
|
||||||
|
{:else}
|
||||||
|
<div in:slide class:-mt-1={!showPubkey}>
|
||||||
|
<ChannelMessage
|
||||||
|
{url}
|
||||||
|
{room}
|
||||||
|
{replyTo}
|
||||||
|
event={$state.snapshot(value as TrustedEvent)}
|
||||||
|
{showPubkey} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<p class="flex h-10 items-center justify-center py-20">
|
||||||
|
{#if loadingEvents}
|
||||||
|
<Spinner loading={loadingEvents}>Looking for messages...</Spinner>
|
||||||
|
{:else}
|
||||||
|
<Spinner>End of message history</Spinner>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
||||||
<div>
|
<div>
|
||||||
{#if parent}
|
{#if parent}
|
||||||
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||||
@@ -303,14 +309,24 @@
|
|||||||
{#if share}
|
{#if share}
|
||||||
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
|
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
|
||||||
{/if}
|
{/if}
|
||||||
<ChannelCompose bind:this={compose} {onSubmit} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<ChannelCompose bind:this={compose} {onSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showScrollButton}
|
{#if showScrollButton}
|
||||||
<div in:fade class="fixed bottom-14 right-4">
|
<div in:fade class="chat__scroll-down">
|
||||||
<Button class="btn btn-circle btn-neutral" onclick={scrollToBottom}>
|
<Button class="btn btn-circle btn-neutral" onclick={scrollToBottom}>
|
||||||
<Icon icon="alt-arrow-down" />
|
<Icon icon="alt-arrow-down" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showFixedNewMessages}
|
||||||
|
<div class="relative z-feature flex justify-center">
|
||||||
|
<div transition:fly={{duration: 200}} class="fixed top-12">
|
||||||
|
<Button class="btn btn-primary btn-xs rounded-full" onclick={scrollToNewMessages}>
|
||||||
|
New Messages
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -42,8 +42,6 @@
|
|||||||
return sortBy(e => -max([scores.get(e.id), e.created_at]), threads)
|
return sortBy(e => -max([scores.get(e.id), e.created_at]), threads)
|
||||||
})
|
})
|
||||||
|
|
||||||
$inspect({threads, comments, events})
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const {cleanup} = makeFeed({
|
const {cleanup} = makeFeed({
|
||||||
element: element!,
|
element: element!,
|
||||||
@@ -98,7 +96,7 @@
|
|||||||
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2">
|
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2">
|
||||||
{#each events as event (event.id)}
|
{#each events as event (event.id)}
|
||||||
<div in:fly>
|
<div in:fly>
|
||||||
<ThreadItem {url} {event} />
|
<ThreadItem {url} event={$state.snapshot(event)} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<p class="flex h-10 items-center justify-center py-20">
|
<p class="flex h-10 items-center justify-center py-20">
|
||||||
|
|||||||
Reference in New Issue
Block a user