Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3c65c3970 | |||
| a5b868cd56 | |||
| 8fcc56a408 | |||
| c8dfbc936b | |||
| f1e76a1ed1 | |||
| 6ecc3e6770 | |||
| b05c408977 | |||
| e484c3cb00 | |||
| 69d0e11ba4 | |||
| 27d9d4fff1 | |||
| c089812363 | |||
| 07dd1e97dc | |||
| 7f6a1bff34 | |||
| 7d1310722a | |||
| cb57710654 | |||
| c74c116667 | |||
| 0ba55f2387 | |||
| 622214713b | |||
| d8cf48381b | |||
| 7dc7b5abeb |
@@ -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_PLATFORM_URL=https://flotilla.social
|
||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
# 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
|
||||
|
||||
* Show spinner when joining a room
|
||||
* Reduce self-rate limiting of REQs
|
||||
* Fix disabled signers link
|
||||
* Prepare for iOS release
|
||||
* Improve threads and calendar pages
|
||||
* Improve quote rendering and new messages button
|
||||
|
||||
# 0.2.7
|
||||
|
||||
* Add calendar events
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "social.flotilla"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 7
|
||||
versionName "0.2.7"
|
||||
versionCode 10
|
||||
versionName "0.2.11"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ const config: CapacitorConfig = {
|
||||
},
|
||||
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
||||
// server: {
|
||||
// url: "http://192.168.1.251:1847",
|
||||
// url: "http://192.168.1.250:1847",
|
||||
// cleartext: true
|
||||
// },
|
||||
};
|
||||
|
||||
@@ -351,12 +351,14 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 0.2.11;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -374,12 +376,14 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 0.2.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Flotilla</string>
|
||||
<string>Flotilla</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -30,6 +30,8 @@
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string></string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
@@ -45,5 +47,7 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Generated
+44
-30
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flotilla",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.11",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/app": "^7.0.0",
|
||||
@@ -22,15 +22,15 @@
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.6",
|
||||
"@welshman/app": "~0.0.42",
|
||||
"@welshman/content": "~0.0.17",
|
||||
"@welshman/content": "~0.0.18",
|
||||
"@welshman/dvm": "~0.0.14",
|
||||
"@welshman/editor": "~0.0.13",
|
||||
"@welshman/editor": "~0.0.15",
|
||||
"@welshman/feeds": "~0.0.30",
|
||||
"@welshman/lib": "~0.0.40",
|
||||
"@welshman/net": "~0.0.46",
|
||||
"@welshman/lib": "~0.0.41",
|
||||
"@welshman/net": "~0.0.47",
|
||||
"@welshman/signer": "~0.0.20",
|
||||
"@welshman/store": "~0.0.16",
|
||||
"@welshman/util": "~0.0.60",
|
||||
"@welshman/util": "~0.0.61",
|
||||
"daisyui": "^4.12.10",
|
||||
"date-picker-svelte": "^2.13.0",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -4766,13 +4766,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/content": {
|
||||
"version": "0.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.17.tgz",
|
||||
"integrity": "sha512-xiBSL8BSzHrwRmGqKXkR/S6EK7a1wT1rG1qdlQN30lBX5ZS+NSkoI0aNuF8p313mElHNZWgrqxFaat+FML4yOw==",
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.18.tgz",
|
||||
"integrity": "sha512-7LHs9xKStrkaet9VY1PWSEUWrdIaIThIo+ByN6lF3nRZwPTExrBy4rPXnEa5roVAAwgmlhXw3zTkfGP15V6joQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.2",
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"@welshman/lib": "~0.0.40",
|
||||
"nostr-tools": "^2.7.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4793,9 +4793,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/editor": {
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.13.tgz",
|
||||
"integrity": "sha512-860kn8iOXHKGBOnL3zalFQVw8eeILNU6YQ4V+xFtgqIxxCMk1c/9F5k0k0OyloUqRNjtSG6hvLdQLacBvhz2WQ==",
|
||||
"version": "0.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.15.tgz",
|
||||
"integrity": "sha512-Eg3alzv+cjCXtr6oEItRqoRSD4DTllt3c2JyJTxpF/KNiy8XHHMeUSpVFgph3+pAt5jwyl6b1feKPEwpShgqHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^2.11.5",
|
||||
@@ -4813,11 +4813,25 @@
|
||||
"@tiptap/suggestion": "^2.11.5",
|
||||
"@welshman/lib": "~0.0.40",
|
||||
"@welshman/util": "^0.0.60",
|
||||
"nostr-editor": "^0.0.4-pre.12",
|
||||
"nostr-editor": "^0.0.4-pre.13",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"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": {
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.0.30.tgz",
|
||||
@@ -4829,9 +4843,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/lib": {
|
||||
"version": "0.0.40",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.40.tgz",
|
||||
"integrity": "sha512-6Qk5fJABv+7HPqhNC5eLM4VZxCLpcu22nShmrNMbamkMwr4eLj2Bl4dRmuzFsvMcsL/Jc148zqpfuq37CY2NCw==",
|
||||
"version": "0.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.41.tgz",
|
||||
"integrity": "sha512-FMJVoPZw8Vi1fd2/ulwqlBS1tvjkFAm9lg+Dz5SXItXxrNC06YMRTjGjInCBEkArrvNGPUjchzSFDNmbH0fxHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@scure/base": "^1.1.6",
|
||||
@@ -4840,13 +4854,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/net": {
|
||||
"version": "0.0.46",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.46.tgz",
|
||||
"integrity": "sha512-ehH4grz0VHjuofyVUE3r5GoynHTh+cIT/XFH6ov6nOGRU/LZXCLGk/9CUPlqNYHRfc/zBtaIyfVu0AelLqV6lw==",
|
||||
"version": "0.0.47",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.47.tgz",
|
||||
"integrity": "sha512-/mIr+QyLH+RlD16rsPDTIW250lOm5eNaLO6dhZw8dMKznMhVtSWe/X/lJZOXmexzbB2z7WYZVN5x5TggZROyxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"@welshman/util": "~0.0.58",
|
||||
"@welshman/lib": "~0.0.40",
|
||||
"@welshman/util": "~0.0.59",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
@@ -4917,13 +4931,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/util": {
|
||||
"version": "0.0.60",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.60.tgz",
|
||||
"integrity": "sha512-kqZgYnrwxKx0JTDZnTSaQYc2ev7E9ZjNDy5MclX36d5T/qPUspmwksAOodFJY9kJoJd49bf1omAmBTgnFJfeHw==",
|
||||
"version": "0.0.61",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.61.tgz",
|
||||
"integrity": "sha512-+l4YX01msAtnyylzpIFIAYubvnBLyr9hGx3iRO5LS3OPv/yUDOeyYJseWDqorkIiN5BRT7PCgnWJdlQP71ZtAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ws": "^8.5.13",
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"@welshman/lib": "~0.0.40",
|
||||
"nostr-tools": "^2.7.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -10152,9 +10166,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-editor": {
|
||||
"version": "0.0.4-pre.12",
|
||||
"resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.4-pre.12.tgz",
|
||||
"integrity": "sha512-vztmbEKxt2jnO1JEoprwVf3s4TN4D3B0fcsrhckOITR1KaDX88QhIG+qTee92xp+n96vYj4GQt0W06rSv3NXHA==",
|
||||
"version": "0.0.4-pre.13",
|
||||
"resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.4-pre.13.tgz",
|
||||
"integrity": "sha512-izIidrrIjQp41MAY2dNoticQSc0E5XOFKEe04tmZdTdF9Ry8CKxIdv6yvO3qh4gdhrOq+QPLTRii6X3X5iC/5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"light-bolt11-decoder": "^3.1.1"
|
||||
|
||||
+8
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "./build.sh",
|
||||
"sourcemaps": "./sourcemaps.sh",
|
||||
"release:android": "cap build android --androidreleasetype APK --signing-type apksigner",
|
||||
"sourcemaps": "./build.sh && ./sourcemaps.sh",
|
||||
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check src && eslint src",
|
||||
@@ -51,15 +51,15 @@
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.6",
|
||||
"@welshman/app": "~0.0.42",
|
||||
"@welshman/content": "~0.0.17",
|
||||
"@welshman/content": "~0.0.18",
|
||||
"@welshman/dvm": "~0.0.14",
|
||||
"@welshman/editor": "~0.0.13",
|
||||
"@welshman/editor": "~0.0.15",
|
||||
"@welshman/feeds": "~0.0.30",
|
||||
"@welshman/lib": "~0.0.40",
|
||||
"@welshman/net": "~0.0.46",
|
||||
"@welshman/lib": "~0.0.41",
|
||||
"@welshman/net": "~0.0.47",
|
||||
"@welshman/signer": "~0.0.20",
|
||||
"@welshman/store": "~0.0.16",
|
||||
"@welshman/util": "~0.0.60",
|
||||
"@welshman/util": "~0.0.61",
|
||||
"daisyui": "^4.12.10",
|
||||
"date-picker-svelte": "^2.13.0",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
@@ -323,3 +323,9 @@ emoji-picker {
|
||||
--input-font-color: var(--base-content);
|
||||
--outline-color: var(--base-100);
|
||||
}
|
||||
|
||||
/* progress */
|
||||
|
||||
progress[value]::-webkit-progress-value {
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
+3
-52
@@ -1,6 +1,6 @@
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {get} from "svelte/store"
|
||||
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
||||
import {ctx, uniq, equals} from "@welshman/lib"
|
||||
import {
|
||||
DELETE,
|
||||
REPORT,
|
||||
@@ -26,12 +26,10 @@ import {
|
||||
getTag,
|
||||
getListTags,
|
||||
getRelayTags,
|
||||
isShareableRelayUrl,
|
||||
getRelayTagValues,
|
||||
toNostrURI,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, EventContent, EventTemplate, List} from "@welshman/util"
|
||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import type {TrustedEvent, EventContent, EventTemplate} from "@welshman/util"
|
||||
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
||||
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
||||
import {
|
||||
@@ -40,13 +38,9 @@ import {
|
||||
repository,
|
||||
publishThunk,
|
||||
publishThunks,
|
||||
loadProfile,
|
||||
loadInboxRelaySelections,
|
||||
profilesByPubkey,
|
||||
relaySelectionsByPubkey,
|
||||
getWriteRelayUrls,
|
||||
loadFollows,
|
||||
loadMutes,
|
||||
tagEvent,
|
||||
tagEventForReaction,
|
||||
getRelayUrls,
|
||||
@@ -67,11 +61,9 @@ import {
|
||||
userMembership,
|
||||
INDEXER_RELAYS,
|
||||
NIP46_PERMS,
|
||||
loadMembership,
|
||||
loadSettings,
|
||||
getDefaultPubkeys,
|
||||
userRoomsByUrl,
|
||||
} from "@app/state"
|
||||
import {loadUserData} from "@app/requests"
|
||||
|
||||
// Utils
|
||||
|
||||
@@ -161,47 +153,6 @@ export const logout = async () => {
|
||||
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
|
||||
|
||||
export const broadcastUserData = async (relays: string[]) => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import {fromPairs} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {LOCALE, secondsToDate} from "@welshman/app"
|
||||
|
||||
type Props = {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {event}: Props = $props()
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
const startDate = $derived(secondsToDate(parseInt(meta.start)))
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex h-14 w-14 flex-col items-center justify-center gap-1 rounded-box border border-solid border-base-content p-2 sm:h-24 sm:w-24">
|
||||
<span class="sm:text-lg">{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</span>
|
||||
<span class="sm:text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {fromPairs} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {formatTimestamp, formatTimestampAsDate, formatTimestampAsTime} from "@welshman/app"
|
||||
|
||||
type Props = {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {event}: Props = $props()
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
const start = $derived(parseInt(meta.start))
|
||||
const end = $derived(parseInt(meta.end))
|
||||
const startDateDisplay = $derived(formatTimestampAsDate(start))
|
||||
const endDateDisplay = $derived(formatTimestampAsDate(end))
|
||||
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
|
||||
</script>
|
||||
|
||||
<p class="text-xl">{meta.title || meta.name}</p>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon="clock-circle" size={4} />
|
||||
{formatTimestampAsTime(start)} — {isSingleDay ? formatTimestampAsTime(end) : formatTimestamp(end)}
|
||||
</div>
|
||||
@@ -1,38 +1,22 @@
|
||||
<script lang="ts">
|
||||
import {fromPairs} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {formatTimestamp, formatTimestampAsDate, formatTimestampAsTime} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import {makeCalendarPath} from "@app/routes"
|
||||
|
||||
const {
|
||||
url,
|
||||
event,
|
||||
}: {
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
} = $props()
|
||||
}
|
||||
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
const end = $derived(parseInt(meta.end))
|
||||
const start = $derived(parseInt(meta.start))
|
||||
const startDateDisplay = $derived(formatTimestampAsDate(start))
|
||||
const endDateDisplay = $derived(formatTimestampAsDate(end))
|
||||
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
|
||||
const {url, event}: Props = $props()
|
||||
</script>
|
||||
|
||||
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<p class="text-xl">{meta.title || meta.name}</p>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon="clock-circle" size={4} />
|
||||
{formatTimestampAsTime(start)} — {isSingleDay
|
||||
? formatTimestampAsTime(end)
|
||||
: formatTimestamp(end)}
|
||||
</div>
|
||||
<CalendarEventHeader {event} />
|
||||
</div>
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {fromPairs} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
|
||||
type Props = {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {event}: Props = $props()
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
</script>
|
||||
|
||||
<span>
|
||||
Posted by <ProfileLink pubkey={event.pubkey} />
|
||||
</span>
|
||||
{#if meta.location}
|
||||
<span>•</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon icon="map-point" size={4} />
|
||||
{meta.location}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -4,7 +4,7 @@
|
||||
import {slide} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
|
||||
const {
|
||||
verb,
|
||||
@@ -22,7 +22,12 @@
|
||||
transition:slide>
|
||||
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
{#key event.id}
|
||||
<Content {event} hideMedia minLength={100} maxLength={300} expandMode="disabled" />
|
||||
<NoteContent
|
||||
{event}
|
||||
hideMediaAtDepth={0}
|
||||
minLength={100}
|
||||
maxLength={300}
|
||||
expandMode="disabled" />
|
||||
{/key}
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||
<Icon icon="close-circle" />
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-sm">
|
||||
<Content {event} quoteProps={{minimal: true, relays: [url]}} />
|
||||
<Content {event} relays={[url]} />
|
||||
{#if thunk}
|
||||
<ThunkStatus {thunk} class="mt-2" />
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {fromNostrURI} from "@welshman/util"
|
||||
import {nthEq} from "@welshman/lib"
|
||||
import {
|
||||
@@ -22,7 +21,6 @@
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ContentToken from "@app/components/ContentToken.svelte"
|
||||
import ContentCode from "@app/components/ContentCode.svelte"
|
||||
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
||||
@@ -38,9 +36,9 @@
|
||||
minLength?: number
|
||||
maxLength?: number
|
||||
showEntire?: boolean
|
||||
hideMedia?: boolean
|
||||
hideMediaAtDepth?: number
|
||||
expandMode?: string
|
||||
quoteProps?: Record<string, any>
|
||||
relays?: string[]
|
||||
depth?: number
|
||||
}
|
||||
|
||||
@@ -49,9 +47,9 @@
|
||||
minLength = 500,
|
||||
maxLength = 700,
|
||||
showEntire = $bindable(false),
|
||||
hideMedia = false,
|
||||
hideMediaAtDepth = 1,
|
||||
expandMode = "block",
|
||||
quoteProps = {},
|
||||
relays = [],
|
||||
depth = 0,
|
||||
}: Props = $props()
|
||||
|
||||
@@ -64,13 +62,13 @@
|
||||
const isBlock = (i: number) => {
|
||||
const parsed = fullContent[i]
|
||||
|
||||
if (!parsed || hideMedia) return false
|
||||
if (!parsed || hideMediaAtDepth <= depth) return false
|
||||
|
||||
if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i) && depth < 1) {
|
||||
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i)) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -108,7 +106,7 @@
|
||||
: truncate(fullContent, {
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: hideMedia ? 20 : 200,
|
||||
mediaLength: hideMediaAtDepth <= depth ? 20 : 200,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -151,11 +149,7 @@
|
||||
<ContentMention value={parsed.value} />
|
||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
||||
{#if isBlock(i)}
|
||||
<ContentQuote {...quoteProps} value={parsed.value} {event}>
|
||||
{#snippet noteContent({event, minimal}: {event: TrustedEvent; minimal: boolean})}
|
||||
<Content {quoteProps} hideMedia={minimal || hideMedia} {event} depth={depth + 1} />
|
||||
{/snippet}
|
||||
</ContentQuote>
|
||||
<ContentQuote {depth} {relays} {hideMediaAtDepth} value={parsed.value} {event} />
|
||||
{:else}
|
||||
<Link
|
||||
external
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
const {value} = $props()
|
||||
|
||||
let hideImage = $state(false)
|
||||
|
||||
const url = value.url.toString()
|
||||
|
||||
const loadPreview = async () => {
|
||||
@@ -20,6 +22,10 @@
|
||||
return json
|
||||
}
|
||||
|
||||
const onError = () => {
|
||||
hideImage = true
|
||||
}
|
||||
|
||||
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
||||
</script>
|
||||
|
||||
@@ -40,9 +46,10 @@
|
||||
</div>
|
||||
{:then preview}
|
||||
<div class="bg-alt flex max-w-xl flex-col leading-normal">
|
||||
{#if preview.image}
|
||||
{#if preview.image && !hideImage}
|
||||
<img
|
||||
alt="Link preview"
|
||||
onerror={onError}
|
||||
src={imgproxy(preview.image)}
|
||||
class="bg-alt max-h-72 object-contain object-center" />
|
||||
{/if}
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import {deriveEvent, entityLink, ROOM} from "@app/state"
|
||||
import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes"
|
||||
|
||||
const {value, event, noteContent, relays = [], minimal = false} = $props()
|
||||
const {value, event, depth, hideMediaAtDepth, relays = []} = $props()
|
||||
|
||||
const {id, identifier, kind, pubkey, relays: relayHints = []} = value
|
||||
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
||||
@@ -103,8 +104,8 @@
|
||||
|
||||
<Button class="my-2 block max-w-full text-left" {onclick}>
|
||||
{#if $quote}
|
||||
<NoteCard {minimal} event={$quote} class="bg-alt rounded-box p-4">
|
||||
{@render noteContent({event: $quote, minimal})}
|
||||
<NoteCard event={$quote} class="bg-alt rounded-box p-4">
|
||||
<NoteContent {hideMediaAtDepth} {relays} event={$quote} depth={depth + 1} />
|
||||
</NoteCard>
|
||||
{:else}
|
||||
<div class="rounded-box p-4">
|
||||
|
||||
@@ -13,9 +13,14 @@
|
||||
import {pushModal, clearModals} from "@app/modal"
|
||||
import {PLATFORM_NAME, BURROW_URL} from "@app/state"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {loadUserData} from "@app/commands"
|
||||
import {loadUserData} from "@app/requests"
|
||||
import {setChecked} from "@app/notifications"
|
||||
|
||||
let signers: any[] = $state([])
|
||||
let loading: string | undefined = $state()
|
||||
|
||||
const disabled = $derived(loading ? true : undefined)
|
||||
|
||||
const signUp = () => pushModal(SignUp)
|
||||
|
||||
const onSuccess = async (session: Session, relays: string[] = []) => {
|
||||
@@ -70,9 +75,6 @@
|
||||
|
||||
const loginWithBunker = () => pushModal(LogInBunker)
|
||||
|
||||
let signers: any[] = $state([])
|
||||
let loading: string | undefined = $state()
|
||||
|
||||
const hasSigner = $derived(getNip07() || signers.length > 0)
|
||||
|
||||
onMount(async () => {
|
||||
@@ -90,7 +92,7 @@
|
||||
you to own your social identity.
|
||||
</p>
|
||||
{#if getNip07()}
|
||||
<Button disabled={Boolean(loading)} onclick={loginWithNip07} class="btn btn-primary">
|
||||
<Button {disabled} onclick={loginWithNip07} class="btn btn-primary">
|
||||
{#if loading === "nip07"}
|
||||
<span class="loading loading-spinner mr-3"></span>
|
||||
{:else}
|
||||
@@ -100,7 +102,7 @@
|
||||
</Button>
|
||||
{/if}
|
||||
{#each signers as app}
|
||||
<Button disabled={Boolean(loading)} class="btn btn-primary" onclick={() => loginWithNip55(app)}>
|
||||
<Button {disabled} class="btn btn-primary" onclick={() => loginWithNip55(app)}>
|
||||
{#if loading === "nip55"}
|
||||
<span class="loading loading-spinner mr-3"></span>
|
||||
{:else}
|
||||
@@ -110,7 +112,7 @@
|
||||
</Button>
|
||||
{/each}
|
||||
{#if BURROW_URL && !hasSigner}
|
||||
<Button disabled={Boolean(loading)} onclick={loginWithPassword} class="btn btn-primary">
|
||||
<Button {disabled} onclick={loginWithPassword} class="btn btn-primary">
|
||||
{#if loading === "password"}
|
||||
<span class="loading loading-spinner mr-3"></span>
|
||||
{:else}
|
||||
@@ -121,13 +123,13 @@
|
||||
{/if}
|
||||
<Button
|
||||
onclick={loginWithBunker}
|
||||
disabled={Boolean(loading)}
|
||||
{disabled}
|
||||
class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}">
|
||||
<Icon icon="cpu" />
|
||||
Log in with Remote Signer
|
||||
</Button>
|
||||
{#if BURROW_URL && hasSigner}
|
||||
<Button disabled={Boolean(loading)} onclick={loginWithPassword} class="btn">
|
||||
<Button {disabled} onclick={loginWithPassword} class="btn">
|
||||
{#if loading === "password"}
|
||||
<span class="loading loading-spinner mr-3"></span>
|
||||
{:else}
|
||||
@@ -139,7 +141,7 @@
|
||||
{#if !hasSigner || !BURROW_URL}
|
||||
<Link
|
||||
external
|
||||
disabled={Boolean(loading)}
|
||||
{disabled}
|
||||
href="https://nostrapps.com#signers"
|
||||
class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}">
|
||||
<Icon icon="compass" />
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import QRCode from "@app/components/QRCode.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 {setChecked} from "@app/notifications"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
||||
import {loadUserData} from "@app/commands"
|
||||
import {loadUserData} from "@app/requests"
|
||||
import {clearModals, pushModal} from "@app/modal"
|
||||
import {setChecked} from "@app/notifications"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
@@ -57,14 +57,14 @@
|
||||
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
||||
|
||||
let showMenu = $state(false)
|
||||
let replaceState = false
|
||||
let replaceState = $state(false)
|
||||
let element: Element | undefined = $state()
|
||||
|
||||
const members = $derived(
|
||||
$memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey),
|
||||
)
|
||||
|
||||
onMount(async () => {
|
||||
onMount(() => {
|
||||
replaceState = Boolean(element?.closest(".drawer"))
|
||||
pullConservatively({relays: [url], filters: [{kinds: [GROUP_META]}]})
|
||||
})
|
||||
@@ -112,19 +112,25 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex min-h-0 flex-col gap-1 overflow-auto">
|
||||
<SecondaryNavItem href={makeSpacePath(url)}>
|
||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url)}>
|
||||
<Icon icon="home-smile" /> Home
|
||||
</SecondaryNavItem>
|
||||
<SecondaryNavItem href={threadsPath} notification={$notifications.has(threadsPath)}>
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={threadsPath}
|
||||
notification={$notifications.has(threadsPath)}>
|
||||
<Icon icon="notes-minimalistic" /> Threads
|
||||
</SecondaryNavItem>
|
||||
<SecondaryNavItem href={calendarPath} notification={$notifications.has(calendarPath)}>
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={calendarPath}
|
||||
notification={$notifications.has(calendarPath)}>
|
||||
<Icon icon="calendar-minimalistic" /> Calendar
|
||||
</SecondaryNavItem>
|
||||
<div class="h-2"></div>
|
||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||
{#each $userRooms as room, i (room)}
|
||||
<MenuSpaceRoomItem notify {url} {room} />
|
||||
<MenuSpaceRoomItem {replaceState} notify {url} {room} />
|
||||
{/each}
|
||||
{#if $otherRooms.length > 0}
|
||||
<div class="h-2"></div>
|
||||
@@ -137,9 +143,9 @@
|
||||
</SecondaryNavHeader>
|
||||
{/if}
|
||||
{#each $otherRooms as room, i (room)}
|
||||
<MenuSpaceRoomItem {url} {room} />
|
||||
<MenuSpaceRoomItem {replaceState} {url} {room} />
|
||||
{/each}
|
||||
<SecondaryNavItem onclick={addRoom}>
|
||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
||||
<Icon icon="add-circle" />
|
||||
Create room
|
||||
</SecondaryNavItem>
|
||||
|
||||
@@ -10,15 +10,19 @@
|
||||
url: any
|
||||
room: any
|
||||
notify?: boolean
|
||||
replaceState?: boolean
|
||||
}
|
||||
|
||||
const {url, room, notify = false}: Props = $props()
|
||||
const {url, room, notify = false, replaceState = false}: Props = $props()
|
||||
|
||||
const path = makeRoomPath(url, room)
|
||||
const channel = deriveChannel(url, room)
|
||||
</script>
|
||||
|
||||
<SecondaryNavItem href={path} notification={notify ? $notifications.has(path) : false}>
|
||||
<SecondaryNavItem
|
||||
href={path}
|
||||
{replaceState}
|
||||
notification={notify ? $notifications.has(path) : false}>
|
||||
{#if channelIsLocked($channel)}
|
||||
<Icon icon="lock" size={4} />
|
||||
{:else}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {EVENT_TIME} from "@welshman/util"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import CalendarEventDate from "@app/components/CalendarEventDate.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
|
||||
const props: ComponentProps<typeof Content> = $props()
|
||||
</script>
|
||||
|
||||
{#if props.event.kind === EVENT_TIME}
|
||||
<div class="flex items-start gap-4">
|
||||
<CalendarEventDate event={props.event} />
|
||||
<div class="flex flex-grow flex-col">
|
||||
<div class="flex flex-grow flex-wrap justify-between gap-2">
|
||||
<CalendarEventHeader event={props.event} />
|
||||
</div>
|
||||
<div class="flex py-2 opacity-50">
|
||||
<div class="h-px flex-grow bg-base-content opacity-25"></div>
|
||||
</div>
|
||||
<Content {...props} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Content {...props} />
|
||||
{/if}
|
||||
@@ -4,7 +4,7 @@
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import {publishDelete, publishReaction} from "@app/commands"
|
||||
@@ -26,7 +26,7 @@
|
||||
</script>
|
||||
|
||||
<NoteCard {event} class="card2 bg-alt">
|
||||
<Content {event} expandMode="inline" />
|
||||
<NoteContent {event} expandMode="inline" />
|
||||
<div class="flex w-full justify-between gap-2">
|
||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
|
||||
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<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, 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>
|
||||
Are you sure? 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">
|
||||
import {ctx} from "@welshman/lib"
|
||||
import type {Profile} from "@welshman/util"
|
||||
import {
|
||||
createEvent,
|
||||
makeProfile,
|
||||
@@ -8,76 +9,31 @@
|
||||
isPublishedProfile,
|
||||
} from "@welshman/util"
|
||||
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 InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
||||
import InfoHandle from "@app/components/InfoHandle.svelte"
|
||||
import {pushModal, clearModals} from "@app/modal"
|
||||
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||
import {clearModals} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
const values = $state({...($profilesByPubkey.get($pubkey!) || makeProfile())})
|
||||
const initialValues = {...($profilesByPubkey.get($pubkey!) || makeProfile())}
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const saveEdit = () => {
|
||||
const onsubmit = (profile: Profile) => {
|
||||
const relays = ctx.app.router.FromUser().getUrls()
|
||||
const template = isPublishedProfile(values)
|
||||
? editProfile($state.snapshot(values))
|
||||
: createProfile($state.snapshot(values))
|
||||
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
|
||||
const event = createEvent(template.kind, template)
|
||||
|
||||
publishThunk({event, relays})
|
||||
pushToast({message: "Your profile has been updated!"})
|
||||
clearModals()
|
||||
}
|
||||
|
||||
let file: File | undefined = $state()
|
||||
</script>
|
||||
|
||||
<form class="col-4" onsubmit={preventDefault(saveEdit)}>
|
||||
<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}
|
||||
</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>
|
||||
<ProfileEditForm {initialValues} {onsubmit}>
|
||||
{#snippet footer()}
|
||||
<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>
|
||||
{/snippet}
|
||||
</ProfileEditForm>
|
||||
|
||||
@@ -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>
|
||||
@@ -8,5 +8,5 @@
|
||||
</script>
|
||||
|
||||
{#if $profile}
|
||||
<Content event={{content: $profile.about, tags: []}} hideMedia />
|
||||
<Content event={{content: $profile.about, tags: []}} />
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {postJson} from "@welshman/lib"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -8,16 +9,22 @@
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import LogIn from "@app/components/LogIn.svelte"
|
||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||
import SignUpKey from "@app/components/SignUpKey.svelte"
|
||||
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
||||
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"
|
||||
|
||||
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/?an=Flotilla&at=${at}&ac=${ac}`
|
||||
const nstart = `https://start.njump.me/?${params.toString()}`
|
||||
|
||||
const login = () => pushModal(LogIn)
|
||||
|
||||
@@ -37,18 +44,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
const signup = () => {
|
||||
const usePassword = () => {
|
||||
if (BURROW_URL) {
|
||||
signupPassword()
|
||||
}
|
||||
}
|
||||
|
||||
const useKey = () => pushModal(SignUpKey)
|
||||
|
||||
let email = $state("")
|
||||
let password = $state("")
|
||||
let loading = $state(false)
|
||||
</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>
|
||||
<p class="m-auto max-w-sm text-center">
|
||||
{PLATFORM_NAME} is built using the
|
||||
@@ -89,10 +98,17 @@
|
||||
</p>
|
||||
<Divider>Or</Divider>
|
||||
{/if}
|
||||
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||
<Icon icon="square-share-line" />
|
||||
Get going on nstart
|
||||
</a>
|
||||
{#if Capacitor.isNativePlatform()}
|
||||
<Button onclick={useKey} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||
<Icon icon="key" />
|
||||
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">
|
||||
Already have an account?
|
||||
<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>
|
||||
@@ -32,7 +32,7 @@
|
||||
{formatTimestamp(event.created_at)}
|
||||
</p>
|
||||
{/if}
|
||||
<Content {event} expandMode="inline" quoteProps={{relays: [url]}} />
|
||||
<Content {event} expandMode="inline" relays={[url]} />
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Posted by <ProfileLink pubkey={event.pubkey} />
|
||||
|
||||
+93
-7
@@ -1,5 +1,19 @@
|
||||
import {get, writable} from "svelte/store"
|
||||
import {partition, 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 {
|
||||
MESSAGE,
|
||||
DELETE,
|
||||
@@ -9,10 +23,11 @@ import {
|
||||
matchFilters,
|
||||
getTagValues,
|
||||
getTagValue,
|
||||
isShareableRelayUrl,
|
||||
} 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 type {Subscription} from "@welshman/net"
|
||||
import type {Subscription, SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import type {AppSyncOpts, Thunk} from "@welshman/app"
|
||||
import {
|
||||
subscribe,
|
||||
@@ -22,10 +37,23 @@ import {
|
||||
hasNegentropy,
|
||||
thunkWorker,
|
||||
createFeedController,
|
||||
loadRelay,
|
||||
loadMutes,
|
||||
loadFollows,
|
||||
loadProfile,
|
||||
loadInboxRelaySelections,
|
||||
getRelayUrls,
|
||||
} from "@welshman/app"
|
||||
import {createScroller} from "@lib/html"
|
||||
import {daysBetween} from "@lib/util"
|
||||
import {userRoomsByUrl, getUrlsForEvent} from "@app/state"
|
||||
import {
|
||||
INDEXER_RELAYS,
|
||||
getDefaultPubkeys,
|
||||
userRoomsByUrl,
|
||||
getUrlsForEvent,
|
||||
loadMembership,
|
||||
loadSettings,
|
||||
} from "@app/state"
|
||||
|
||||
// Utils
|
||||
|
||||
@@ -55,6 +83,7 @@ export const makeFeed = ({
|
||||
feedFilters,
|
||||
subscriptionFilters,
|
||||
element,
|
||||
onEvent,
|
||||
onExhausted,
|
||||
initialEvents = [],
|
||||
}: {
|
||||
@@ -62,12 +91,21 @@ export const makeFeed = ({
|
||||
feedFilters: Filter[]
|
||||
subscriptionFilters: Filter[]
|
||||
element: HTMLElement
|
||||
onEvent?: (event: TrustedEvent) => void
|
||||
onExhausted?: () => void
|
||||
initialEvents?: TrustedEvent[]
|
||||
}) => {
|
||||
const seen = new Set<string>()
|
||||
const buffer = writable<TrustedEvent[]>([])
|
||||
const events = writable(initialEvents)
|
||||
|
||||
for (const event of initialEvents) {
|
||||
if (!seen.has(event.id)) {
|
||||
seen.add(event.id)
|
||||
onEvent?.(event)
|
||||
}
|
||||
}
|
||||
|
||||
const insertEvent = (event: TrustedEvent) => {
|
||||
buffer.update($buffer => {
|
||||
for (let i = 0; i < $buffer.length; i++) {
|
||||
@@ -77,6 +115,11 @@ export const makeFeed = ({
|
||||
|
||||
return [...$buffer, event]
|
||||
})
|
||||
|
||||
if (!seen.has(event.id)) {
|
||||
seen.add(event.id)
|
||||
onEvent?.(event)
|
||||
}
|
||||
}
|
||||
|
||||
const removeEvents = (ids: string[]) => {
|
||||
@@ -270,13 +313,17 @@ export const makeCalendarFeed = ({
|
||||
export const listenForNotifications = () => {
|
||||
const subs: Subscription[] = []
|
||||
|
||||
for (const [url, rooms] of userRoomsByUrl.get()) {
|
||||
for (const [url, allRooms] of userRoomsByUrl.get()) {
|
||||
// Limit how many rooms we load at a time, since we have to send a separate filter
|
||||
// for each one due to nip 29 breaking postel's law
|
||||
const rooms = shuffle(Array.from(allRooms)).slice(0, 30)
|
||||
|
||||
load({
|
||||
relays: [url],
|
||||
filters: [
|
||||
{kinds: [THREAD], limit: 1},
|
||||
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
|
||||
...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
|
||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -286,7 +333,7 @@ export const listenForNotifications = () => {
|
||||
filters: [
|
||||
{kinds: [THREAD], since: now()},
|
||||
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
|
||||
...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
||||
],
|
||||
}),
|
||||
)
|
||||
@@ -298,3 +345,42 @@ 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),
|
||||
]),
|
||||
])
|
||||
|
||||
// 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))
|
||||
|
||||
+5
-1
@@ -345,7 +345,11 @@ export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
||||
export const getMembershipUrls = (list?: 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) =>
|
||||
|
||||
@@ -24,15 +24,16 @@
|
||||
import {fade} from "@lib/transition"
|
||||
import {page} from "$app/stores"
|
||||
|
||||
const {children, href = "", notification = false, ...restProps} = $props()
|
||||
const {children, href = "", notification = false, replaceState = false, ...restProps} = $props()
|
||||
|
||||
const active = $derived($page.url.pathname === href)
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
{...restProps}
|
||||
{href}
|
||||
{...restProps}
|
||||
data-sveltekit-replacestate={replaceState}
|
||||
class="{restProps.class} relative flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
|
||||
class:text-base-content={active}
|
||||
class:bg-base-100={active}>
|
||||
|
||||
@@ -76,3 +76,16 @@ export const createScroller = ({
|
||||
}
|
||||
|
||||
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 {theme} from "@app/theme"
|
||||
import {INDEXER_RELAYS, userMembership, ensureUnwrapped, canDecrypt} from "@app/state"
|
||||
import {loadUserData, loginWithNip46} from "@app/commands"
|
||||
import {listenForNotifications} from "@app/requests"
|
||||
import {loadUserData, listenForNotifications} from "@app/requests"
|
||||
import {loginWithNip46} from "@app/commands"
|
||||
import * as commands from "@app/commands"
|
||||
import * as requests from "@app/requests"
|
||||
import * as notifications from "@app/notifications"
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import {onMount} from "svelte"
|
||||
import {addToMapKey, dec, gt} from "@welshman/lib"
|
||||
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 {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -14,15 +15,26 @@
|
||||
import SpaceCheck from "@app/components/SpaceCheck.svelte"
|
||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import {
|
||||
memberships,
|
||||
membershipByPubkey,
|
||||
getMembershipUrls,
|
||||
loadMembership,
|
||||
userRoomsByUrl,
|
||||
getDefaultPubkeys,
|
||||
} from "@app/state"
|
||||
import {discoverRelays} from "@app/commands"
|
||||
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 scores = new Map<string, Set<string>>()
|
||||
|
||||
@@ -36,20 +48,23 @@
|
||||
})
|
||||
|
||||
const relaySearch = $derived(
|
||||
createSearch($relays, {
|
||||
getValue: (relay: Relay) => relay.url,
|
||||
sortFn: ({score, item}) => {
|
||||
if (score && score > 0.1) return -score!
|
||||
createSearch(
|
||||
$relays.filter(r => wotGraph.has(r.url)),
|
||||
{
|
||||
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})
|
||||
@@ -128,9 +143,9 @@
|
||||
{/if}
|
||||
</Button>
|
||||
{/each}
|
||||
{#await discoverRelays($memberships)}
|
||||
<div class="flex justify-center">
|
||||
<Spinner loading>Loading more relays...</Spinner>
|
||||
{#await discoverRelays()}
|
||||
<div class="flex justify-center py-20" out:fly>
|
||||
<Spinner loading>Looking for spaces...</Spinner>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
||||
import ProfileDelete from "@app/components/ProfileDelete.svelte"
|
||||
import InfoKeys from "@app/components/InfoKeys.svelte"
|
||||
import {PLATFORM_NAME} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
@@ -25,6 +26,8 @@
|
||||
const startEdit = () => pushModal(ProfileEdit)
|
||||
|
||||
const startEject = () => pushModal(InfoKeys)
|
||||
|
||||
const startDelete = () => pushModal(ProfileDelete)
|
||||
</script>
|
||||
|
||||
<div class="content column gap-4">
|
||||
@@ -50,7 +53,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
{#key $profile?.about}
|
||||
<Content event={{content: $profile?.about || "", tags: []}} hideMedia />
|
||||
<Content event={{content: $profile?.about || "", tags: []}} hideMediaAtDepth={0} />
|
||||
{/key}
|
||||
</div>
|
||||
{#if $session?.email}
|
||||
@@ -117,4 +120,10 @@
|
||||
</FieldInline>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card2 bg-alt col-4 shadow-xl">
|
||||
<Button class="btn btn-outline btn-error" onclick={startDelete}>
|
||||
<Icon icon="trash-bin-2" />
|
||||
Delete your profile
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
import RelayItem from "@app/components/RelayItem.svelte"
|
||||
import RelayAdd from "@app/components/RelayAdd.svelte"
|
||||
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 writeRelayUrls = derived(userRelaySelections, getWriteRelayUrls)
|
||||
|
||||
@@ -50,8 +50,12 @@
|
||||
|
||||
const joinRoom = async () => {
|
||||
if (hasNip29($relay)) {
|
||||
joiningRoom = true
|
||||
|
||||
const message = await getThunkError(nip29.joinRoom(url, room))
|
||||
|
||||
joiningRoom = false
|
||||
|
||||
if (message && !message.includes("already")) {
|
||||
return pushToast({theme: "error", message})
|
||||
}
|
||||
@@ -126,7 +130,8 @@
|
||||
|
||||
const scrollToBottom = () => element?.scrollTo({top: 0, behavior: "smooth"})
|
||||
|
||||
let loading = $state(true)
|
||||
let joiningRoom = $state(false)
|
||||
let loadingEvents = $state(true)
|
||||
let share = $state(popKey<TrustedEvent | undefined>("share"))
|
||||
let parent: TrustedEvent | undefined = $state()
|
||||
let element: HTMLElement | undefined = $state()
|
||||
@@ -147,6 +152,12 @@
|
||||
let newMessagesSeen = false
|
||||
|
||||
if (events) {
|
||||
const lastUserEvent = $events.find(e => e.pubkey === $pubkey)
|
||||
|
||||
// Adjust last checked to account for messages that came from a different device
|
||||
const adjustedLastChecked =
|
||||
lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked
|
||||
|
||||
for (const event of $events.toReversed()) {
|
||||
if (seen.has(event.id)) {
|
||||
continue
|
||||
@@ -156,9 +167,9 @@
|
||||
|
||||
if (
|
||||
!newMessagesSeen &&
|
||||
adjustedLastChecked &&
|
||||
event.pubkey !== $pubkey &&
|
||||
lastChecked &&
|
||||
event.created_at > lastChecked
|
||||
event.created_at > adjustedLastChecked
|
||||
) {
|
||||
elements.push({type: "new-messages", id: "new-messages"})
|
||||
newMessagesSeen = true
|
||||
@@ -196,7 +207,7 @@
|
||||
subscriptionFilters: [{kinds: [DELETE, REACTION, MESSAGE], "#h": [room], since: now()}],
|
||||
initialEvents: getEventsForUrl(url, [{...filter, limit: 20}]),
|
||||
onExhausted: () => {
|
||||
loading = false
|
||||
loadingEvents = false
|
||||
},
|
||||
}))
|
||||
})
|
||||
@@ -211,7 +222,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="saib relative flex h-full flex-col">
|
||||
<div class="relative flex h-full flex-col">
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
@@ -232,8 +243,12 @@
|
||||
Leave Room
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral btn-sm" onclick={joinRoom}>
|
||||
<Icon icon="login-2" />
|
||||
<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}
|
||||
@@ -265,8 +280,8 @@
|
||||
{/if}
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
{#if loading}
|
||||
<Spinner loading>Looking for messages...</Spinner>
|
||||
{#if loadingEvents}
|
||||
<Spinner loading={loadingEvents}>Looking for messages...</Spinner>
|
||||
{:else}
|
||||
<Spinner>End of message history</Spinner>
|
||||
{/if}
|
||||
@@ -281,7 +296,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="saib">
|
||||
<div>
|
||||
{#if parent}
|
||||
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||
{/if}
|
||||
|
||||
@@ -126,7 +126,11 @@
|
||||
<strong>Calendar</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div class="md:hidden">
|
||||
<div class="row-2">
|
||||
<Button class="btn btn-primary btn-sm" onclick={createEvent}>
|
||||
<Icon icon="calendar-add" />
|
||||
Create an Event
|
||||
</Button>
|
||||
<MenuSpaceButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -157,12 +161,4 @@
|
||||
<p class="flex h-10 items-center justify-center py-20" transition:fly>That's all!</p>
|
||||
{/if}
|
||||
</div>
|
||||
<Button
|
||||
class="tooltip tooltip-left fixed bottom-16 right-2 z-feature p-1 md:bottom-4 md:right-4"
|
||||
data-tip="Create an Event"
|
||||
onclick={createEvent}>
|
||||
<div class="btn btn-circle btn-primary flex h-12 w-12 items-center justify-center">
|
||||
<Icon icon="calendar-add" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {sortBy, fromPairs, sleep} from "@welshman/lib"
|
||||
import {sortBy, sleep} from "@welshman/lib"
|
||||
import {COMMENT, getTagValue} from "@welshman/util"
|
||||
import {
|
||||
repository,
|
||||
subscribe,
|
||||
formatTimestamp,
|
||||
LOCALE,
|
||||
secondsToDate,
|
||||
formatTimestampAsDate,
|
||||
formatTimestampAsTime,
|
||||
} from "@welshman/app"
|
||||
import {repository, subscribe} from "@welshman/app"
|
||||
import {deriveEvents} from "@welshman/store"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
@@ -20,8 +12,10 @@
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import CalendarEventMeta from "@app/components/CalendarEventMeta.svelte"
|
||||
import CalendarEventDate from "@app/components/CalendarEventDate.svelte"
|
||||
import EventReply from "@app/components/EventReply.svelte"
|
||||
import {deriveEvent, decodeRelay} from "@app/state"
|
||||
import {setChecked} from "@app/notifications"
|
||||
@@ -31,13 +25,6 @@
|
||||
const event = deriveEvent(id)
|
||||
const filters = [{kinds: [COMMENT], "#E": [id]}]
|
||||
const replies = deriveEvents(repository, {filters})
|
||||
const meta = $derived(fromPairs($event.tags) as Record<string, string>)
|
||||
const end = $derived(parseInt(meta.end))
|
||||
const start = $derived(parseInt(meta.start))
|
||||
const startDate = $derived(secondsToDate(start))
|
||||
const startDateDisplay = $derived(formatTimestampAsDate(start))
|
||||
const endDateDisplay = $derived(formatTimestampAsDate(end))
|
||||
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
@@ -95,39 +82,18 @@
|
||||
{/if}
|
||||
<div class="card2 bg-alt col-3 z-feature">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex h-24 w-24 flex-col items-center justify-center gap-1 rounded-box border border-solid border-base-content p-2">
|
||||
<span class="text-lg"
|
||||
>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</span>
|
||||
<span class="text-4xl"
|
||||
>{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||
</div>
|
||||
<CalendarEventDate event={$event} />
|
||||
<div class="flex flex-grow flex-col">
|
||||
<div class="flex flex-grow justify-between gap-2">
|
||||
<p class="text-xl">{meta.title || meta.name}</p>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon="clock-circle" size={4} />
|
||||
{formatTimestampAsTime(start)} — {isSingleDay
|
||||
? formatTimestampAsTime(end)
|
||||
: formatTimestamp(end)}
|
||||
</div>
|
||||
<CalendarEventHeader event={$event} />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm opacity-75">
|
||||
<span>
|
||||
Posted by <ProfileLink pubkey={$event.pubkey} />
|
||||
</span>
|
||||
{#if meta.location}
|
||||
<span>•</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon icon="map-point" size={4} />
|
||||
{meta.location}
|
||||
</span>
|
||||
{/if}
|
||||
<CalendarEventMeta event={$event} />
|
||||
</div>
|
||||
<div class="flex py-2 opacity-50">
|
||||
<div class="h-px flex-grow bg-base-content opacity-25"></div>
|
||||
</div>
|
||||
<Content showEntire event={$event} quoteProps={{relays: [url]}} />
|
||||
<Content showEntire event={$event} relays={[url]} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-col justify-end sm:flex-row">
|
||||
@@ -141,12 +107,12 @@
|
||||
<p>Failed to load comments.</p>
|
||||
{/await}
|
||||
{/if}
|
||||
<PageBar class="mx-0">
|
||||
<PageBar class="!mx-0">
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
<Button class="btn btn-neutral btn-sm" onclick={back}>
|
||||
<Button class="btn btn-neutral btn-sm flex-nowrap whitespace-nowrap" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
<span class="hidden sm:inline">Go back</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {derived} from "svelte/store"
|
||||
import {page} from "$app/stores"
|
||||
import {sortBy, min, nthEq} from "@welshman/lib"
|
||||
import {THREAD, COMMENT, getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||
import {throttled} from "@welshman/store"
|
||||
import {feedFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
||||
import {createFeedController, userMutes} from "@welshman/app"
|
||||
import {createScroller, type Scroller} from "@lib/html"
|
||||
import {sortBy, max, nthEq} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {THREAD, REACTION, DELETE, COMMENT, getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||
import {userMutes} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -16,69 +13,63 @@
|
||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||
import ThreadItem from "@app/components/ThreadItem.svelte"
|
||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||
import {decodeRelay, deriveEventsForUrl} from "@app/state"
|
||||
import {decodeRelay, getEventsForUrl} from "@app/state"
|
||||
import {setChecked} from "@app/notifications"
|
||||
import {makeFeed} from "@app/requests"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const url = decodeRelay($page.params.relay)
|
||||
const threadFilter = {kinds: [THREAD]}
|
||||
const commentFilter = {kinds: [COMMENT], "#K": [String(THREAD)]}
|
||||
const feed = feedFromFilters([threadFilter, commentFilter])
|
||||
const threads = deriveEventsForUrl(url, [threadFilter])
|
||||
const comments = deriveEventsForUrl(url, [commentFilter])
|
||||
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
|
||||
const threads: TrustedEvent[] = $state([])
|
||||
const comments: TrustedEvent[] = $state([])
|
||||
|
||||
const events = throttled(
|
||||
800,
|
||||
derived([threads, comments], ([$threads, $comments]) => {
|
||||
const scores = new Map<string, number>()
|
||||
|
||||
for (const comment of $comments) {
|
||||
const id = comment.tags.find(nthEq(0, "E"))?.[1]
|
||||
|
||||
if (id) {
|
||||
scores.set(id, min([scores.get(id), -comment.created_at]))
|
||||
}
|
||||
}
|
||||
|
||||
return sortBy(
|
||||
e => min([scores.get(e.id), -e.created_at]),
|
||||
$threads.filter(e => !mutedPubkeys.includes(e.pubkey)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
let loading = $state(true)
|
||||
let element: HTMLElement | undefined = $state()
|
||||
|
||||
const createThread = () => pushModal(ThreadCreate, {url})
|
||||
|
||||
const ctrl = createFeedController({
|
||||
useWindowing: true,
|
||||
feed: makeIntersectionFeed(makeRelayFeed(url), feed),
|
||||
onExhausted: () => {
|
||||
loading = false
|
||||
},
|
||||
const events = $derived.by(() => {
|
||||
const scores = new Map<string, number>()
|
||||
|
||||
for (const comment of comments) {
|
||||
const id = comment.tags.find(nthEq(0, "E"))?.[1]
|
||||
|
||||
if (id) {
|
||||
scores.set(id, max([scores.get(id), comment.created_at]))
|
||||
}
|
||||
}
|
||||
|
||||
return sortBy(e => -max([scores.get(e.id), e.created_at]), threads)
|
||||
})
|
||||
|
||||
let limit = 10
|
||||
let loading = $state(true)
|
||||
let element: Element | undefined = $state()
|
||||
let scroller: Scroller
|
||||
$inspect({threads, comments, events})
|
||||
|
||||
onMount(() => {
|
||||
scroller = createScroller({
|
||||
const {cleanup} = makeFeed({
|
||||
element: element!,
|
||||
delay: 300,
|
||||
threshold: 3000,
|
||||
onScroll: () => {
|
||||
limit += 10
|
||||
|
||||
if ($events.length - limit < 10) {
|
||||
ctrl.load(50)
|
||||
relays: [url],
|
||||
feedFilters: [{kinds: [THREAD, COMMENT]}],
|
||||
subscriptionFilters: [
|
||||
{kinds: [THREAD, REACTION, DELETE]},
|
||||
{kinds: [COMMENT], "#K": [String(THREAD)]},
|
||||
],
|
||||
initialEvents: getEventsForUrl(url, [{kinds: [THREAD, COMMENT], limit: 10}]),
|
||||
onEvent: event => {
|
||||
if (event.kind === THREAD && !mutedPubkeys.includes(event.pubkey)) {
|
||||
threads.push(event)
|
||||
}
|
||||
|
||||
if (event.kind === COMMENT) {
|
||||
comments.push(event)
|
||||
}
|
||||
},
|
||||
onExhausted: () => {
|
||||
loading = false
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
scroller?.stop()
|
||||
cleanup()
|
||||
setChecked($page.url.pathname)
|
||||
}
|
||||
})
|
||||
@@ -105,21 +96,21 @@
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
<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>
|
||||
<ThreadItem {url} {event} />
|
||||
</div>
|
||||
{/each}
|
||||
{#if loading || $events.length === 0}
|
||||
<p class="flex h-10 items-center justify-center py-20" out:fly>
|
||||
<Spinner {loading}>
|
||||
{#if loading}
|
||||
Looking for threads...
|
||||
{:else if $events.length === 0}
|
||||
No threads found.
|
||||
{/if}
|
||||
</Spinner>
|
||||
</p>
|
||||
{/if}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
<Spinner {loading}>
|
||||
{#if loading}
|
||||
Looking for threads...
|
||||
{:else if events.length === 0}
|
||||
No threads found.
|
||||
{:else}
|
||||
That's all!
|
||||
{/if}
|
||||
</Spinner>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
{/if}
|
||||
<NoteCard event={$event} class="card2 bg-alt z-feature w-full">
|
||||
<div class="col-3 ml-12">
|
||||
<Content showEntire event={$event} quoteProps={{relays: [url]}} />
|
||||
<Content showEntire event={$event} relays={[url]} />
|
||||
<ThreadActions event={$event} {url} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
@@ -90,12 +90,12 @@
|
||||
<p>Failed to load thread.</p>
|
||||
{/await}
|
||||
{/if}
|
||||
<PageBar class="mx-0">
|
||||
<PageBar class="!mx-0">
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
<Button class="btn btn-neutral btn-sm" onclick={back}>
|
||||
<Button class="btn btn-neutral btn-sm flex-nowrap whitespace-nowrap" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
<span class="hidden sm:inline">Go back</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
Reference in New Issue
Block a user