mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
506 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69099728b2 | ||
|
|
190b99d55a | ||
|
|
95adb53640 | ||
|
|
25a4b68958 | ||
|
|
e91c793f73 | ||
|
|
5b477c64cf | ||
|
|
11ca2c14be | ||
|
|
1b39bf1d52 | ||
|
|
a4e3625f26 | ||
|
|
e75b5d173b | ||
|
|
027cc660e2 | ||
|
|
49850e773f | ||
|
|
44d100f60a | ||
|
|
f110436853 | ||
|
|
aebbe0974a | ||
|
|
a79ceb527f | ||
|
|
29bb0d2682 | ||
|
|
794af6eb2b | ||
|
|
3e83762761 | ||
|
|
c6e1dc9d79 | ||
|
|
6354b32205 | ||
|
|
6618644ba3 | ||
|
|
329d133291 | ||
|
|
3fec18b9a8 | ||
|
|
de395f5629 | ||
|
|
55a1626159 | ||
|
|
581f13eea8 | ||
|
|
b960a4692a | ||
|
|
4256e3f3cf | ||
|
|
f291bb126a | ||
|
|
e5c8e6dd7f | ||
|
|
bbeb2a66ee | ||
|
|
7ec5357653 | ||
|
|
96a2645070 | ||
|
|
bc8b73e458 | ||
|
|
da96180a10 | ||
|
|
4dbd833c0d | ||
|
|
d5f27c7994 | ||
|
|
20425a7678 | ||
|
|
26798e2b57 | ||
|
|
9cb7f9cc21 | ||
|
|
094adcd6f6 | ||
|
|
3dbd79d879 | ||
|
|
126d58e2d1 | ||
|
|
87d0d2e768 | ||
|
|
50470771b0 | ||
|
|
acd0a13abf | ||
|
|
bb6bd3f0ae | ||
|
|
4d1e9cb8d5 | ||
|
|
abeccb23a2 | ||
|
|
d57c03a01a | ||
|
|
be1ad9cdb8 | ||
|
|
3a850ba26c | ||
|
|
96c4092138 | ||
|
|
a4bf093cb3 | ||
|
|
c5edcab051 | ||
|
|
a7f60082f0 | ||
|
|
91f5dffc6d | ||
|
|
8e4ee7a618 | ||
|
|
332bfcd88a | ||
|
|
1872a5879e | ||
|
|
a293976501 | ||
|
|
a1095b4c17 | ||
|
|
95d678f62f | ||
|
|
b625ac6d2d | ||
|
|
f848ad20d8 | ||
|
|
198e049873 | ||
|
|
f2d007f9ba | ||
|
|
4b9c3f239c | ||
|
|
3ac04cdc63 | ||
|
|
f9d231063d | ||
|
|
4bca74888d | ||
|
|
17ff27bac9 | ||
|
|
3913384fcf | ||
|
|
b02b8c4ed9 | ||
|
|
ed31b12e3f | ||
|
|
3b63264927 | ||
|
|
640c47e29f | ||
|
|
3dd1b949eb | ||
|
|
13bd492e32 | ||
|
|
ef8b9be0e8 | ||
|
|
c44f59b1ce | ||
|
|
0c4e5be1ea | ||
|
|
bc491b154b | ||
|
|
a7259b21ee | ||
|
|
06fae3ba31 | ||
|
|
ea7a839568 | ||
|
|
6912a120f8 | ||
|
|
3d0364ec5e | ||
|
|
f76fc23840 | ||
|
|
43914d655c | ||
|
|
e9a33a88ca | ||
|
|
40ce80786e | ||
|
|
4581233c10 | ||
|
|
f81631d95c | ||
|
|
5214470209 | ||
|
|
f65fd30728 | ||
|
|
4eb03f66f9 | ||
|
|
7ea9f72a0a | ||
|
|
a468f3b6d6 | ||
|
|
df88cab92d | ||
|
|
3d400d4197 | ||
|
|
b808f5a1f6 | ||
|
|
5d7f37fb41 | ||
|
|
5f0fd52f26 | ||
|
|
e556f7716c | ||
|
|
17f071b0f4 | ||
|
|
0a41806d7e | ||
|
|
882c39fd0e | ||
|
|
2b3005797e | ||
|
|
0e8c6fa434 | ||
|
|
4ba92931f6 | ||
|
|
940e59b323 | ||
|
|
4d8583eed9 | ||
|
|
35d677f531 | ||
|
|
273af03c9f | ||
|
|
cd4b352bdf | ||
|
|
ea2054fa56 | ||
|
|
fe9f2e71a0 | ||
|
|
4f32776b24 | ||
|
|
21488561e0 | ||
|
|
570f7d787d | ||
|
|
6e7f6eccf1 | ||
|
|
ce3de5caf7 | ||
|
|
28f68382dd | ||
|
|
57aec95e8d | ||
|
|
2d21529176 | ||
|
|
bb7cdc973a | ||
|
|
fb78c1a4c4 | ||
|
|
abd53c87a0 | ||
|
|
c5bd3ebcd3 | ||
|
|
974b4fcfb8 | ||
|
|
f1547a020c | ||
|
|
0a25018f20 | ||
|
|
59ea94c248 | ||
|
|
9e21fcd2e8 | ||
|
|
6371f2e0d0 | ||
|
|
207fc6beba | ||
|
|
7068263c00 | ||
|
|
092c057e6f | ||
|
|
15c4fd61f1 | ||
|
|
c89a02eb0d | ||
|
|
104b836bbe | ||
|
|
869628ae16 | ||
|
|
07f16c2efe | ||
|
|
365a526b12 | ||
|
|
7f4e1f153a | ||
|
|
d9471f9531 | ||
|
|
5e20806229 | ||
|
|
f91f68a74e | ||
|
|
c4044858a9 | ||
|
|
776bc6d36f | ||
|
|
8e66776ee1 | ||
|
|
a170335db8 | ||
|
|
59c18f2c75 | ||
|
|
6613d1cacd | ||
|
|
bed5f96f20 | ||
|
|
c624293661 | ||
|
|
9dc208deba | ||
|
|
decf7df2bb | ||
|
|
ebbcb603fb | ||
|
|
8613ae4f31 | ||
|
|
56510e4213 | ||
|
|
cd17badaf4 | ||
|
|
a4ce8439de | ||
|
|
2c5f80f613 | ||
|
|
cad0ff3d80 | ||
|
|
ae99700cef | ||
|
|
03c692a8cf | ||
|
|
efe0bde005 | ||
|
|
3f09150d80 | ||
|
|
bc367b2a7b | ||
|
|
380026c7be | ||
|
|
0b3bce5bc2 | ||
|
|
c40c1ed383 | ||
|
|
c17fb4641a | ||
|
|
6d89de9444 | ||
|
|
3b81731771 | ||
|
|
541b197a64 | ||
|
|
250d89ca0a | ||
|
|
25b2cb42d0 | ||
|
|
65bc617222 | ||
|
|
067938657f | ||
|
|
72106ea478 | ||
|
|
32324e5bbe | ||
|
|
c3447e2a05 | ||
|
|
a462221ca6 | ||
|
|
b1b9707c5f | ||
|
|
48da76c6a3 | ||
|
|
a096d62cfb | ||
|
|
8702ec4d96 | ||
|
|
4b5405c6b7 | ||
|
|
589bb041b2 | ||
|
|
ff9b619d26 | ||
|
|
14ff2fbf8c | ||
|
|
7290f16d0d | ||
|
|
567cfa33e8 | ||
|
|
31adfe80ff | ||
|
|
dbd0fe03fa | ||
|
|
1514de53c7 | ||
|
|
0adea4f40a | ||
|
|
23dcda55d3 | ||
|
|
d991af72ff | ||
|
|
f96e2c02de | ||
|
|
c596e16182 | ||
|
|
9c361dbe1e | ||
|
|
485ed871d1 | ||
|
|
20eb95ae57 | ||
|
|
3c01c17d59 | ||
|
|
f3cefe0e13 | ||
|
|
4db617aeef | ||
|
|
72bc7e0ed5 | ||
|
|
f997586e4b | ||
|
|
1d208edd33 | ||
|
|
c0212c09e5 | ||
|
|
8c1ca8dc8b | ||
|
|
c44f155a82 | ||
|
|
0eb32ae4fb | ||
|
|
9b3e77e2d5 | ||
|
|
61cf21a875 | ||
|
|
6a13b2b7b7 | ||
|
|
72f6e47478 | ||
|
|
8f1b10efa6 | ||
|
|
9231fa5457 | ||
|
|
496ee8a58c | ||
|
|
7b1c28c4b7 | ||
|
|
cfa59dab01 | ||
|
|
61000e9662 | ||
|
|
7c02dc3d2d | ||
|
|
d707900aa0 | ||
|
|
9c5e5381a4 | ||
|
|
b8b754a698 | ||
|
|
aa7f480f14 | ||
|
|
e8ba9a2d2f | ||
|
|
b6184ad075 | ||
|
|
352a7f12a2 | ||
|
|
4be617e64a | ||
|
|
4e2717c5c5 | ||
|
|
610fcd7e0c | ||
|
|
0054c01921 | ||
|
|
d299b65c8a | ||
|
|
0889473c15 | ||
|
|
009326ae0b | ||
|
|
60686d0fef | ||
|
|
4c5606cd8d | ||
|
|
dd2ec3f05f | ||
|
|
b147e6d4c3 | ||
|
|
2745b29bf7 | ||
|
|
be62ca5374 | ||
|
|
b51efb5caf | ||
|
|
5776e74bb5 | ||
|
|
eaa45aee05 | ||
|
|
c11e41529e | ||
|
|
cbf9b31a3f | ||
|
|
d40dccde11 | ||
|
|
e3c6c35688 | ||
|
|
05e7b305a7 | ||
|
|
3b15242a02 | ||
|
|
5db9330306 | ||
|
|
559ad40223 | ||
|
|
3ad42d2d8b | ||
|
|
b465785c27 | ||
|
|
9b483f903d | ||
|
|
0c7e5fddea | ||
|
|
46fd8c651c | ||
|
|
7e1ff20e6d | ||
|
|
89b0064a11 | ||
|
|
b5a0c8b6d5 | ||
|
|
943d46e01a | ||
|
|
5d281c34d5 | ||
|
|
bfad08a4a7 | ||
|
|
8cbd11816e | ||
|
|
aa15085f40 | ||
|
|
5fc7526076 | ||
|
|
007a0b0d43 | ||
|
|
391a0bfab2 | ||
|
|
ea8fbd4af9 | ||
|
|
16dd14f403 | ||
|
|
8f3c9d2d93 | ||
|
|
44b8ef5d8f | ||
|
|
c5a88bba97 | ||
|
|
6a6fadb493 | ||
|
|
4d38c1dfb0 | ||
|
|
da060a27fd | ||
|
|
49f0ca30b2 | ||
|
|
e8c98f31e6 | ||
|
|
ff8e5dd4e4 | ||
|
|
e444e59f66 | ||
|
|
bb469700b7 | ||
|
|
3423065b94 | ||
|
|
549baa2ceb | ||
|
|
c068fe38be | ||
|
|
acfb596647 | ||
|
|
323694761e | ||
|
|
540194e0ca | ||
|
|
6a99210633 | ||
|
|
d0a5e7d44b | ||
|
|
e0c569ea9b | ||
|
|
1696ad1248 | ||
|
|
77656ffc84 | ||
|
|
aa3202de4b | ||
|
|
3387e119d5 | ||
|
|
0a44bb4904 | ||
|
|
d65ffd1978 | ||
|
|
4d426a011a | ||
|
|
7ec1ec1a3f | ||
|
|
787b9ab9db | ||
|
|
20f8d14d28 | ||
|
|
58628dbc12 | ||
|
|
3a7402a62b | ||
|
|
fe92deeb53 | ||
|
|
25fee78296 | ||
|
|
787eb01c2c | ||
|
|
1a2e34bdcf | ||
|
|
a0bea7602e | ||
|
|
ddb23118a2 | ||
|
|
0a62b898be | ||
|
|
37dad897cf | ||
|
|
b79cd53d34 | ||
|
|
d7930d4527 | ||
|
|
face5ba6e6 | ||
|
|
427423edda | ||
|
|
153666b9b4 | ||
|
|
1ece456f41 | ||
|
|
a456517fbf | ||
|
|
d3ddb86dbb | ||
|
|
0862be2761 | ||
|
|
cf28e799b7 | ||
|
|
94c900e98f | ||
|
|
e144eabf96 | ||
|
|
e209b9e0cc | ||
|
|
7618a5039d | ||
|
|
9e871c5719 | ||
|
|
9be81ec1a5 | ||
|
|
438d7c9122 | ||
|
|
c0a8da0e0c | ||
|
|
51c68bfaf1 | ||
|
|
511fcb4c2a | ||
|
|
09e47d4174 | ||
|
|
33b91050e1 | ||
|
|
35e1c0b9c0 | ||
|
|
74b943336c | ||
|
|
d121d73bb7 | ||
|
|
cbb8a345e2 | ||
|
|
143d8c30aa | ||
|
|
515659791b | ||
|
|
009b5655f9 | ||
|
|
bc7a7effb8 | ||
|
|
c66d63dbb1 | ||
|
|
be297b89b3 | ||
|
|
2ed3de8d80 | ||
|
|
12a51f6e6d | ||
|
|
dfd959be4e | ||
|
|
75a8e461ad | ||
|
|
7c8d44559e | ||
|
|
395b16050b | ||
|
|
4cb5b63631 | ||
|
|
e92d5c8a5c | ||
|
|
9cbcb3b39d | ||
|
|
85ebb7918b | ||
|
|
5d256bab2c | ||
|
|
9171209d44 | ||
|
|
3ecf761d68 | ||
|
|
39e89834ba | ||
|
|
bba4d28e4b | ||
|
|
d0a3679778 | ||
|
|
423bc04dba | ||
|
|
9f7672249b | ||
|
|
79d66cca8f | ||
|
|
7cb9cc6cd5 | ||
|
|
7d6f1a539f | ||
|
|
155e57ab11 | ||
|
|
7952f558eb | ||
|
|
958a460ea9 | ||
|
|
3776f65179 | ||
|
|
44c39b01ea | ||
|
|
d73dd662c6 | ||
|
|
4ef1ba1ac6 | ||
|
|
276dc6589c | ||
|
|
f9c381a113 | ||
|
|
b77e6c1b6c | ||
|
|
17eea1dca9 | ||
|
|
7293b0985b | ||
|
|
9c9c7d3f24 | ||
|
|
072e437277 | ||
|
|
2b95cc013a | ||
|
|
7151d3052a | ||
|
|
cf6a74b162 | ||
|
|
3dbbc039b3 | ||
|
|
00b06a0e2a | ||
|
|
3b489fb3cb | ||
|
|
f27c8811d5 | ||
|
|
f2169e0236 | ||
|
|
3e41bab5d5 | ||
|
|
1e1bc1b172 | ||
|
|
0621e8f7c2 | ||
|
|
4f5665a2bd | ||
|
|
c598c6135c | ||
|
|
8c1e54e147 | ||
|
|
50eb03bc3e | ||
|
|
c63ace1d9c | ||
|
|
e8ede21c17 | ||
|
|
684ba158ab | ||
|
|
e834dbd1e5 | ||
|
|
e80473a7d9 | ||
|
|
c198df57fd | ||
|
|
543f7a329c | ||
|
|
61119e6916 | ||
|
|
9c160006fa | ||
|
|
2f8e62fda5 | ||
|
|
70baf55e1c | ||
|
|
463938768e | ||
|
|
ebbcfa26fb | ||
|
|
e7cde18371 | ||
|
|
1ca904b415 | ||
|
|
e43047c027 | ||
|
|
67fd48c5b8 | ||
|
|
0d83af8912 | ||
|
|
46b75abc81 | ||
|
|
618edf9dd5 | ||
|
|
0de01ca295 | ||
|
|
2e9beacb13 | ||
|
|
9db9283952 | ||
|
|
452d2accc9 | ||
|
|
bf8791088c | ||
|
|
15ccdf0c73 | ||
|
|
c903996cc2 | ||
|
|
052fa22d59 | ||
|
|
e90945d201 | ||
|
|
2e02edc333 | ||
|
|
cbee188ca5 | ||
|
|
7f451ee7c7 | ||
|
|
138428a233 | ||
|
|
fd86e3c045 | ||
|
|
283fb96bb7 | ||
|
|
6db28dca0b | ||
|
|
2474ba1713 | ||
|
|
fc560d2752 | ||
|
|
2ead6645c0 | ||
|
|
138bb144ee | ||
|
|
e2ac534be0 | ||
|
|
aa5dad9fac | ||
|
|
0d92c86459 | ||
|
|
9e9f6c3164 | ||
|
|
d84d146981 | ||
|
|
617db3613f | ||
|
|
60a44d25eb | ||
|
|
2ffb920f75 | ||
|
|
f8baf83146 | ||
|
|
08090f57b5 | ||
|
|
19ef2ca041 | ||
|
|
3a21396838 | ||
|
|
0705d070bd | ||
|
|
d6541e37c3 | ||
|
|
7583f7f8e1 | ||
|
|
c654d2267c | ||
|
|
0a0b6aefa0 | ||
|
|
a1ad460b90 | ||
|
|
9f6684b044 | ||
|
|
ac60cbeb02 | ||
|
|
92ae288720 | ||
|
|
0b049c656c | ||
|
|
42db03790a | ||
|
|
60b7c6bd0d | ||
|
|
390edf311d | ||
|
|
26fb6fead4 | ||
|
|
7871a7672b | ||
|
|
e037254b39 | ||
|
|
403351610b | ||
|
|
36af9e2346 | ||
|
|
c3c9bd016e | ||
|
|
a4953f628d | ||
|
|
3afe0c9978 | ||
|
|
b8b51db0b5 | ||
|
|
f42881a48e | ||
|
|
6fc72f47bb | ||
|
|
49698de99c | ||
|
|
041c788e21 | ||
|
|
15a2d77a0f | ||
|
|
86fd92e505 | ||
|
|
1f34a79676 | ||
|
|
5bdc684f21 | ||
|
|
944fbcd000 | ||
|
|
da53001e65 | ||
|
|
efa294ea72 | ||
|
|
c75176891f | ||
|
|
328fcf86d7 | ||
|
|
4fcbffdb3b | ||
|
|
5ef36363cd | ||
|
|
cba65344b9 | ||
|
|
425fc24bc2 | ||
|
|
6c8021f219 | ||
|
|
a6205a29c4 | ||
|
|
c86daa6950 | ||
|
|
08d5f2ebb5 | ||
|
|
478c4cc356 | ||
|
|
292c6de08a | ||
|
|
ed9d942ce0 | ||
|
|
012e97494e | ||
|
|
006a2229fe | ||
|
|
8c69105fdb | ||
|
|
638bdb7b2d | ||
|
|
2004caa53c | ||
|
|
2e1bcf3e52 | ||
|
|
d586a0dc25 | ||
|
|
4491f3aedb |
@@ -745,8 +745,8 @@ android {
|
||||
applicationId = "com.vitorpamplona.amethyst"
|
||||
minSdk = 26 // Android 8.0 (Oreo)
|
||||
targetSdk = 36 // Android 15
|
||||
versionCode = 434
|
||||
versionName = "1.06.2"
|
||||
versionCode = 439
|
||||
versionName = "1.07.3"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
|
||||
86
.claude/skills/find-non-lambda-logs/SKILL.md
Normal file
86
.claude/skills/find-non-lambda-logs/SKILL.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: find-non-lambda-logs
|
||||
description: Use when auditing or migrating Log calls to lambda overloads, after adding new logging, or checking for string interpolation in Log.d/i/w/e calls that waste allocations when the log level is filtered out
|
||||
---
|
||||
|
||||
# Find Non-Lambda Log Calls
|
||||
|
||||
## Overview
|
||||
|
||||
Locates `Log.d/i/w/e` calls that use string interpolation without the lambda overload, wasting string allocation when the log level is filtered out in release builds.
|
||||
|
||||
## When to Use
|
||||
|
||||
- After merging branches that add new logging
|
||||
- Periodic audit of logging hygiene
|
||||
- After migrating `android.util.Log` usages to the shared `Log` wrapper
|
||||
|
||||
## What to Flag
|
||||
|
||||
Calls with **string interpolation** (`$` in message) that do **not** pass a throwable:
|
||||
|
||||
```kotlin
|
||||
// FLAG - interpolation without lambda, no throwable
|
||||
Log.d("Tag", "Processing ${event.id}")
|
||||
Log.w("Tag", "Failed for $url")
|
||||
|
||||
// IGNORE - passes throwable (lambda overload doesn't accept throwable)
|
||||
Log.w("Tag", "Error: ${e.message}", e)
|
||||
Log.e("Tag", "Failed for $url", throwable)
|
||||
|
||||
// IGNORE - no interpolation (no allocation benefit from lambda)
|
||||
Log.d("Tag", "Initialization complete")
|
||||
```
|
||||
|
||||
## Search Commands
|
||||
|
||||
**Important:** Tags can be string literals (`"Tag"`) or variables (`tag`, `LOG_TAG`). Run both patterns for each step.
|
||||
|
||||
### Step 1: Find interpolated Log.d/Log.i (highest priority — filtered in release)
|
||||
|
||||
```
|
||||
pattern: Log\.(d|i)\("[^"]+",\s*"[^"]*\$
|
||||
type: kotlin
|
||||
```
|
||||
```
|
||||
pattern: Log\.(d|i)\(\w+,\s*"[^"]*\$
|
||||
type: kotlin
|
||||
```
|
||||
|
||||
### Step 2: Find interpolated Log.w/Log.e without throwable
|
||||
|
||||
```
|
||||
pattern: Log\.(w|e)\("[^"]+",\s*"[^"]*\$
|
||||
type: kotlin
|
||||
```
|
||||
```
|
||||
pattern: Log\.(w|e)\(\w+,\s*"[^"]*\$
|
||||
type: kotlin
|
||||
```
|
||||
|
||||
Then **manually exclude** lines where a throwable is passed as third argument (ending with `, e)`, `, throwable)`, etc.). Check the actual line — a catch block catching `e` doesn't mean `e` is passed to the Log call.
|
||||
|
||||
### Step 3: Verify no android.util.Log leakage
|
||||
|
||||
```
|
||||
pattern: android\.util\.Log\.(d|i|w|e|v)\(
|
||||
type: kotlin
|
||||
```
|
||||
|
||||
These bypass the `Log.minLevel` filter entirely. Exclude `PlatformLog.android.kt` which is the wrapper implementation.
|
||||
|
||||
## Fix Pattern
|
||||
|
||||
```kotlin
|
||||
// Before
|
||||
Log.d("Tag", "Processing event ${event.id} from ${relay.url}")
|
||||
|
||||
// After
|
||||
Log.d("Tag") { "Processing event ${event.id} from ${relay.url}" }
|
||||
```
|
||||
|
||||
## Do NOT Convert
|
||||
|
||||
- Calls passing a `Throwable` parameter - the lambda overload `(tag) { message }` has no throwable parameter
|
||||
- Static string calls with no `$` interpolation - no allocation benefit
|
||||
- Commented-out log calls
|
||||
@@ -7,7 +7,7 @@ description: Integration guide for using the Quartz Nostr KMP library in externa
|
||||
|
||||
Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects.
|
||||
|
||||
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.06.2` (Maven Central)
|
||||
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.07.3` (Maven Central)
|
||||
**Targets**: JVM 21+, Android (minSdk 21+), iOS (XCFramework `quartz-kmpKit`)
|
||||
**License**: MIT
|
||||
|
||||
@@ -19,7 +19,7 @@ Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
quartz = "1.06.2"
|
||||
quartz = "1.07.3"
|
||||
|
||||
[libraries]
|
||||
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
|
||||
@@ -41,7 +41,7 @@ kotlin {
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.06.2")
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.07.3")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Current version
|
||||
|
||||
```
|
||||
com.vitorpamplona.quartz:quartz:1.06.2
|
||||
com.vitorpamplona.quartz:quartz:1.07.3
|
||||
```
|
||||
|
||||
Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/quartz
|
||||
@@ -16,7 +16,7 @@ Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/qua
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
quartz = "1.06.2"
|
||||
quartz = "1.07.3"
|
||||
|
||||
[libraries]
|
||||
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
|
||||
@@ -55,7 +55,7 @@ kotlin {
|
||||
```kotlin
|
||||
// build.gradle.kts (app module)
|
||||
dependencies {
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.06.2")
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.07.3")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -70,7 +70,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.06.2")
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.07.3")
|
||||
// JNA needed for libsodium (NIP-44) on JVM
|
||||
implementation("net.java.dev.jna:jna:5.18.1")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ The Quartz library was successfully converted from Android-only to full KMP supp
|
||||
## Current artifact
|
||||
|
||||
```
|
||||
com.vitorpamplona.quartz:quartz:1.06.2
|
||||
com.vitorpamplona.quartz:quartz:1.07.3
|
||||
```
|
||||
|
||||
See `.claude/skills/quartz-integration/SKILL.md` for full integration guide.
|
||||
@@ -12,8 +12,11 @@ echo "$JAVA_HOME"
|
||||
echo "$(java -version)"
|
||||
echo "Running test... "
|
||||
|
||||
./gradlew test --quiet
|
||||
|
||||
if [ "${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
|
||||
./gradlew test --quiet -x :desktopApp:test -x :desktopApp:upxDownload -x :desktopApp:vlcDownload
|
||||
else
|
||||
./gradlew test --quiet
|
||||
fi
|
||||
status=$?
|
||||
|
||||
if [ "$status" = 0 ] ; then
|
||||
|
||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -71,6 +74,10 @@ jobs:
|
||||
- name: Test (gradle)
|
||||
run: ./gradlew test --no-daemon
|
||||
|
||||
- name: Stop Gradle Daemon
|
||||
if: always()
|
||||
run: ./gradlew --stop
|
||||
|
||||
- name: Android Test Report
|
||||
uses: asadmansr/android-test-report-action@v1.2.0
|
||||
if: ${{ always() && matrix.os == 'ubuntu-latest' }}
|
||||
@@ -85,7 +92,7 @@ jobs:
|
||||
build-android:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
@@ -188,6 +195,10 @@ jobs:
|
||||
- name: Build Desktop Distribution
|
||||
run: ./gradlew :desktopApp:${{ matrix.task }}
|
||||
|
||||
- name: Stop Gradle Daemon
|
||||
if: always()
|
||||
run: ./gradlew --stop
|
||||
|
||||
- name: Upload Desktop Distribution
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
|
||||
3
.github/workflows/create-release.yml
vendored
3
.github/workflows/create-release.yml
vendored
@@ -5,6 +5,9 @@ on:
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/crowdin.yml
vendored
4
.github/workflows/crowdin.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
# paths: ["app/src/main/res/**/strings.xml"] // removes filter to allow downloads at any moment.
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -23,6 +23,7 @@
|
||||
/.idea/deviceManager.xml
|
||||
/.idea/inspectionProfiles/
|
||||
/.idea/migrations.xml
|
||||
/desktopApp/vlc-temp/
|
||||
/commons/.idea/gradle.xml
|
||||
/commons/.idea/misc.xml
|
||||
/commons/.idea/workspace.xml
|
||||
@@ -162,3 +163,6 @@ TASKS.md
|
||||
desktopApp/src/jvmMain/appResources/linux/
|
||||
desktopApp/src/jvmMain/appResources/macos/
|
||||
desktopApp/src/jvmMain/appResources/windows/
|
||||
|
||||
# Git worktrees
|
||||
.worktrees/
|
||||
|
||||
215
CHANGELOG.md
215
CHANGELOG.md
@@ -1,5 +1,214 @@
|
||||
<a id="v1.07.3"></a>
|
||||
# [Release v1.07.3: GIF Keyboard](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.3) - 2026-03-31
|
||||
|
||||
- Migrates Shorts UI out of a paged design
|
||||
- New edge-to-edge feed for Pictures only
|
||||
- New edge-to-edge feed for Shorts only
|
||||
- New edge-to-edge feed for long Videos only
|
||||
- Migrates Badges to kind 10008
|
||||
- Migrates Bookmarks to kind 10003
|
||||
- Fixes AOSP keyboard auto-correction bug
|
||||
- Fixes Poll's top nav filter
|
||||
- Fixes loading of Bookmark sets
|
||||
- Fixes sorting stability by including event ids
|
||||
- Fixes cursor position in quote post field
|
||||
- Fixes rendering of nostr: uris when composing notes
|
||||
- Adds Basic support for reaction notifications
|
||||
- Refactors NIP-58 Badges on Quartz
|
||||
- Refactors LocalCache methods
|
||||
- Improvements to the Chess engine (debug only)
|
||||
- Add error handling to Tor service initialization and control
|
||||
|
||||
<a id="v1.07.2"></a>
|
||||
# [Release v1.07.2: GIF Keyboard](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.2) - 2026-03-30
|
||||
|
||||
- Adds GIF uploads support from Keyboard
|
||||
- Migrates text fields and @ modifiers to the new Jetpack Compose states
|
||||
- Fixes Bug that wasn't openning Amber to sign
|
||||
- Fixes Bug on rejections using old Ambers
|
||||
- Replaces hex input with user search dialog in relay management (allow/ban user)
|
||||
|
||||
<a id="v1.07.1"></a>
|
||||
# [Release v1.07.1: Fixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.1) - 2026-03-30
|
||||
|
||||
- New translations
|
||||
- Fixes BLE's module need for synchronized events.
|
||||
|
||||
<a id="v1.07.0"></a>
|
||||
# [Release v1.07.0: Vanish, Relay Management, Pin Notes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.0) - 2026-03-30
|
||||
|
||||
Amethyst:
|
||||
- Adds support to Pin Notes
|
||||
- Adds support to Polls feed screen
|
||||
- Adds support for Requests to Vanish
|
||||
- Adds support for Relay management (NIP-86) from Amethyst
|
||||
- Adds support for Relay monitor assessments (NIP-66) in the Relay Info screen.
|
||||
- Adds support for Relay member information (NIP-43) in the Relay Info screen
|
||||
- Adds support for WebBookmarks
|
||||
- Adds support for Zap Goals
|
||||
|
||||
Quartz:
|
||||
- Rewords the NostrClient API for simpler commands.
|
||||
- Adds NIP-15: Nostr Marketplace protocol — product listings, stalls, merchant events (#2020)
|
||||
- Adds NIP-24: Birthday field support added to UserMetadata (#1979)
|
||||
- Adds NIP-29: Relay-based groups — group events, member management, moderation (#2021)
|
||||
- Adds NIP-32: Labeling protocol — LabelEvent and tag parsing (#1975)
|
||||
- Adds NIP-43: Relay access metadata and membership management with UI screens (#2010)
|
||||
- Adds NIP-60: Cashu wallet and spending history event support (#2009)
|
||||
- Adds NIP-61: Nutzaps protocol support (#2008)
|
||||
- Adds NIP-62: Request to Vanish feature — data deletion with relay compliance testing (#1958)
|
||||
- Adds NIP-66: Relay monitor discovery events — UI, filter assembler, subscription composable (#1968)
|
||||
- Adds NIP-69: P2P Order Events (kind 38383) (#2019)
|
||||
- Adds NIP-75: Zap Goals support with creation and rendering (#1965)
|
||||
- Adds NIP-77: Negentropy set reconciliation protocol support (#1955)
|
||||
- Adds NIP-7D: Thread events (kind 11) rendering (#2016)
|
||||
- Adds NIP-85: Trusted Assertions — assertions for events and addressables (#1981)
|
||||
- Adds NIP-86: Relay management UI and client implementation (#1954)
|
||||
- Adds NIP-87: Ecash mint discoverability — Cashu and Fedimint event types (#2015)
|
||||
- Adds NIP-89: Compliance fixes, PlatformLinkTag parsing, app handler extensions (#2030)
|
||||
- Adds NIP-90: All DVM kind event classes from data-vending-machines spec, restructured packages (#2023, #2025)
|
||||
- Adds NIP-A4: Add k tag to zap requests and enforce e tag prohibition (#1978)
|
||||
- Adds NIP-BE: Bluetooth Low Energy mesh networking / Nostr BLE Communications Protocol (#2022)
|
||||
- Adds NIP-C7: Chat messages (kind 9) implementation (#2018)
|
||||
- Adds NIP-5A: Static website event rendering (#2017)
|
||||
- Adds NIP-51, kind 10001: Pinned notes feature (#1956)
|
||||
|
||||
Improvements and Fixes:
|
||||
- Show toast instead of dialog on media download success
|
||||
- Dynamically adjust preferred Blossom server when list changes
|
||||
- Add relay discovery to node master rendering list
|
||||
- Add scroll to settings page
|
||||
- Solves crashing when multiple relays with the same url are included in the resulting list.
|
||||
- Keep screen on during PiP playback and survive screen lock (#1999)
|
||||
- Desktop feed loading — missing events, broken profile navigation (#2027)
|
||||
- Use getOrCreateNote for reply linking to fix flaky thread test (#2027)
|
||||
- Route ReadsScreen following-mode events through cache (#2027)
|
||||
- Pin ElectrumX server certs for Samsung One UI 7 / Android 16 compatibility (#1937)
|
||||
- Duplicate keys in relay management lazy column — sort pubkeys
|
||||
- URL detector — fixes localhost:3030 strings, Japanese character URLs
|
||||
- Web Bookmarks floating action button shape (circle) and open graph previews.
|
||||
- NIP-86 requests now send Accept and Content-Type headers
|
||||
- On DMs, activates decryption for all filetypes that match decryption url with the cipher info, not only binaries
|
||||
- Adds a try/finally to subscriptions to make sure they close even in crashes.
|
||||
- Protects against crashes when the signer sends an unverifiable payload back to Amethyst
|
||||
|
||||
Desktop:
|
||||
- Cache-centric architecture for desktop feeds (#1905)
|
||||
- Render reposts and quoted notes in feed (#2027)
|
||||
|
||||
Performance
|
||||
- Faster startup procedures with less loading on the main thread.
|
||||
- 20x Faster Rfc3986Normalizer and way less objects being created.
|
||||
- Url Detector without using regex
|
||||
- Parallelize preference file access at startup for faster launch
|
||||
- Remove internal runBlocking calls
|
||||
- Lazy loading the memory trimming service
|
||||
- Speeding up DrawerContent rendering
|
||||
- Eagerly delete intermediate temp files in upload pipeline
|
||||
|
||||
Refactoring:
|
||||
- Simplify NostrClient API for beginner-friendliness (#1986)
|
||||
- Simplify relay API with AutoCloseable and serve() helper (#2031)
|
||||
- URL detector performance and readability improvements (#2013)
|
||||
- Restructure NIP-90 DVMs to match NIP-88 Polls pattern (#2023)
|
||||
- Reorganize NIP-BE into subpackages
|
||||
- Move public messages and trusted assertions out of experimental
|
||||
|
||||
Migrations & Deprecation Fixes
|
||||
|
||||
- Migrate `LocalClipboardManager` to `LocalClipboard` (#1995)
|
||||
- Migrate `LocalAutofillTree` to semantics-based Autofill API (#1996)
|
||||
- Migrate `TabRow`/`ScrollableTabRow` to Material 3 Secondary variants (#1994)
|
||||
- Replace `ContextCompat.startActivity` with `Context.startActivity` (#1993)
|
||||
- Suppress deprecation and unchecked cast warnings in Quartz internal code (#1985)
|
||||
|
||||
Platform & Build
|
||||
|
||||
- Add linuxX64 target and restructure native source sets
|
||||
- Add explicit permissions to GitHub Actions workflows
|
||||
- Stop Gradle daemons after build steps
|
||||
- Increase Android CI build timeout to 45 minutes
|
||||
- Update video compression library to latest
|
||||
- Update dependencies, add local maven repo for easier library development
|
||||
- Remove libsodium files
|
||||
- Git hooks installation in worktrees (.git file vs directory)
|
||||
- Fixes lack of proxy access to download VLC files in claude's web environment.
|
||||
- Log level filtering — LogLevel enum, PlatformLog, lambda overloads for 194+ Log.d() calls (#2005)
|
||||
- Parallelize BaseDBTest forEachDB using coroutines
|
||||
|
||||
Documentation
|
||||
|
||||
- Add CLIENT.md guide for building Nostr clients with Quartz
|
||||
- Add RELAY.md guide for building relays with Ktor, NostrServer, SQLite
|
||||
|
||||
Contributors
|
||||
|
||||
- @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z — Primary development
|
||||
- @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef — Log level filtering, video compression, toast downloads, CI fixes, git worktree fix
|
||||
- @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c — Desktop cache architecture, desktop bugfixes
|
||||
- @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 — CI workflow permissions
|
||||
- @npub1a3tx8wcrt789skl6gg7rqwj4wey0j53eesr4z6asd4h4jwrd62jq0wkq4k — Remove libsodium files
|
||||
- **mstrofnone** — ElectrumX cert pinning for Samsung One UI 7
|
||||
|
||||
Translations
|
||||
|
||||
- Czech, German, Swedish, and Portuguese by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
|
||||
- Hungarian by @npub1dnvslq0vvrs8d603suykc4harv94yglcxwna9sl2xu8grt2afm3qgfh0tp
|
||||
- French by @npub106efcyntxc5qwl3w8krrhyt626m59ya2nk9f40px5s968u5xdwhsjsr8fz
|
||||
- Polish by @npub16gjyljum0ksrrm28zzvejydgxwfm7xse98zwc4hlgq8epxeuggushqwyrm
|
||||
- Hindi by @npub1ww6huwu3xye6r05n3qkjeq62wds5pq0jswhl7uc59lchc0n0ns4sdtw5e6
|
||||
- Slovenian by @npub1qqqqqqz7nhdqz3uuwmzlflxt46lyu7zkuqhcapddhgz66c4ddynswreecw
|
||||
- Bengali by @npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t
|
||||
- Spanish by @npub1luhyzgce7qtcs6r6v00ryjxza8av8u4dzh3avg0zks38tjktnmxspxq903
|
||||
- Chinese by hypnotichemionus4 and @npub1gd8e0xfkylc7v8c5a6hkpj4gelwwcy99jt90lqjseqjj2t253s2s6ch58h
|
||||
- Russian by Anton Zhao
|
||||
|
||||
<a id="v1.06.3"></a>
|
||||
# [Release v1.06.3: Anon Posting](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.3) - 2026-03-24
|
||||
|
||||
Improvements to Anon Posting and Reply navigation on DMs
|
||||
|
||||
<a id="v1.06.2"></a>
|
||||
# [Release v1.06.2: Multi-choice Polls](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.2) - 2026-03-24
|
||||
|
||||
- Add poll type selector for single and multiple choice polls
|
||||
- Add drag-to-seek support on the video progress indicator
|
||||
- Hide video controls on playback start
|
||||
- Recording indicator bar stops recording when tapped, not just the small stop icon.
|
||||
- Fixes non http uris in the references tag
|
||||
- Makes sure quote tags only happen once in events.
|
||||
- Removes the clickable NIP-05 URL in the @ tagging of users
|
||||
- Reduces the need to compute naddr to index articles in bookmarks
|
||||
- Checks if p tags are user pubkeys when loading drafts.
|
||||
- Scroll to the replied message when clicking reply preview in chat
|
||||
- Add an anonymous reply with a throwaway keypair
|
||||
- Fixes pool rendering when one label is large and the other is small
|
||||
- Use uri-reference-kmp in commonMain. Remove platform-specific implementations.
|
||||
- Avoids crashing the app in URLs with japanese chars
|
||||
- Sorts followers into a set to avoid LazyColumn key conflicts.
|
||||
- Clickable relay rows on the profile page
|
||||
- Move cache lookups from NavHost route lambdas into screen composables
|
||||
- Improvements to the status of attestations. Validity first, then Processing status.
|
||||
- Fixes Tor Manager flow value and improves Tor binding lifecycle.
|
||||
- Simplifies the report feed in the user profile
|
||||
- Clears some inconsistencies in translated strings
|
||||
- Fixes client tags
|
||||
- Fixes routes for AppDefinition events.
|
||||
|
||||
<a id="v1.06.1"></a>
|
||||
# [Release v1.06.1: Fixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.1) - 2026-03-23
|
||||
|
||||
- Improvements to the rendering of Polls.
|
||||
- Solves some of the crashes of the concurrent modification exception
|
||||
- Fixes URL parsers with Japanese chars
|
||||
- Fixes Wallet import from Primal on Poco phones
|
||||
- Improves the wording of the Last Seen
|
||||
- Fixes for "Cannot disable reuse from root if it was caused by other groups"
|
||||
- Fixes comparator to avoid Comparison method violates its general contract!
|
||||
- Improves zap-store settings
|
||||
|
||||
<a id="v1.06.0"></a>
|
||||
# [Release v1.06.0: Polls, Relay Feeds, Wallets and much more](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.0) - 2025-03-21
|
||||
# [Release v1.06.0: Polls, Relay Feeds, Wallets and much more](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.0) - 2026-03-21
|
||||
|
||||
Polls:
|
||||
- Adds support for creating and rendering NIP-85
|
||||
@@ -271,13 +480,13 @@ Updated translations:
|
||||
- Russian by Anton Zhao
|
||||
|
||||
<a id="v1.05.1"></a>
|
||||
# [Release v1.05.1: BugFixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08
|
||||
# [Release v1.05.1: BugFixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2026-01-08
|
||||
|
||||
- Fixed mixed DMs between logged in users.
|
||||
- Fixed draft screen click to edit post.
|
||||
|
||||
<a id="v1.05.0"></a>
|
||||
# [Release v1.05.0: Bookmark Lists and WoT Scores](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08
|
||||
# [Release v1.05.0: Bookmark Lists and WoT Scores](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2026-01-08
|
||||
|
||||
#Amethyst v1.05.0: Bookmark Lists, Voice Notes, and WoT Scores
|
||||
|
||||
|
||||
169
README.md
169
README.md
@@ -39,107 +39,104 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
|
||||
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px">
|
||||
|
||||
- [x] Events / Relay Subscriptions (NIP-01)
|
||||
- [x] Basic protocol flow (NIP-01)
|
||||
- [x] Follow List (NIP-02)
|
||||
- [x] OpenTimestamps Attestations (NIP-03)
|
||||
- [x] Private Messages (NIP-04 -- to be removed)
|
||||
- [x] DNS Address (NIP-05)
|
||||
- [x] Mnemonic seed phrase (NIP-06)
|
||||
- [ ] WebBrowser Signer (NIP-07, Not applicable)
|
||||
- [x] Old-style mentions (NIP-08)
|
||||
- [x] Event Deletion (NIP-09)
|
||||
- [x] Encrypted Direct Message (NIP-04)
|
||||
- [x] DNS-based Identifiers (NIP-05)
|
||||
- [x] Key Derivation from Mnemonic (NIP-06)
|
||||
- [ ] window.nostr for Web Browsers (NIP-07, Not applicable)
|
||||
- [x] Handling Mentions (NIP-08)
|
||||
- [x] Event Deletion Request (NIP-09)
|
||||
- [x] Text Notes and Threads (NIP-10)
|
||||
- [x] Relay Information Document (NIP-11)
|
||||
- [x] Generic Tag Queries (NIP-12)
|
||||
- [x] Proof of Work Display (NIP-13)
|
||||
- [ ] Proof of Work Calculations (NIP-13)
|
||||
- [x] Events with a Subject (NIP-14)
|
||||
- [ ] Marketplace (NIP-15)
|
||||
- [x] Event Treatment (NIP-16)
|
||||
- [x] Proof of Work (NIP-13)
|
||||
- [x] Subject Tag in Text Events (NIP-14)
|
||||
- [x] Nostr Marketplace (NIP-15)
|
||||
- [x] Private Direct Messages (NIP-17)
|
||||
- [x] Image/Video/Url/LnInvoice/Cashu Previews
|
||||
- [x] Reposts, Quotes, Generic Reposts (NIP-18)
|
||||
- [x] Bech32 Encoding support (NIP-19)
|
||||
- [x] Command Results (NIP-20)
|
||||
- [x] URI Support (NIP-21)
|
||||
- [x] Long-form Content (NIP-23) (view only)
|
||||
- [x] User Profile Fields / Relay list (NIP-24)
|
||||
- [x] Reposts (NIP-18)
|
||||
- [x] bech32-encoded Entities (NIP-19)
|
||||
- [x] nostr: URI Scheme (NIP-21)
|
||||
- [x] Comment (NIP-22)
|
||||
- [x] Long-form Content (NIP-23)
|
||||
- [x] Extra Metadata Fields and Tags (NIP-24)
|
||||
- [x] Reactions (NIP-25)
|
||||
- [ ] Delegated Event Signing (NIP-26, Will not implement)
|
||||
- [x] Delegated Event Signing (NIP-26)
|
||||
- [x] Text Note References (NIP-27)
|
||||
- [x] Public Chats (NIP-28)
|
||||
- [ ] Relay-based Groups (NIP-29)
|
||||
- [x] Public Chat (NIP-28)
|
||||
- [x] Relay-based Groups (NIP-29)
|
||||
- [x] Custom Emoji (NIP-30)
|
||||
- [x] Event alt descriptors (NIP-31)
|
||||
- [ ] Labeling (NIP-32)
|
||||
- [x] Git Stuff (NIP-34)
|
||||
- [x] Dealing with Unknown Events (NIP-31)
|
||||
- [x] Labeling (NIP-32)
|
||||
- [x] git stuff (NIP-34)
|
||||
- [x] Torrents (NIP-35)
|
||||
- [x] Sensitive Content (NIP-36)
|
||||
- [x] Drafts (NIP-37)
|
||||
- [x] User Status Event (NIP-38)
|
||||
- [x] External Identities (NIP-39)
|
||||
- [x] Expiration Support (NIP-40)
|
||||
- [x] Relay Authentication (NIP-42)
|
||||
- [ ] Relay Access Metadata and Requests (NIP-43)
|
||||
- [x] Versioned Encrypted Payloads (NIP-44)
|
||||
- [x] Event Counts (NIP-45)
|
||||
- [o] Nostr Connect (NIP-46)
|
||||
- [o] Wallet Connect API (NIP-47)
|
||||
- [ ] Proxy Tags (NIP-48, Not applicable)
|
||||
- [x] Encryption for import/export (NIP-49)
|
||||
- [x] Relay Search (NIP-50)
|
||||
- [x] Draft Events (NIP-37)
|
||||
- [x] User Statuses (NIP-38)
|
||||
- [x] External Identities in Profiles (NIP-39)
|
||||
- [x] Expiration Timestamp (NIP-40)
|
||||
- [x] Authentication of Clients to Relays (NIP-42)
|
||||
- [x] Relay Access Metadata and Requests (NIP-43)
|
||||
- [x] Encrypted Payloads / Versioned (NIP-44)
|
||||
- [x] Counting Results (NIP-45)
|
||||
- [x] Nostr Remote Signing (NIP-46)
|
||||
- [x] Nostr Wallet Connect (NIP-47)
|
||||
- [x] Proxy Tags (NIP-48)
|
||||
- [x] Private Key Encryption (NIP-49)
|
||||
- [x] Search Capability (NIP-50)
|
||||
- [x] Lists (NIP-51)
|
||||
- [o] Calendar Events (NIP-52)
|
||||
- [x] Live Activities & Live Chats (NIP-53)
|
||||
- [x] Calendar Events (NIP-52)
|
||||
- [x] Live Activities (NIP-53)
|
||||
- [x] Wiki (NIP-54)
|
||||
- [x] Android Signer (NIP-55)
|
||||
- [x] Android Signer Application (NIP-55)
|
||||
- [x] Reporting (NIP-56)
|
||||
- [x] Lightning Tips
|
||||
- [x] Zaps (NIP-57)
|
||||
- [x] Lightning Zaps (NIP-57)
|
||||
- [x] Zap Splits (NIP-57)
|
||||
- [x] Private Zaps (NIP-57 / Draft)
|
||||
- [x] Zapraiser (NIP-57 / Draft)
|
||||
- [x] Private Zaps (NIP-57)
|
||||
- [x] Zapraiser (NIP-57)
|
||||
- [x] Badges (NIP-58)
|
||||
- [x] Gift Wraps & Seals (NIP-59)
|
||||
- [ ] Cashu Wallets (NIP-60)
|
||||
- [ ] Nutzaps (NIP-61)
|
||||
- [x] Gift Wrap (NIP-59)
|
||||
- [x] Pubkey Static Websites (NIP-5A)
|
||||
- [x] Cashu Wallet (NIP-60)
|
||||
- [x] Nutzaps (NIP-61)
|
||||
- [x] Request to Vanish (NIP-62)
|
||||
- [x] Chess (NIP-64)
|
||||
- [x] Chess / PGN (NIP-64)
|
||||
- [x] Relay List Metadata (NIP-65)
|
||||
- [x] Relay Discovery and Monitoring (NIP-66)
|
||||
- [x] Picture-first feeds (NIP-68)
|
||||
- [ ] Peer-to-peer Orders (NIP-69)
|
||||
- [o] Protected Events (NIP-70)
|
||||
- [x] Relay Discovery and Liveness Monitoring (NIP-66)
|
||||
- [x] Picture-first Feeds (NIP-68)
|
||||
- [x] Peer-to-peer Order Events (NIP-69)
|
||||
- [x] Protected Events (NIP-70)
|
||||
- [x] Video Events (NIP-71)
|
||||
- [x] Moderated Communities (NIP-72)
|
||||
- [x] External Content IDs (NIP-73)
|
||||
- [ ] Zap Goals (NIP-75)
|
||||
- [ ] Negentropy Syncing (NIP-77)
|
||||
- [x] Arbitrary Custom App Data (NIP-78)
|
||||
- [ ] NIP-29 Threads (NIP-7D)
|
||||
- [x] Zap Goals (NIP-75)
|
||||
- [x] Negentropy Syncing (NIP-77)
|
||||
- [x] Application-specific Data (NIP-78)
|
||||
- [x] Threads (NIP-7D)
|
||||
- [x] Highlights (NIP-84)
|
||||
- [x] Trusted Assertions (NIP-85)
|
||||
- [ ] Relay Management API (NIP-86)
|
||||
- [ ] ECash Mint Discoverability (NIP-87)
|
||||
- [x] Relay Management API (NIP-86)
|
||||
- [x] Ecash Mint Discoverability (NIP-87)
|
||||
- [x] Polls (NIP-88)
|
||||
- [x] Relay Notify Request
|
||||
- [x] Recommended Application Handlers (NIP-89)
|
||||
- [x] Data Vending Machine (NIP-90)
|
||||
- [x] Inline Metadata (NIP-92)
|
||||
- [x] Verifiable file URLs (NIP-94)
|
||||
- [x] Data Vending Machines (NIP-90)
|
||||
- [x] Media Attachments (NIP-92)
|
||||
- [x] File Metadata (NIP-94)
|
||||
- [x] Binary Blobs (NIP-95/Draft)
|
||||
- [x] HTTP File Storage Integration (NIP-96)
|
||||
- [x] HTTP Auth (NIP-98)
|
||||
- [x] Classifieds (NIP-99)
|
||||
- [x] Classified Listings (NIP-99)
|
||||
- [x] Voice Messages (NIP-A0)
|
||||
- [x] Public Messages (NIP-A4)
|
||||
- [ ] Web Bookmarks
|
||||
- [x] Web Bookmarks (NIP-B0)
|
||||
- [x] Blossom (NIP-B7)
|
||||
- [ ] Nostr BLE Communications Protocol (NIP-BE)
|
||||
- [x] Nostr BLE Communications Protocol (NIP-BE)
|
||||
- [x] Code Snippets (NIP-C0)
|
||||
- [ ] NIP-29 Chats (NIP-C7)
|
||||
- [x] Chats (NIP-C7)
|
||||
- [ ] MLS Protocol (NIP-EE)
|
||||
- [x] Audio Tracks (zapstr.live) (kind:31337)
|
||||
- [x] Lightning Tips
|
||||
- [x] Image/Video/Url/LnInvoice/Cashu Previews
|
||||
- [x] Push Notifications (Google and Unified Push)
|
||||
- [x] In-Device Automatic Translations
|
||||
- [x] Hashtag Following and Custom Hashtags
|
||||
@@ -405,24 +402,24 @@ to `onPause` methods.
|
||||
|
||||
### Feature Parity Table
|
||||
|
||||
| Feature Category | Feature / Component | Android / JVM Support | iOS Support | Notes |
|
||||
| :--- | :--- | :---: | :---: | :--- |
|
||||
| **Cryptography** | Secp256k1 (Schnorr, Keys) | ✅ Full | ❌ No | Core Nostr signing/verification is missing on iOS. |
|
||||
| | LibSodium (ChaCha20, Poly1305) | ✅ Full | ❌ No | AEAD and stream ciphers are unimplemented. |
|
||||
| | AES Encryption (CBC & GCM) | ✅ Full | ❌ No | `AESCBC` and `AESGCM` are stubs on iOS. |
|
||||
| | Hashing (SHA-256, etc.) | ✅ Full | ❌ No | `DigestInstance` is unimplemented. |
|
||||
| | MAC (HmacSHA256, etc.) | ✅ Full | ❌ No | `MacInstance` is unimplemented. |
|
||||
| **Data & Serialization** | JSON Mapping (Optimized) | ✅ Full | ❌ No | `OptimizedJsonMapper` is a stub; cannot parse/serialize Events. |
|
||||
| | GZip Compression | ✅ Full | ❌ No | `GZip` implementation is missing. |
|
||||
| | BitSet | ✅ Full | ❌ No | `BitSet` utility is unimplemented. |
|
||||
| | LargeCache | ✅ Full | ❌ No | `LargeCache` methods (get, keys, size, etc.) are stubs. |
|
||||
| **NIP Support** | NIP-96 (File Storage Info) | ✅ Full | ❌ No | `ServerInfoParser` is unimplemented. |
|
||||
| | NIP-46 (Remote Signer) | ✅ Full | ⚠️ Partial | Some methods in `NostrSignerRemote` are unimplemented in `commonMain`. |
|
||||
| | NIP-03 (OTS / Timestamps) | ✅ Full | ❌ No | `BitcoinExplorer` and `RemoteCalendar` have stubs in `commonMain`. |
|
||||
| **Utilities** | URL Encoding / Decoding | ✅ Full | ❌ No | `UrlEncoder` and `URLs.ios.kt` are unimplemented. |
|
||||
| | Unicode Normalization | ✅ Full | ❌ No | `UnicodeNormalizer` is a stub. |
|
||||
| | Platform Logging | ✅ Full | ✅ Full | iOS uses `NSLog`, Android uses standard Log. |
|
||||
| | Current Time | ✅ Full | ✅ Full | Implemented using `NSDate` on iOS. |
|
||||
| Feature Category | Feature / Component | Android / JVM Support | iOS Support | Notes |
|
||||
|:-------------------------|:-------------------------------|:---------------------:|:-----------:|:-----------------------------------------------------------------------|
|
||||
| **Cryptography** | Secp256k1 (Schnorr, Keys) | ✅ Full | ✅ Full | |
|
||||
| | LibSodium (ChaCha20, Poly1305) | ✅ Full | ✅ Full | |
|
||||
| | AES Encryption (CBC & GCM) | ✅ Full | ✅ Full | |
|
||||
| | Hashing (SHA-256, etc.) | ✅ Full | ✅ Full | |
|
||||
| | MAC (HmacSHA256, etc.) | ✅ Full | ✅ Full | |
|
||||
| **Data & Serialization** | JSON Mapping (Optimized) | ✅ Full | ✅ Full | A fully custom implementation exists in `commonMain`. |
|
||||
| | GZip Compression | ✅ Full | ✅ Full | |
|
||||
| | BitSet | ✅ Full | ✅ Full | |
|
||||
| | LargeCache | ✅ Full | ✅ Full | |
|
||||
| **NIP Support** | NIP-96 (File Storage Info) | ✅ Full | ✅ Full | |
|
||||
| | NIP-46 (Remote Signer) | ✅ Full | ⚠️ Partial | Some methods in `NostrSignerRemote` are unimplemented in `commonMain`. |
|
||||
| | NIP-03 (OTS / Timestamps) | ✅ Full | ❌ No | `BitcoinExplorer` and `RemoteCalendar` have stubs in `commonMain`. |
|
||||
| **Utilities** | URL Encoding / Decoding | ✅ Full | ✅ Full | |
|
||||
| | Unicode Normalization | ✅ Full | ✅ Full | |
|
||||
| | Platform Logging | ✅ Full | ✅ Full | iOS uses `NSLog`, Android uses standard Log. |
|
||||
| | Current Time | ✅ Full | ✅ Full | Implemented using `NSDate` on iOS. |
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -54,9 +54,9 @@ android {
|
||||
applicationId = "com.vitorpamplona.amethyst"
|
||||
minSdk = libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode = 434
|
||||
versionName = generateVersionName("1.06.2")
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"0b6af7660b44215b0edf9c39a1c9c0b4aafba7aba1ae28665ffcecb1a9717195\""
|
||||
versionCode = 439
|
||||
versionName = generateVersionName("1.07.3")
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"12cd4bce977ed53502cf121ecba89a190ab02685333c8f230bac35b04f920eeb\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -70,9 +70,9 @@ class ThreadDualAxisChartAssemblerTest {
|
||||
Account(
|
||||
settings = AccountSettings(keyPair = keyPair),
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
|
||||
nwcFilterAssembler = NWCPaymentFilterAssembler(client),
|
||||
otsResolverBuilder = EmptyOtsResolverBuilder,
|
||||
geolocationFlow = { MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading) },
|
||||
nwcFilterAssembler = { NWCPaymentFilterAssembler(client) },
|
||||
otsResolverBuilder = { EmptyOtsResolverBuilder.build() },
|
||||
cache = LocalCache,
|
||||
client = client,
|
||||
scope = scope,
|
||||
|
||||
@@ -46,7 +46,7 @@ object PushDistributorHandler : PushDistributorActions {
|
||||
|
||||
fun setEndpoint(newEndpoint: String) {
|
||||
endpointInternal = newEndpoint
|
||||
Log.d("PushHandler", "New endpoint saved : $endpointInternal")
|
||||
Log.d("PushHandler") { "New endpoint saved : $endpointInternal" }
|
||||
}
|
||||
|
||||
fun removeEndpoint() {
|
||||
|
||||
@@ -53,7 +53,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
instance: String,
|
||||
) {
|
||||
val messageStr = message.content.decodeToString()
|
||||
Log.d(TAG, "New message $messageStr for Instance: $instance")
|
||||
Log.d(TAG) { "New message $messageStr for Instance: $instance" }
|
||||
scope.launch {
|
||||
try {
|
||||
parseMessage(messageStr)?.let {
|
||||
@@ -61,7 +61,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.d(TAG, "Message could not be parsed: ${e.message}")
|
||||
Log.d(TAG) { "Message could not be parsed: ${e.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
) {
|
||||
val sanitizedEndpoint = if (endpoint.url.endsWith("?up=1")) endpoint.url.dropLast(5) else endpoint.url
|
||||
if (sanitizedEndpoint != pushHandler.getSavedEndpoint()) {
|
||||
Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint")
|
||||
Log.d(TAG) { "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint" }
|
||||
pushHandler.setEndpoint(sanitizedEndpoint)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) {
|
||||
@@ -97,7 +97,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
NotificationUtils.getOrCreateDMChannel(appContext)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint")
|
||||
Log.d(TAG) { "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
reason: FailedReason,
|
||||
instance: String,
|
||||
) {
|
||||
Log.d(TAG, "Registration failed for Instance: $instance")
|
||||
Log.d(TAG) { "Registration failed for Instance: $instance" }
|
||||
pushHandler.forceRemoveDistributor(context)
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
instance: String,
|
||||
) {
|
||||
val removedEndpoint = pushHandler.getSavedEndpoint()
|
||||
Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance")
|
||||
Log.d(TAG) { "Endpoint: $removedEndpoint removed for Instance: $instance" }
|
||||
Log.d(TAG, "App is unregistered. ")
|
||||
pushHandler.forceRemoveDistributor(context)
|
||||
pushHandler.removeEndpoint()
|
||||
|
||||
@@ -35,7 +35,7 @@ object PushNotificationUtils {
|
||||
accounts: List<AccountInfo>,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
) = with(Dispatchers.IO) {
|
||||
if (!pushHandler.savedDistributorExists()) return
|
||||
if (!pushHandler.savedDistributorExists()) return@with
|
||||
|
||||
val currentDistributor = PushDistributorHandler.getSavedDistributor()
|
||||
PushDistributorHandler.saveDistributor(currentDistributor)
|
||||
|
||||
@@ -23,10 +23,12 @@ package com.vitorpamplona.amethyst
|
||||
import android.app.Application
|
||||
import com.vitorpamplona.amethyst.service.logging.Logging
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.LogLevel
|
||||
|
||||
class Amethyst : Application() {
|
||||
init {
|
||||
Log.d("AmethystApp", "Creating App $this")
|
||||
Log.minLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.ERROR
|
||||
Log.d("AmethystApp") { "Creating App $this" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -36,7 +38,7 @@ class Amethyst : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d("AmethystApp", "onCreate $this")
|
||||
Log.d("AmethystApp") { "onCreate $this" }
|
||||
instance = AppModules(this)
|
||||
|
||||
if (isDebug) {
|
||||
@@ -48,7 +50,7 @@ class Amethyst : Application() {
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
Log.d("AmethystApp", "onTerminate $this")
|
||||
Log.d("AmethystApp") { "onTerminate $this" }
|
||||
instance.terminate(this)
|
||||
}
|
||||
|
||||
@@ -59,7 +61,7 @@ class Amethyst : Application() {
|
||||
*/
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
Log.d("AmethystApp", "onTrimMemory $level")
|
||||
Log.d("AmethystApp") { "onTrimMemory $level" }
|
||||
instance.trim()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,15 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.memory.MemoryCache
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.UiSettings
|
||||
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
|
||||
import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier
|
||||
import com.vitorpamplona.amethyst.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder
|
||||
@@ -63,9 +64,11 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinder
|
||||
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||
import com.vitorpamplona.amethyst.ui.resourceCacheInit
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager
|
||||
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
@@ -87,6 +90,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -94,6 +98,7 @@ import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
|
||||
class AppModules(
|
||||
@@ -109,31 +114,54 @@ class AppModules(
|
||||
|
||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
|
||||
|
||||
// Pre-load both preference DataStores in parallel on IO threads.
|
||||
// Both constructors use runBlocking internally, so starting them concurrently
|
||||
// reduces total blocking time from (torPrefs + uiPrefs) to ~max(torPrefs, uiPrefs).
|
||||
private val uiPrefsDeferred =
|
||||
applicationIOScope.async {
|
||||
val prefs = UiSharedPreferences.uiPreferences(appContext) ?: UiSettings()
|
||||
UiSharedPreferences(prefs, appContext, applicationIOScope)
|
||||
}
|
||||
|
||||
private val torPrefsDeferred =
|
||||
applicationIOScope.async {
|
||||
val prefs = TorSharedPreferences.torPreferences(appContext) ?: TorSettings()
|
||||
TorSharedPreferences(prefs, appContext, applicationIOScope)
|
||||
}
|
||||
|
||||
// Blocking load of UI Preferences to avoid theme/language blinking
|
||||
val uiPrefs by lazy {
|
||||
UiSharedPreferences(appContext, applicationIOScope)
|
||||
Log.d("AppModules", "UiSharedPreferences Init")
|
||||
runBlocking { uiPrefsDeferred.await() }
|
||||
}
|
||||
|
||||
// Blocking load of Tor Settings to avoid connection leaks
|
||||
val torPrefs by lazy {
|
||||
TorSharedPreferences(appContext, applicationIOScope)
|
||||
Log.d("AppModules", "TorSharedPreferences Init")
|
||||
runBlocking { torPrefsDeferred.await() }
|
||||
}
|
||||
|
||||
// Namecoin ElectrumX server preferences (global, like Tor settings)
|
||||
val namecoinPrefs by lazy {
|
||||
Log.d("AppModules", "NamecoinSharedPreferences Init")
|
||||
NamecoinSharedPreferences(appContext, applicationIOScope)
|
||||
}
|
||||
|
||||
// OTS blockchain explorer preferences (global, like Tor settings)
|
||||
val otsPrefs by lazy {
|
||||
Log.d("AppModules", "OtsSharedPreferences Init")
|
||||
OtsSharedPreferences(appContext, applicationIOScope)
|
||||
}
|
||||
|
||||
// App services that should be run as soon as there are subscribers to their flows
|
||||
val locationManager = LocationState(appContext, applicationIOScope)
|
||||
val locationManager by lazy {
|
||||
Log.d("AppModules", "LocationManager Init")
|
||||
LocationState(appContext, applicationIOScope)
|
||||
}
|
||||
val connManager = ConnectivityManager(appContext, applicationIOScope)
|
||||
|
||||
val uiState by lazy {
|
||||
Log.d("AppModules", "UiSettingsState Init")
|
||||
UiSettingsState(uiPrefs.value, connManager.isMobileOrFalse, applicationIOScope)
|
||||
}
|
||||
|
||||
@@ -158,40 +186,67 @@ class AppModules(
|
||||
// Offers easy methods to know when connections are happening through Tor or not
|
||||
val roleBasedHttpClientBuilder = RoleBasedHttpClientBuilder(okHttpClients, torPrefs.value)
|
||||
|
||||
// Custom fetcher that considers tor settings and avoids forwarding.
|
||||
val nip05Fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05)
|
||||
val electrumXClient by lazy {
|
||||
Log.d("AppModules", "ElectrumXClient Init")
|
||||
val client =
|
||||
ElectrumXClient(
|
||||
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
|
||||
)
|
||||
applicationIOScope.launch {
|
||||
try {
|
||||
val pinnedCerts = namecoinPrefs.loadPinnedCerts()
|
||||
if (pinnedCerts.isNotEmpty()) {
|
||||
client.setDynamicCerts(pinnedCerts)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Non-fatal — defaults will still work
|
||||
}
|
||||
}
|
||||
client
|
||||
}
|
||||
|
||||
val namecoinResolver =
|
||||
NamecoinNameResolver(
|
||||
electrumxClient =
|
||||
ElectrumXClient(
|
||||
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
|
||||
),
|
||||
serverListProvider = {
|
||||
// User-configured custom servers take priority
|
||||
namecoinPrefs.customServersOrNull
|
||||
?: if (roleBasedHttpClientBuilder.shouldUseTorForNIP05("https://electrumx.example.com")) {
|
||||
TOR_ELECTRUMX_SERVERS
|
||||
} else {
|
||||
DEFAULT_ELECTRUMX_SERVERS
|
||||
}
|
||||
},
|
||||
)
|
||||
val nip05Client = Nip05Client(nip05Fetcher, namecoinResolver)
|
||||
val namecoinResolver by
|
||||
lazy {
|
||||
Log.d("AppModules", "Namecoin Resolver Init")
|
||||
NamecoinNameResolver(
|
||||
electrumxClient = electrumXClient,
|
||||
serverListProvider = {
|
||||
// User-configured custom servers take priority
|
||||
namecoinPrefs.customServersOrNull
|
||||
?: if (roleBasedHttpClientBuilder.shouldUseTorForNIP05("https://electrumx.example.com")) {
|
||||
TOR_ELECTRUMX_SERVERS
|
||||
} else {
|
||||
DEFAULT_ELECTRUMX_SERVERS
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Application-wide block height request cache
|
||||
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
|
||||
val nip05Client by
|
||||
lazy {
|
||||
Log.d("AppModules", "NIP05Client Init")
|
||||
Nip05Client(
|
||||
fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05),
|
||||
namecoinResolverBuilder = { namecoinResolver },
|
||||
)
|
||||
}
|
||||
|
||||
val otsResolverBuilder: TorAwareOkHttpOtsResolverBuilder =
|
||||
TorAwareOkHttpOtsResolverBuilder(
|
||||
roleBasedHttpClientBuilder::okHttpClientForMoney,
|
||||
roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
|
||||
otsBlockHeightCache,
|
||||
customExplorerUrl = { otsPrefs.current.normalizedUrl() },
|
||||
)
|
||||
val otsResolverBuilder by
|
||||
lazy {
|
||||
Log.d("AppModules", "OtsResolverBuilder Init")
|
||||
TorAwareOkHttpOtsResolverBuilder(
|
||||
roleBasedHttpClientBuilder::okHttpClientForMoney,
|
||||
roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
|
||||
OtsBlockHeightCache(),
|
||||
customExplorerUrl = { otsPrefs.current.normalizedUrl() },
|
||||
)
|
||||
}
|
||||
|
||||
// Application-wide ots verification cache
|
||||
val otsVerifCache by lazy { VerificationStateCache(otsResolverBuilder) }
|
||||
val otsVerifCache by lazy {
|
||||
Log.d("AppModules", "OtsCache Init")
|
||||
VerificationStateCache(otsResolverBuilder)
|
||||
}
|
||||
|
||||
val torEvaluatorFlow =
|
||||
TorRelayState(
|
||||
@@ -209,7 +264,7 @@ class AppModules(
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// Connects the NostrClient class with okHttp
|
||||
// Connects the INostrClient class with okHttp
|
||||
val websocketBuilder =
|
||||
OkHttpWebSocket.Builder { url ->
|
||||
val useTor = torEvaluatorFlow.flow.value.useTor(url)
|
||||
@@ -243,7 +298,12 @@ class AppModules(
|
||||
val authCoordinator = AuthCoordinator(client, applicationIOScope)
|
||||
|
||||
// Tries to verify new OTS events when they arrive.
|
||||
val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationIOScope)
|
||||
val otsEventVerifier =
|
||||
IncomingOtsEventVerifier(
|
||||
otsVerifCache = { otsVerifCache },
|
||||
cache = cache,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// Tracks if it is possible to connect to relays.
|
||||
val failureTracker = RelayOfflineTracker(client)
|
||||
@@ -269,10 +329,10 @@ class AppModules(
|
||||
// keeps all accounts live
|
||||
val accountsCache =
|
||||
AccountCacheState(
|
||||
geolocationFlow = locationManager.geohashStateFlow,
|
||||
nwcFilterAssembler = sources.nwc,
|
||||
contentResolverFn = ::contentResolverFn,
|
||||
otsResolverBuilder = otsResolverBuilder,
|
||||
geolocationFlow = { locationManager.geohashStateFlow },
|
||||
nwcFilterAssembler = { sources.nwc },
|
||||
contentResolverFn = { appContext.contentResolver },
|
||||
otsResolverBuilder = { otsResolverBuilder.build() },
|
||||
cache = cache,
|
||||
client = client,
|
||||
)
|
||||
@@ -280,8 +340,8 @@ class AppModules(
|
||||
val sessionManager =
|
||||
AccountSessionManager(
|
||||
accountsCache = accountsCache,
|
||||
nip05Client = nip05Client,
|
||||
client = client,
|
||||
nip05ClientBuilder = { nip05Client },
|
||||
clientBuilder = { client },
|
||||
localPreferences = LocalPreferences,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
@@ -307,7 +367,8 @@ class AppModules(
|
||||
}
|
||||
}
|
||||
|
||||
val blossomResolver =
|
||||
val blossomResolver by lazy {
|
||||
Log.d("AppModules", "BlossomServerResolver Init")
|
||||
BlossomServerResolver(
|
||||
loggedInUsers = { listOfNotNull(sessionManager.loggedInAccount()?.pubKey) },
|
||||
blossomServers = { addressesToSubscribe ->
|
||||
@@ -323,45 +384,60 @@ class AppModules(
|
||||
},
|
||||
httpClientBuilder = roleBasedHttpClientBuilder,
|
||||
)
|
||||
}
|
||||
|
||||
// Organizes cache clearing
|
||||
val trimmingService = MemoryTrimmingService(cache)
|
||||
val trimmingService by
|
||||
lazy {
|
||||
MemoryTrimmingService(cache)
|
||||
}
|
||||
|
||||
// as new accounts are loaded, updates the state of the TorRelaySettings, which produces new TorRelayEvaluator
|
||||
// and reconnects relays if the configuration has been changed.
|
||||
val accountsTorStateConnector = AccountsTorStateConnector(accountsCache, torEvaluatorFlow, applicationIOScope)
|
||||
|
||||
// saves the .content of NIP-95 blobs in disk to save memory
|
||||
val nip95cache: File by lazy { Nip95CacheFactory.new(appContext) }
|
||||
val nip95cache: File by lazy {
|
||||
Log.d("AppModules", "NIP95 Cache Init")
|
||||
Nip95CacheFactory.new(appContext)
|
||||
}
|
||||
|
||||
// local video cache with disk + memory
|
||||
val videoCache: VideoCache by lazy { VideoCacheFactory.new(appContext) }
|
||||
val videoCache: VideoCache by lazy {
|
||||
Log.d("AppModules", "VideoCache Init")
|
||||
VideoCacheFactory.new(appContext)
|
||||
}
|
||||
|
||||
// image cache in disk for coil
|
||||
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(appContext) }
|
||||
val diskCache: DiskCache by lazy {
|
||||
Log.d("AppModules", "ImageCacheFactory Init")
|
||||
ImageCacheFactory.newDisk(appContext)
|
||||
}
|
||||
|
||||
// image cache in memory for coil
|
||||
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(appContext) }
|
||||
val memoryCache: MemoryCache by lazy {
|
||||
Log.d("AppModules", "MemoryCache Init")
|
||||
ImageCacheFactory.newMemory(appContext)
|
||||
}
|
||||
|
||||
// crash report storage
|
||||
val crashReportCache: CrashReportCache by lazy { CrashReportCache(appContext) }
|
||||
val crashReportCache = CrashReportCache(appContext)
|
||||
|
||||
// cache for NIP-11 documents
|
||||
val nip11Cache: Nip11CachedRetriever by lazy {
|
||||
Log.d("AppModules", "Nip11CachedRetriever Init")
|
||||
Nip11CachedRetriever(torEvaluatorFlow::okHttpClientForRelay)
|
||||
}
|
||||
|
||||
fun contentResolverFn(): ContentResolver = appContext.contentResolver
|
||||
|
||||
fun setImageLoader() {
|
||||
Log.d("AppModules", "ImageLoaderSetup Init")
|
||||
ImageLoaderSetup.setup(
|
||||
app = appContext,
|
||||
diskCache = { diskCache },
|
||||
memoryCache = { memoryCache },
|
||||
blossomServerResolver = blossomResolver,
|
||||
) { url ->
|
||||
okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(url))
|
||||
}
|
||||
blossomServerResolver = { blossomResolver },
|
||||
callFactory = { okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(appContext, npub)
|
||||
@@ -380,14 +456,20 @@ class AppModules(
|
||||
|
||||
// initializes diskcache on an IO thread.
|
||||
applicationIOScope.launch {
|
||||
// preloads tor preferences
|
||||
torPrefs
|
||||
// Sets Coil - Tor - OkHttp link
|
||||
setImageLoader()
|
||||
}
|
||||
|
||||
// initializes diskcache on an IO thread.
|
||||
applicationIOScope.launch {
|
||||
// Sets Coil - Tor - OkHttp link
|
||||
setImageLoader()
|
||||
uiState
|
||||
}
|
||||
|
||||
// LRUCache should not be instanciated in the Main thread due to blocking
|
||||
applicationIOScope.launch {
|
||||
CachedRobohash
|
||||
resourceCacheInit()
|
||||
}
|
||||
|
||||
// registers to receive events
|
||||
@@ -395,15 +477,10 @@ class AppModules(
|
||||
|
||||
// initializes diskcache on an IO thread.
|
||||
applicationIOScope.launch {
|
||||
// Sets Coil - Tor - OkHttp link
|
||||
delay(3000)
|
||||
// Prepares video cache later
|
||||
delay(10_000)
|
||||
videoCache
|
||||
}
|
||||
|
||||
applicationIOScope.launch {
|
||||
// Eagerly initialize OtsSharedPreferences off the main thread
|
||||
otsPrefs
|
||||
}
|
||||
}
|
||||
|
||||
fun terminate(appContext: Context) {
|
||||
|
||||
@@ -46,19 +46,19 @@ fun debugState(context: Context) {
|
||||
|
||||
val jvmHeapAllocatedMb = totalMemoryMb - freeMemoryMb
|
||||
|
||||
Log.d(STATE_DUMP_TAG, "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB")
|
||||
Log.d(STATE_DUMP_TAG) { "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB" }
|
||||
|
||||
val nativeHeap = Debug.getNativeHeapAllocatedSize() / (1024 * 1024)
|
||||
val maxNative = Debug.getNativeHeapSize() / (1024 * 1024)
|
||||
|
||||
Log.d(STATE_DUMP_TAG, "Total Native Heap Allocated: $nativeHeap/$maxNative MB")
|
||||
Log.d(STATE_DUMP_TAG) { "Total Native Heap Allocated: $nativeHeap/$maxNative MB" }
|
||||
|
||||
val activityManager: ActivityManager? = context.getSystemService()
|
||||
if (activityManager != null) {
|
||||
val isLargeHeap = (context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP) != 0
|
||||
val memClass = if (isLargeHeap) activityManager.largeMemoryClass else activityManager.memoryClass
|
||||
|
||||
Log.d(STATE_DUMP_TAG, "Memory Class $memClass MB (largeHeap $isLargeHeap)")
|
||||
Log.d(STATE_DUMP_TAG) { "Memory Class $memClass MB (largeHeap $isLargeHeap)" }
|
||||
}
|
||||
|
||||
Log.d(
|
||||
@@ -68,14 +68,8 @@ fun debugState(context: Context) {
|
||||
.size() + "/" + normalizedUrls.size(),
|
||||
)
|
||||
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB",
|
||||
)
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB",
|
||||
)
|
||||
Log.d(STATE_DUMP_TAG) { "Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB" }
|
||||
Log.d(STATE_DUMP_TAG) { "Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB" }
|
||||
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
@@ -130,13 +124,12 @@ fun debugState(context: Context) {
|
||||
LocalCache.ephemeralChannels.values().sumOf { it.notes.size() },
|
||||
)
|
||||
LocalCache.chatroomList.forEach { key, room ->
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
Log.d(STATE_DUMP_TAG) {
|
||||
"Private Chats $key: " +
|
||||
room.rooms.size() +
|
||||
" / " +
|
||||
room.rooms.sumOf { key, value -> value.messages.size },
|
||||
)
|
||||
room.rooms.sumOf { key, value -> value.messages.size }
|
||||
}
|
||||
}
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
@@ -173,10 +166,10 @@ fun debugState(context: Context) {
|
||||
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory()?.toLong() ?: 0L })
|
||||
|
||||
qttNotes.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
|
||||
Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB ")
|
||||
Log.d(STATE_DUMP_TAG) { "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB " }
|
||||
}
|
||||
qttAddressables.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
|
||||
Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB ")
|
||||
Log.d(STATE_DUMP_TAG) { "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB " }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +181,7 @@ inline fun <T> logTime(
|
||||
if (isDebug) {
|
||||
val (result, elapsed) = measureTimedValue(block)
|
||||
if (elapsed.inWholeMilliseconds > minToReportMs) {
|
||||
Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage")
|
||||
Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage" }
|
||||
}
|
||||
result
|
||||
} else {
|
||||
@@ -203,7 +196,7 @@ inline fun <T> logTime(
|
||||
if (isDebug) {
|
||||
val (result, elapsed) = measureTimedValue(block)
|
||||
if (elapsed.inWholeMilliseconds > minToReportMs) {
|
||||
Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}")
|
||||
Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}" }
|
||||
}
|
||||
result
|
||||
} else {
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
@@ -59,9 +58,11 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -96,6 +97,10 @@ private object PrefKeys {
|
||||
const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList"
|
||||
const val DEFAULT_NOTIFICATION_FOLLOW_LIST = "defaultNotificationFollowList"
|
||||
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
|
||||
const val DEFAULT_POLLS_FOLLOW_LIST = "defaultPollsFollowList"
|
||||
const val DEFAULT_PICTURES_FOLLOW_LIST = "defaultPicturesFollowList"
|
||||
const val DEFAULT_SHORTS_FOLLOW_LIST = "defaultShortsFollowList"
|
||||
const val DEFAULT_LONGS_FOLLOW_LIST = "defaultLongsFollowList"
|
||||
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
||||
const val LATEST_USER_METADATA = "latestUserMetadata"
|
||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||
@@ -253,7 +258,7 @@ object LocalPreferences {
|
||||
val prefsDir = File(prefsDirPath)
|
||||
prefsDir.list()?.forEach {
|
||||
if (it.contains(npub) && !File(prefsDir, it).delete()) {
|
||||
Log.w("LocalPreferences", "Failed to delete preference file: $it")
|
||||
Log.w("LocalPreferences") { "Failed to delete preference file: $it" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,7 +286,7 @@ object LocalPreferences {
|
||||
*/
|
||||
@SuppressLint("ApplySharedPref")
|
||||
suspend fun deleteAccount(accountInfo: AccountInfo) {
|
||||
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}")
|
||||
Log.d("LocalPreferences") { "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}" }
|
||||
withContext(Dispatchers.IO) {
|
||||
encryptedPreferences(accountInfo.npub).edit(commit = true) { clear() }
|
||||
removeAccount(accountInfo)
|
||||
@@ -330,6 +335,11 @@ object LocalPreferences {
|
||||
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, JsonMapper.toJson(settings.defaultNotificationFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, JsonMapper.toJson(settings.defaultDiscoveryFollowList.value))
|
||||
|
||||
putString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPollsFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPicturesFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultShortsFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLongsFollowList.value))
|
||||
|
||||
putOrRemove(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, settings.zapPaymentRequest.value?.denormalize())
|
||||
|
||||
putOrRemove(PrefKeys.LATEST_CONTACT_LIST, settings.backupContactList)
|
||||
@@ -445,108 +455,155 @@ object LocalPreferences {
|
||||
}
|
||||
|
||||
private suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): AccountSettings? {
|
||||
Log.d("LocalPreferences", "Load account from file $npub")
|
||||
Log.d("LocalPreferences") { "Load account from file $npub" }
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
checkNotInMainThread()
|
||||
|
||||
return@withContext with(encryptedPreferences(npub)) {
|
||||
Log.d("LocalPreferences") { "Load account from file $npub - opened file" }
|
||||
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
|
||||
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
|
||||
val externalSignerPackageName =
|
||||
getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
|
||||
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
|
||||
val externalSignerPackageName = getString(PrefKeys.SIGNER_PACKAGE_NAME, null) ?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
|
||||
|
||||
val defaultHomeFollowList = parseOrNull<TopFilter>(PrefKeys.DEFAULT_HOME_FOLLOW_LIST) ?: TopFilter.AllFollows
|
||||
val defaultStoriesFollowList = parseOrNull<TopFilter>(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST) ?: TopFilter.Global
|
||||
val defaultNotificationFollowList = parseOrNull<TopFilter>(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST) ?: TopFilter.Global
|
||||
val defaultDiscoveryFollowList = parseOrNull<TopFilter>(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST) ?: TopFilter.Global
|
||||
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
|
||||
|
||||
Log.d("LocalPreferences") { "Load account from file $npub - keys ready" }
|
||||
|
||||
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
|
||||
val defaultFileServer = parseOrNull<ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
|
||||
val stripLocationOnUpload = getBoolean(PrefKeys.STRIP_LOCATION_ON_UPLOAD, true)
|
||||
|
||||
val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
|
||||
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
||||
|
||||
val latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
|
||||
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
|
||||
val latestDmRelayList = parseEventOrNull<ChatMessageRelayListEvent>(PrefKeys.LATEST_DM_RELAY_LIST)
|
||||
val latestNip65RelayList = parseEventOrNull<AdvertisedRelayListEvent>(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
||||
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||
val latestIndexRelayList = parseEventOrNull<IndexerRelayListEvent>(PrefKeys.LATEST_INDEX_RELAY_LIST)
|
||||
val latestRelayFeedsList = parseEventOrNull<RelayFeedsListEvent>(PrefKeys.LATEST_RELAY_FEEDS_LIST)
|
||||
val latestBlockedRelayList = parseEventOrNull<BlockedRelayListEvent>(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
|
||||
val latestTrustedRelayList = parseEventOrNull<TrustedRelayListEvent>(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
|
||||
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
|
||||
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
||||
val latestChannelList = parseEventOrNull<ChannelListEvent>(PrefKeys.LATEST_CHANNEL_LIST)
|
||||
val latestCommunityList = parseEventOrNull<CommunityListEvent>(PrefKeys.LATEST_COMMUNITY_LIST)
|
||||
val latestHashtagList = parseEventOrNull<HashtagListEvent>(PrefKeys.LATEST_HASHTAG_LIST)
|
||||
val latestGeohashList = parseEventOrNull<GeohashListEvent>(PrefKeys.LATEST_GEOHASH_LIST)
|
||||
val latestEphemeralList = parseEventOrNull<EphemeralChatListEvent>(PrefKeys.LATEST_EPHEMERAL_LIST)
|
||||
val latestTrustProviderList = parseEventOrNull<TrustProviderListEvent>(PrefKeys.LATEST_TRUST_PROVIDER_LIST)
|
||||
val latestPaymentTargets = parseEventOrNull<PaymentTargetsEvent>(PrefKeys.LATEST_PAYMENT_TARGETS)
|
||||
|
||||
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
|
||||
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
||||
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
|
||||
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
|
||||
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
||||
|
||||
val defaultHomeFollowListStr = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null)
|
||||
val defaultStoriesFollowListStr = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null)
|
||||
val defaultNotificationFollowListStr = getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null)
|
||||
val defaultDiscoveryFollowListStr = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null)
|
||||
|
||||
val defaultPollsFollowListStr = getString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, null)
|
||||
val defaultPicturesFollowListStr = getString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, null)
|
||||
val defaultShortsFollowListStr = getString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, null)
|
||||
val defaultLongsFollowListStr = getString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, null)
|
||||
|
||||
val zapPaymentRequestServerStr = getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)
|
||||
val defaultFileServerStr = getString(PrefKeys.DEFAULT_FILE_SERVER, null)
|
||||
|
||||
val pendingAttestationsStr = getString(PrefKeys.PENDING_ATTESTATIONS, null)
|
||||
val latestUserMetadataStr = getString(PrefKeys.LATEST_USER_METADATA, null)
|
||||
val latestContactListStr = getString(PrefKeys.LATEST_CONTACT_LIST, null)
|
||||
val latestDmRelayListStr = getString(PrefKeys.LATEST_DM_RELAY_LIST, null)
|
||||
val latestNip65RelayListStr = getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)
|
||||
val latestSearchRelayListStr = getString(PrefKeys.LATEST_SEARCH_RELAY_LIST, null)
|
||||
val latestIndexRelayListStr = getString(PrefKeys.LATEST_INDEX_RELAY_LIST, null)
|
||||
val latestRelayFeedsListStr = getString(PrefKeys.LATEST_RELAY_FEEDS_LIST, null)
|
||||
val latestBlockedRelayListStr = getString(PrefKeys.LATEST_BLOCKED_RELAY_LIST, null)
|
||||
val latestTrustedRelayListStr = getString(PrefKeys.LATEST_TRUSTED_RELAY_LIST, null)
|
||||
val latestMuteListStr = getString(PrefKeys.LATEST_MUTE_LIST, null)
|
||||
val latestPrivateHomeRelayListStr = getString(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, null)
|
||||
val latestAppSpecificDataStr = getString(PrefKeys.LATEST_APP_SPECIFIC_DATA, null)
|
||||
val latestChannelListStr = getString(PrefKeys.LATEST_CHANNEL_LIST, null)
|
||||
val latestCommunityListStr = getString(PrefKeys.LATEST_COMMUNITY_LIST, null)
|
||||
val latestHashtagListStr = getString(PrefKeys.LATEST_HASHTAG_LIST, null)
|
||||
val latestGeohashListStr = getString(PrefKeys.LATEST_GEOHASH_LIST, null)
|
||||
val latestEphemeralListStr = getString(PrefKeys.LATEST_EPHEMERAL_LIST, null)
|
||||
val latestTrustProviderListStr = getString(PrefKeys.LATEST_TRUST_PROVIDER_LIST, null)
|
||||
val latestPaymentTargetsStr = getString(PrefKeys.LATEST_PAYMENT_TARGETS, null)
|
||||
val lastReadPerRouteStr = getString(PrefKeys.LAST_READ_PER_ROUTE, null)
|
||||
|
||||
Log.d("LocalPreferences") { "Load account from file $npub - before parsing events" }
|
||||
|
||||
val defaultHomeFollowList = async { parseOrNull<TopFilter>(defaultHomeFollowListStr) ?: TopFilter.AllFollows }
|
||||
val defaultStoriesFollowList = async { parseOrNull<TopFilter>(defaultStoriesFollowListStr) ?: TopFilter.Global }
|
||||
val defaultNotificationFollowList = async { parseOrNull<TopFilter>(defaultNotificationFollowListStr) ?: TopFilter.Global }
|
||||
val defaultDiscoveryFollowList = async { parseOrNull<TopFilter>(defaultDiscoveryFollowListStr) ?: TopFilter.Global }
|
||||
|
||||
val defaultPollsFollowList = async { parseOrNull<TopFilter>(defaultPollsFollowListStr) ?: TopFilter.Global }
|
||||
val defaultPicturesFollowList = async { parseOrNull<TopFilter>(defaultPicturesFollowListStr) ?: TopFilter.Global }
|
||||
val defaultShortsFollowList = async { parseOrNull<TopFilter>(defaultShortsFollowListStr) ?: TopFilter.Global }
|
||||
val defaultLongsFollowList = async { parseOrNull<TopFilter>(defaultLongsFollowListStr) ?: TopFilter.Global }
|
||||
|
||||
val zapPaymentRequestServer = async { parseOrNull<Nip47WalletConnect.Nip47URI>(zapPaymentRequestServerStr) }
|
||||
val defaultFileServer = async { parseOrNull<ServerName>(defaultFileServerStr) ?: DEFAULT_MEDIA_SERVERS[0] }
|
||||
|
||||
val pendingAttestations = async { parseOrNull<Map<HexKey, String>>(pendingAttestationsStr) ?: mapOf() }
|
||||
val latestUserMetadata = async { parseEventOrNull<MetadataEvent>(latestUserMetadataStr) }
|
||||
val latestContactList = async { parseEventOrNull<ContactListEvent>(latestContactListStr) }
|
||||
val latestDmRelayList = async { parseEventOrNull<ChatMessageRelayListEvent>(latestDmRelayListStr) }
|
||||
val latestNip65RelayList = async { parseEventOrNull<AdvertisedRelayListEvent>(latestNip65RelayListStr) }
|
||||
val latestSearchRelayList = async { parseEventOrNull<SearchRelayListEvent>(latestSearchRelayListStr) }
|
||||
val latestIndexRelayList = async { parseEventOrNull<IndexerRelayListEvent>(latestIndexRelayListStr) }
|
||||
val latestRelayFeedsList = async { parseEventOrNull<RelayFeedsListEvent>(latestRelayFeedsListStr) }
|
||||
val latestBlockedRelayList = async { parseEventOrNull<BlockedRelayListEvent>(latestBlockedRelayListStr) }
|
||||
val latestTrustedRelayList = async { parseEventOrNull<TrustedRelayListEvent>(latestTrustedRelayListStr) }
|
||||
val latestMuteList = async { parseEventOrNull<MuteListEvent>(latestMuteListStr) }
|
||||
val latestPrivateHomeRelayList = async { parseEventOrNull<PrivateOutboxRelayListEvent>(latestPrivateHomeRelayListStr) }
|
||||
val latestAppSpecificData = async { parseEventOrNull<AppSpecificDataEvent>(latestAppSpecificDataStr) }
|
||||
val latestChannelList = async { parseEventOrNull<ChannelListEvent>(latestChannelListStr) }
|
||||
val latestCommunityList = async { parseEventOrNull<CommunityListEvent>(latestCommunityListStr) }
|
||||
val latestHashtagList = async { parseEventOrNull<HashtagListEvent>(latestHashtagListStr) }
|
||||
val latestGeohashList = async { parseEventOrNull<GeohashListEvent>(latestGeohashListStr) }
|
||||
val latestEphemeralList = async { parseEventOrNull<EphemeralChatListEvent>(latestEphemeralListStr) }
|
||||
val latestTrustProviderList = async { parseEventOrNull<TrustProviderListEvent>(latestTrustProviderListStr) }
|
||||
val latestPaymentTargets = async { parseEventOrNull<PaymentTargetsEvent>(latestPaymentTargetsStr) }
|
||||
|
||||
val lastReadPerRoute =
|
||||
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
|
||||
MutableStateFlow(it.value)
|
||||
} ?: mapOf()
|
||||
async {
|
||||
parseOrNull<Map<String, Long>>(lastReadPerRouteStr)?.mapValues {
|
||||
MutableStateFlow(it.value)
|
||||
} ?: mapOf()
|
||||
}
|
||||
|
||||
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
|
||||
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
|
||||
Log.d("LocalPreferences") { "Load account from file $npub - asyncs created" }
|
||||
|
||||
return@with AccountSettings(
|
||||
keyPair = keyPair,
|
||||
transientAccount = false,
|
||||
externalSignerPackageName = externalSignerPackageName,
|
||||
localRelayServers = MutableStateFlow(localRelayServers),
|
||||
defaultFileServer = defaultFileServer,
|
||||
defaultFileServer = defaultFileServer.await(),
|
||||
stripLocationOnUpload = stripLocationOnUpload,
|
||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
||||
zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer?.normalize()),
|
||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList.await()),
|
||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList.await()),
|
||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList.await()),
|
||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList.await()),
|
||||
defaultPollsFollowList = MutableStateFlow(defaultPollsFollowList.await()),
|
||||
defaultPicturesFollowList = MutableStateFlow(defaultPicturesFollowList.await()),
|
||||
defaultShortsFollowList = MutableStateFlow(defaultShortsFollowList.await()),
|
||||
defaultLongsFollowList = MutableStateFlow(defaultLongsFollowList.await()),
|
||||
zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer.await()?.normalize()),
|
||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
||||
backupUserMetadata = latestUserMetadata,
|
||||
backupContactList = latestContactList,
|
||||
backupNIP65RelayList = latestNip65RelayList,
|
||||
backupDMRelayList = latestDmRelayList,
|
||||
backupSearchRelayList = latestSearchRelayList,
|
||||
backupIndexRelayList = latestIndexRelayList,
|
||||
backupRelayFeedsList = latestRelayFeedsList,
|
||||
backupBlockedRelayList = latestBlockedRelayList,
|
||||
backupTrustedRelayList = latestTrustedRelayList,
|
||||
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
|
||||
backupMuteList = latestMuteList,
|
||||
backupAppSpecificData = latestAppSpecificData,
|
||||
backupChannelList = latestChannelList,
|
||||
backupCommunityList = latestCommunityList,
|
||||
backupHashtagList = latestHashtagList,
|
||||
backupGeohashList = latestGeohashList,
|
||||
backupEphemeralChatList = latestEphemeralList,
|
||||
backupTrustProviderList = latestTrustProviderList,
|
||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
||||
backupUserMetadata = latestUserMetadata.await(),
|
||||
backupContactList = latestContactList.await(),
|
||||
backupNIP65RelayList = latestNip65RelayList.await(),
|
||||
backupDMRelayList = latestDmRelayList.await(),
|
||||
backupSearchRelayList = latestSearchRelayList.await(),
|
||||
backupIndexRelayList = latestIndexRelayList.await(),
|
||||
backupRelayFeedsList = latestRelayFeedsList.await(),
|
||||
backupBlockedRelayList = latestBlockedRelayList.await(),
|
||||
backupTrustedRelayList = latestTrustedRelayList.await(),
|
||||
backupPrivateHomeRelayList = latestPrivateHomeRelayList.await(),
|
||||
backupMuteList = latestMuteList.await(),
|
||||
backupAppSpecificData = latestAppSpecificData.await(),
|
||||
backupChannelList = latestChannelList.await(),
|
||||
backupCommunityList = latestCommunityList.await(),
|
||||
backupHashtagList = latestHashtagList.await(),
|
||||
backupGeohashList = latestGeohashList.await(),
|
||||
backupEphemeralChatList = latestEphemeralList.await(),
|
||||
backupTrustProviderList = latestTrustProviderList.await(),
|
||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute.await()),
|
||||
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
||||
pendingAttestations = MutableStateFlow(pendingAttestations),
|
||||
backupNipA3PaymentTargets = latestPaymentTargets,
|
||||
pendingAttestations = MutableStateFlow(pendingAttestations.await()),
|
||||
backupNipA3PaymentTargets = latestPaymentTargets.await(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Log.d("LocalPreferences", "Loaded account from file $npub")
|
||||
Log.d("LocalPreferences") { "Loaded account from file $npub" }
|
||||
return result
|
||||
}
|
||||
|
||||
private inline fun <reified T : Any> SharedPreferences.parseOrNull(key: String): T? {
|
||||
val value = getString(key, null)
|
||||
private inline fun <reified T : Any> parseOrNull(value: String?): T? {
|
||||
if (value.isNullOrEmpty() || value == "null") {
|
||||
return null
|
||||
}
|
||||
@@ -558,13 +615,12 @@ object LocalPreferences {
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
|
||||
Log.w("LocalPreferences", "Error Decoding ${T::class.java} from Preferences with value $value", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> SharedPreferences.parseEventOrNull(key: String): T? {
|
||||
val value = getString(key, null)
|
||||
private inline fun <reified T> parseEventOrNull(value: String?): T? {
|
||||
if (value.isNullOrEmpty() || value == "null") {
|
||||
return null
|
||||
}
|
||||
@@ -572,7 +628,7 @@ object LocalPreferences {
|
||||
Event.fromJson(value) as T?
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
|
||||
Log.w("LocalPreferences", "Error Decoding ${T::class.java} from Preferences with value $value", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.PinListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
@@ -81,6 +83,7 @@ import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListD
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip62Vanish.VanishRequestsState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
|
||||
@@ -134,7 +137,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchFirst
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
@@ -143,7 +146,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
@@ -174,6 +177,7 @@ import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
@@ -185,6 +189,7 @@ import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||
@@ -197,7 +202,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprov
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||
@@ -213,6 +218,7 @@ import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.containsAny
|
||||
@@ -220,6 +226,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
@@ -234,9 +241,9 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
class Account(
|
||||
val settings: AccountSettings = AccountSettings(KeyPair()),
|
||||
override val signer: NostrSigner,
|
||||
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||
val otsResolverBuilder: OtsResolverBuilder,
|
||||
val geolocationFlow: () -> StateFlow<LocationState.LocationResult>,
|
||||
val nwcFilterAssembler: () -> NWCPaymentFilterAssembler,
|
||||
val otsResolverBuilder: () -> OtsResolver,
|
||||
val cache: LocalCache,
|
||||
val client: INostrClient,
|
||||
val scope: CoroutineScope,
|
||||
@@ -316,9 +323,13 @@ class Account(
|
||||
val hiddenUsers = HiddenUsersState(muteList.flow, blockPeopleList.flow, scope, settings)
|
||||
|
||||
val labeledBookmarkLists = LabeledBookmarkListsState(signer, cache, scope)
|
||||
val oldBookmarkState = OldBookmarkListState(signer, cache, scope)
|
||||
val bookmarkState = BookmarkListState(signer, cache, scope)
|
||||
val pinState = PinListState(signer, cache, scope)
|
||||
val emoji = EmojiPackState(signer, cache, scope)
|
||||
|
||||
val vanish = VanishRequestsState(signer, cache, client, scope)
|
||||
|
||||
val appSpecific = AppSpecificState(signer, cache, scope, settings)
|
||||
|
||||
val blossomServers = BlossomServerListState(signer, cache, scope, settings)
|
||||
@@ -372,10 +383,9 @@ class Account(
|
||||
geohashCache = geohashListDecryptionCache,
|
||||
)
|
||||
|
||||
// App-ready Feeds
|
||||
val liveHomeFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||
fun topNavFilterFlow(listName: MutableStateFlow<TopFilter>) =
|
||||
FeedTopNavFilterState(
|
||||
feedFilterListName = settings.defaultHomeFollowList,
|
||||
feedFilterListName = listName,
|
||||
kind3Follows = kind3FollowList.flow,
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
@@ -387,56 +397,31 @@ class Account(
|
||||
scope = scope,
|
||||
).flow
|
||||
|
||||
// App-ready Feeds
|
||||
val liveHomeFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultHomeFollowList)
|
||||
val liveHomeFollowListsPerRelay = OutboxLoaderState(liveHomeFollowLists, cache, scope).flow
|
||||
|
||||
val liveStoriesFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||
FeedTopNavFilterState(
|
||||
feedFilterListName = settings.defaultStoriesFollowList,
|
||||
kind3Follows = kind3FollowList.flow,
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = defaultGlobalRelays.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
proxyRelays = proxyRelayList.flow,
|
||||
caches = feedDecryptionCaches,
|
||||
signer = signer,
|
||||
scope = scope,
|
||||
).flow
|
||||
|
||||
val liveStoriesFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultStoriesFollowList)
|
||||
val liveStoriesFollowListsPerRelay = OutboxLoaderState(liveStoriesFollowLists, cache, scope).flow
|
||||
|
||||
val liveDiscoveryFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||
FeedTopNavFilterState(
|
||||
feedFilterListName = settings.defaultDiscoveryFollowList,
|
||||
kind3Follows = kind3FollowList.flow,
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = defaultGlobalRelays.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
proxyRelays = proxyRelayList.flow,
|
||||
caches = feedDecryptionCaches,
|
||||
signer = signer,
|
||||
scope = scope,
|
||||
).flow
|
||||
|
||||
val liveDiscoveryFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultDiscoveryFollowList)
|
||||
val liveDiscoveryFollowListsPerRelay = OutboxLoaderState(liveDiscoveryFollowLists, cache, scope).flow
|
||||
|
||||
val liveNotificationFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||
FeedTopNavFilterState(
|
||||
feedFilterListName = settings.defaultNotificationFollowList,
|
||||
kind3Follows = kind3FollowList.flow,
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = defaultGlobalRelays.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
proxyRelays = proxyRelayList.flow,
|
||||
caches = feedDecryptionCaches,
|
||||
signer = signer,
|
||||
scope = scope,
|
||||
).flow
|
||||
|
||||
val liveNotificationFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultNotificationFollowList)
|
||||
val liveNotificationFollowListsPerRelay = OutboxLoaderState(liveNotificationFollowLists, cache, scope).flow
|
||||
|
||||
val livePollsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultPollsFollowList)
|
||||
val livePollsFollowListsPerRelay = OutboxLoaderState(livePollsFollowLists, cache, scope).flow
|
||||
|
||||
val livePicturesFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultPicturesFollowList)
|
||||
val livePicturesFollowListsPerRelay = OutboxLoaderState(livePicturesFollowLists, cache, scope).flow
|
||||
|
||||
val liveShortsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultShortsFollowList)
|
||||
val liveShortsFollowListsPerRelay = OutboxLoaderState(liveShortsFollowLists, cache, scope).flow
|
||||
|
||||
val liveLongsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultLongsFollowList)
|
||||
val liveLongsFollowListsPerRelay = OutboxLoaderState(liveLongsFollowLists, cache, scope).flow
|
||||
|
||||
override fun isWriteable(): Boolean = settings.isWriteable()
|
||||
|
||||
suspend fun updateWarnReports(warnReports: Boolean): Boolean {
|
||||
@@ -597,7 +582,7 @@ class Account(
|
||||
onResponse: (Response?) -> Unit,
|
||||
) {
|
||||
val (event, relay) = nip47SignerState.sendNwcRequest(request, onResponse)
|
||||
client.send(event, setOf(relay))
|
||||
client.publish(event, setOf(relay))
|
||||
}
|
||||
|
||||
suspend fun sendZapPaymentRequestFor(
|
||||
@@ -606,7 +591,7 @@ class Account(
|
||||
onResponse: (Response?) -> Unit,
|
||||
) {
|
||||
val (event, relay) = nip47SignerState.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse)
|
||||
client.send(event, setOf(relay))
|
||||
client.publish(event, setOf(relay))
|
||||
}
|
||||
|
||||
suspend fun createZapRequestFor(
|
||||
@@ -655,7 +640,7 @@ class Account(
|
||||
myRelayList.addAll(it.relays)
|
||||
}
|
||||
|
||||
client.send(deletionEvent, myRelayList)
|
||||
client.publish(deletionEvent, myRelayList)
|
||||
cache.justConsumeMyOwnEvent(deletionEvent)
|
||||
}
|
||||
}
|
||||
@@ -669,7 +654,7 @@ class Account(
|
||||
if (event.pubKey != signer.pubKey) return
|
||||
|
||||
val deletionEvent = signer.sign(DeletionEvent.build(listOf(event)))
|
||||
client.send(deletionEvent, outboxRelays.flow.value + additionalRelays)
|
||||
client.publish(deletionEvent, outboxRelays.flow.value + additionalRelays)
|
||||
cache.justConsumeMyOwnEvent(deletionEvent)
|
||||
}
|
||||
|
||||
@@ -692,7 +677,7 @@ class Account(
|
||||
|
||||
suspend fun boost(note: Note) {
|
||||
RepostAction.repost(note, signer)?.let { event ->
|
||||
client.send(event, computeMyReactionToNote(note, event))
|
||||
client.publish(event, computeMyReactionToNote(note, event))
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
@@ -714,7 +699,7 @@ class Account(
|
||||
event: Event,
|
||||
relays: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
client.send(event, relays)
|
||||
client.publish(event, relays)
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
@@ -930,7 +915,7 @@ class Account(
|
||||
// download the event and send it.
|
||||
noteEvent.host?.let { host ->
|
||||
client
|
||||
.downloadFirstEvent(
|
||||
.fetchFirst(
|
||||
filters =
|
||||
note.relays.associateWith { relay ->
|
||||
listOf(
|
||||
@@ -943,11 +928,11 @@ class Account(
|
||||
},
|
||||
)?.let { downloadedEvent ->
|
||||
val toRelays = computeRelayListToBroadcast(downloadedEvent)
|
||||
client.send(downloadedEvent, toRelays)
|
||||
client.publish(downloadedEvent, toRelays)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client.send(noteEvent, computeRelayListToBroadcast(note))
|
||||
client.publish(noteEvent, computeRelayListToBroadcast(note))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -995,7 +980,7 @@ class Account(
|
||||
val relays = outboxRelays.flow.value + commEvent.relayUrls() + community.relays + (post.author?.inboxRelays() ?: emptyList())
|
||||
|
||||
cache.justConsumeMyOwnEvent(signedEvent)
|
||||
client.send(signedEvent, relays)
|
||||
client.publish(signedEvent, relays)
|
||||
}
|
||||
|
||||
fun sendAutomatic(events: List<Event>) = events.forEach { sendAutomatic(it) }
|
||||
@@ -1003,24 +988,49 @@ class Account(
|
||||
fun sendAutomatic(event: Event?) {
|
||||
if (event == null) return
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
client.send(event, computeRelayListToBroadcast(event))
|
||||
client.publish(event, computeRelayListToBroadcast(event))
|
||||
}
|
||||
|
||||
suspend fun sendWebBookmark(
|
||||
url: String,
|
||||
title: String?,
|
||||
description: String,
|
||||
hashtags: List<String> = emptyList(),
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val template = WebBookmarkEvent.build(url, title, description, tags = hashtags)
|
||||
val signedEvent = signer.sign(template)
|
||||
|
||||
cache.justConsumeMyOwnEvent(signedEvent)
|
||||
client.publish(signedEvent, computeRelayListToBroadcast(signedEvent))
|
||||
}
|
||||
|
||||
suspend fun deleteWebBookmark(event: WebBookmarkEvent) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val template = DeletionEvent.build(listOf(event))
|
||||
val signedEvent = signer.sign(template)
|
||||
|
||||
cache.justConsumeMyOwnEvent(signedEvent)
|
||||
client.publish(signedEvent, computeRelayListToBroadcast(signedEvent))
|
||||
}
|
||||
|
||||
fun sendMyPublicAndPrivateOutbox(event: Event?) {
|
||||
if (event == null) return
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
client.send(event, outboxRelays.flow.value)
|
||||
client.publish(event, outboxRelays.flow.value)
|
||||
}
|
||||
|
||||
fun sendMyPublicAndPrivateOutbox(events: List<Event>) {
|
||||
events.forEach {
|
||||
client.send(it, outboxRelays.flow.value)
|
||||
client.publish(it, outboxRelays.flow.value)
|
||||
cache.justConsumeMyOwnEvent(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendLiterallyEverywhere(event: Event) {
|
||||
client.send(event, followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value)
|
||||
client.publish(event, followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value)
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
@@ -1037,7 +1047,7 @@ class Account(
|
||||
|
||||
cache.justConsumeMyOwnEvent(signedEvent)
|
||||
|
||||
client.send(signedEvent, computeRelayListToBroadcast(signedEvent))
|
||||
client.publish(signedEvent, computeRelayListToBroadcast(signedEvent))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,10 +1083,10 @@ class Account(
|
||||
|
||||
val relayList = computeRelayListToBroadcast(signedEvent)
|
||||
|
||||
client.send(data, relayList = relayList)
|
||||
client.publish(data, relayList = relayList)
|
||||
cache.justConsumeMyOwnEvent(data)
|
||||
|
||||
client.send(signedEvent, relayList = relayList)
|
||||
client.publish(signedEvent, relayList = relayList)
|
||||
cache.justConsumeMyOwnEvent(signedEvent)
|
||||
|
||||
return cache.getNoteIfExists(signedEvent.id)
|
||||
@@ -1097,8 +1107,8 @@ class Account(
|
||||
signedEvent: FileStorageHeaderEvent,
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
client.send(data, relayList = relayList)
|
||||
client.send(signedEvent, relayList = relayList)
|
||||
client.publish(data, relayList = relayList)
|
||||
client.publish(signedEvent, relayList = relayList)
|
||||
}
|
||||
|
||||
fun sendHeader(
|
||||
@@ -1106,7 +1116,7 @@ class Account(
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
onReady: (Note) -> Unit,
|
||||
) {
|
||||
client.send(signedEvent, relayList = relayList)
|
||||
client.publish(signedEvent, relayList = relayList)
|
||||
cache.justConsumeMyOwnEvent(signedEvent)
|
||||
|
||||
cache.getNoteIfExists(signedEvent.id)?.let { onReady(it) }
|
||||
@@ -1253,7 +1263,7 @@ class Account(
|
||||
) {
|
||||
val event = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
client.send(event, relayList)
|
||||
client.publish(event, relayList)
|
||||
}
|
||||
|
||||
suspend fun <T : Event> signAndSendPrivatelyOrBroadcast(
|
||||
@@ -1264,9 +1274,9 @@ class Account(
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
val relays = relayList(event)
|
||||
if (!relays.isNullOrEmpty()) {
|
||||
client.send(event, relays.toSet())
|
||||
client.publish(event, relays.toSet())
|
||||
} else {
|
||||
client.send(event, computeRelayListToBroadcast(event))
|
||||
client.publish(event, computeRelayListToBroadcast(event))
|
||||
}
|
||||
return event
|
||||
}
|
||||
@@ -1286,9 +1296,9 @@ class Account(
|
||||
|
||||
val relayList = computeRelayListToBroadcast(note)
|
||||
|
||||
client.send(event, relayList)
|
||||
client.publish(event, relayList)
|
||||
|
||||
broadcast.forEach { client.send(it, relayList) }
|
||||
broadcast.forEach { client.publish(it, relayList) }
|
||||
|
||||
return event
|
||||
}
|
||||
@@ -1300,10 +1310,19 @@ class Account(
|
||||
val anonymousSigner = NostrSignerInternal(KeyPair())
|
||||
val event = anonymousSigner.sign(template)
|
||||
|
||||
val relayList = nip65RelayList.outboxFlow.value.toSet()
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
val note =
|
||||
if (event is AddressableEvent) {
|
||||
cache.getOrCreateAddressableNote(event.address())
|
||||
} else {
|
||||
cache.getOrCreateNote(event.id)
|
||||
}
|
||||
|
||||
client.send(event, relayList)
|
||||
broadcast.forEach { client.send(it, relayList) }
|
||||
val relayList = computeRelayListToBroadcast(note)
|
||||
|
||||
client.publish(event, relayList)
|
||||
|
||||
broadcast.forEach { client.publish(it, relayList) }
|
||||
|
||||
return event
|
||||
}
|
||||
@@ -1335,7 +1354,7 @@ class Account(
|
||||
extraNotesToBroadcast: List<Event>,
|
||||
) {
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
extraNotesToBroadcast.forEach { client.send(it, relays) }
|
||||
extraNotesToBroadcast.forEach { client.publish(it, relays) }
|
||||
}
|
||||
|
||||
suspend fun createAndSendDraftIgnoreErrors(
|
||||
@@ -1367,9 +1386,9 @@ class Account(
|
||||
|
||||
val relayList = (privateStorageRelayList.flow.value + localRelayList.flow.value + extraRelays).toSet()
|
||||
if (relayList.isNotEmpty()) {
|
||||
client.send(draftEvent, relayList)
|
||||
client.publish(draftEvent, relayList)
|
||||
broadcast.forEach {
|
||||
client.send(it, relayList.toSet())
|
||||
client.publish(it, relayList.toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1396,8 +1415,8 @@ class Account(
|
||||
cache.justConsumeMyOwnEvent(deletionEvent)
|
||||
|
||||
if (relayList.isNotEmpty()) {
|
||||
client.send(deletedDraft, relayList)
|
||||
client.send(deletionEvent, relayList)
|
||||
client.publish(deletedDraft, relayList)
|
||||
client.publish(deletionEvent, relayList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1420,9 +1439,9 @@ class Account(
|
||||
|
||||
val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value
|
||||
if (relayList.isNotEmpty()) {
|
||||
client.send(event, relayList + noteRelays)
|
||||
client.publish(event, relayList + noteRelays)
|
||||
} else {
|
||||
client.send(event, outboxRelays.flow.value + noteRelays)
|
||||
client.publish(event, outboxRelays.flow.value + noteRelays)
|
||||
}
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
@@ -1446,9 +1465,9 @@ class Account(
|
||||
|
||||
val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value
|
||||
if (relayList.isNotEmpty()) {
|
||||
client.send(event, relayList + noteRelays)
|
||||
client.publish(event, relayList + noteRelays)
|
||||
} else {
|
||||
client.send(event, outboxRelays.flow.value + noteRelays)
|
||||
client.publish(event, outboxRelays.flow.value + noteRelays)
|
||||
}
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
@@ -1509,9 +1528,9 @@ class Account(
|
||||
} else {
|
||||
val it = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(it)
|
||||
client.send(it, relayList = relayList)
|
||||
client.publish(it, relayList = relayList)
|
||||
|
||||
mapEntitiesToNotes(quotes).forEach { it.event?.let { client.send(it, relayList = relayList) } }
|
||||
mapEntitiesToNotes(quotes).forEach { it.event?.let { client.publish(it, relayList = relayList) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1554,9 +1573,9 @@ class Account(
|
||||
} else {
|
||||
val it = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(it)
|
||||
client.send(it, relayList = relayList)
|
||||
client.publish(it, relayList = relayList)
|
||||
|
||||
broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relayList) } }
|
||||
broadcastNotes.forEach { it.event?.let { client.publish(it, relayList = relayList) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1581,8 +1600,8 @@ class Account(
|
||||
val newEvent = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(newEvent)
|
||||
|
||||
client.send(newEvent, relayList = relays)
|
||||
client.send(bountyEvent, relayList = relays)
|
||||
client.publish(newEvent, relayList = relays)
|
||||
client.publish(bountyEvent, relayList = relays)
|
||||
}
|
||||
|
||||
suspend fun sendEdit(
|
||||
@@ -1609,9 +1628,9 @@ class Account(
|
||||
val note = cache.getOrCreateNote(event.id)
|
||||
val relayList = computeRelayListToBroadcast(note)
|
||||
|
||||
client.send(event, relayList = relayList)
|
||||
client.publish(event, relayList = relayList)
|
||||
|
||||
broadcast.forEach { client.send(it, relayList) }
|
||||
broadcast.forEach { client.publish(it, relayList) }
|
||||
}
|
||||
|
||||
override suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<PrivateDmEvent>) {
|
||||
@@ -1622,7 +1641,7 @@ class Account(
|
||||
val destinationRelays = recipient?.let { cache.getOrCreateUser(it).dmInboxRelays() } ?: emptyList()
|
||||
|
||||
cache.justConsumeMyOwnEvent(newEvent)
|
||||
client.send(newEvent, outboxRelays.flow.value + destinationRelays)
|
||||
client.publish(newEvent, outboxRelays.flow.value + destinationRelays)
|
||||
}
|
||||
|
||||
override suspend fun sendNip17EncryptedFile(template: EventTemplate<ChatMessageEncryptedFileHeaderEvent>) {
|
||||
@@ -1640,7 +1659,7 @@ class Account(
|
||||
override suspend fun sendGiftWraps(wraps: List<GiftWrapEvent>) {
|
||||
wraps.forEach { wrap ->
|
||||
val relayList = computeRelayListToBroadcast(wrap)
|
||||
client.send(wrap, relayList)
|
||||
client.publish(wrap, relayList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1661,7 +1680,7 @@ class Account(
|
||||
}
|
||||
|
||||
val relayList = computeRelayListToBroadcast(wrap)
|
||||
client.send(wrap, relayList)
|
||||
client.publish(wrap, relayList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1774,6 +1793,86 @@ class Account(
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
suspend fun migrateOldBookmarksToNew() {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val oldList = oldBookmarkState.getBookmarkList() ?: return
|
||||
val oldPublic = oldList.publicBookmarks()
|
||||
val oldPrivate = oldList.privateBookmarks(signer) ?: emptyList()
|
||||
|
||||
if (oldPublic.isEmpty() && oldPrivate.isEmpty()) return
|
||||
|
||||
val existingNewList = bookmarkState.getBookmarkList()
|
||||
|
||||
val newEvent =
|
||||
if (existingNewList != null) {
|
||||
val existingPublic = existingNewList.publicBookmarks()
|
||||
val existingPrivate = existingNewList.privateBookmarks(signer) ?: emptyList()
|
||||
|
||||
val existingPublicIds = existingPublic.map { it.toTagIdOnly().toList() }.toSet()
|
||||
val existingPrivateIds = existingPrivate.map { it.toTagIdOnly().toList() }.toSet()
|
||||
|
||||
val newPublic = oldPublic.filter { it.toTagIdOnly().toList() !in existingPublicIds }
|
||||
val newPrivate = oldPrivate.filter { it.toTagIdOnly().toList() !in existingPrivateIds }
|
||||
|
||||
if (newPublic.isEmpty() && newPrivate.isEmpty()) return
|
||||
|
||||
val mergedPublic = existingPublic + newPublic
|
||||
val mergedPrivate = existingPrivate + newPrivate
|
||||
|
||||
BookmarkListEvent.create(
|
||||
publicBookmarks = mergedPublic,
|
||||
privateBookmarks = mergedPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
BookmarkListEvent.create(
|
||||
publicBookmarks = oldPublic,
|
||||
privateBookmarks = oldPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
|
||||
sendMyPublicAndPrivateOutbox(newEvent)
|
||||
}
|
||||
|
||||
suspend fun addPin(note: Note) {
|
||||
if (!isWriteable() || note.isDraft()) return
|
||||
|
||||
sendMyPublicAndPrivateOutbox(pinState.addPin(note))
|
||||
}
|
||||
|
||||
suspend fun removePin(note: Note) {
|
||||
if (!isWriteable() || note.isDraft()) return
|
||||
|
||||
val event = pinState.removePin(note)
|
||||
if (event != null) {
|
||||
sendMyPublicAndPrivateOutbox(event)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createAddPinEvent(note: Note): Pair<Event, Set<NormalizedRelayUrl>>? {
|
||||
if (!isWriteable() || note.isDraft()) return null
|
||||
|
||||
val event = pinState.addPin(note)
|
||||
val relays = outboxRelays.flow.value
|
||||
|
||||
return event to relays
|
||||
}
|
||||
|
||||
suspend fun createRemovePinEvent(note: Note): Pair<Event, Set<NormalizedRelayUrl>>? {
|
||||
if (!isWriteable() || note.isDraft()) return null
|
||||
|
||||
val event = pinState.removePin(note) ?: return null
|
||||
val relays = outboxRelays.flow.value
|
||||
|
||||
return event to relays
|
||||
}
|
||||
|
||||
fun consumePinEvent(event: Event) {
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
suspend fun createAuthEvent(
|
||||
relay: NormalizedRelayUrl,
|
||||
challenge: String,
|
||||
@@ -1803,7 +1902,7 @@ class Account(
|
||||
onReady: (event: NIP90ContentDiscoveryRequestEvent) -> Unit,
|
||||
) {
|
||||
val relays = nip65RelayList.inboxFlow.value.toSet()
|
||||
val request = NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey.pubkeyHex, signer.pubKey, relays, signer)
|
||||
val request = signer.sign<NIP90ContentDiscoveryRequestEvent>(NIP90ContentDiscoveryRequestEvent.build(dvmPublicKey.pubkeyHex, signer.pubKey, relays))
|
||||
|
||||
val relayList =
|
||||
dvmPublicKey.inboxRelays()?.toSet()?.ifEmpty { null }
|
||||
@@ -1812,7 +1911,7 @@ class Account(
|
||||
cache.justConsumeMyOwnEvent(request)
|
||||
onReady(request)
|
||||
delay(100)
|
||||
client.send(request, relayList)
|
||||
client.publish(request, relayList)
|
||||
}
|
||||
|
||||
fun cachedDecryptContent(note: Note): String? = cachedDecryptContent(note.event)
|
||||
@@ -1970,6 +2069,31 @@ class Account(
|
||||
|
||||
suspend fun saveBlockedRelayList(blockedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays))
|
||||
|
||||
suspend fun requestToVanish(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
reason: String,
|
||||
createdAt: Long,
|
||||
) {
|
||||
if (!isWriteable() || relays.isEmpty()) return
|
||||
|
||||
val template = RequestToVanishEvent.build(relays, reason, createdAt)
|
||||
val signedEvent = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(signedEvent)
|
||||
client.publish(signedEvent, outboxRelays.flow.value + relays.toSet())
|
||||
}
|
||||
|
||||
suspend fun requestToVanishFromEverywhere(
|
||||
reason: String,
|
||||
createdAt: Long,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val template = RequestToVanishEvent.buildVanishFromEverywhere(reason, createdAt)
|
||||
val signedEvent = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(signedEvent)
|
||||
client.publish(signedEvent, followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value)
|
||||
}
|
||||
|
||||
suspend fun sendNip65RelayList(relays: List<AdvertisedRelayInfo>) = sendLiterallyEverywhere(nip65RelayList.saveRelayList(relays))
|
||||
|
||||
suspend fun sendBlossomServersList(servers: List<String>) = sendMyPublicAndPrivateOutbox(blossomServers.saveBlossomServersList(servers))
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
@@ -57,6 +56,7 @@ import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -165,6 +165,10 @@ class AccountSettings(
|
||||
val defaultStoriesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultNotificationFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultDiscoveryFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultPollsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultPicturesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultShortsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultLongsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val zapPaymentRequest: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?> = MutableStateFlow(null),
|
||||
var hideDeleteRequestDialog: Boolean = false,
|
||||
var hideBlockAlertDialog: Boolean = false,
|
||||
@@ -321,6 +325,50 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultPollsFollowList(name: FeedDefinition) {
|
||||
changeDefaultPollsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultPollsFollowList(name: TopFilter) {
|
||||
if (defaultPollsFollowList.value != name) {
|
||||
defaultPollsFollowList.tryEmit(name)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultPicturesFollowList(name: FeedDefinition) {
|
||||
changeDefaultPicturesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultPicturesFollowList(name: TopFilter) {
|
||||
if (defaultPicturesFollowList.value != name) {
|
||||
defaultPicturesFollowList.tryEmit(name)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultShortsFollowList(name: FeedDefinition) {
|
||||
changeDefaultShortsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultShortsFollowList(name: TopFilter) {
|
||||
if (defaultShortsFollowList.value != name) {
|
||||
defaultShortsFollowList.tryEmit(name)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultLongsFollowList(name: FeedDefinition) {
|
||||
changeDefaultLongsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultLongsFollowList(name: TopFilter) {
|
||||
if (defaultLongsFollowList.value != name) {
|
||||
defaultLongsFollowList.tryEmit(name)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// language services
|
||||
// ---
|
||||
|
||||
@@ -87,7 +87,7 @@ class AntiSpamFilter {
|
||||
val link1 = njumpLink(NAddress.create(existingAddress.kind, existingAddress.pubKeyHex, existingAddress.dTag, relay))
|
||||
val link2 = njumpLink(NAddress.create(event.kind, event.pubKey, event.dTag(), relay))
|
||||
|
||||
Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2")
|
||||
Log.w("Duplicated/SPAM") { "${relay?.url} $link1 $link2" }
|
||||
|
||||
// Log down offenders
|
||||
val spammer = logOffender(hash, event)
|
||||
@@ -114,7 +114,7 @@ class AntiSpamFilter {
|
||||
val link1 = njumpLink(NEvent.create(existingEvent, null, null, relay))
|
||||
val link2 = njumpLink(NEvent.create(event.id, null, null, relay))
|
||||
|
||||
Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2")
|
||||
Log.w("Duplicated/SPAM") { "${relay?.url} $link1 $link2" }
|
||||
|
||||
// Log down offenders
|
||||
val spammer = logOffender(hash, event)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.utils.cache.CacheCollectors
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -151,19 +151,19 @@ class UiSettingsFlow(
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun build(torSettings: UiSettings): UiSettingsFlow =
|
||||
fun build(uiSettings: UiSettings): UiSettingsFlow =
|
||||
UiSettingsFlow(
|
||||
MutableStateFlow(torSettings.theme),
|
||||
MutableStateFlow(torSettings.preferredLanguage),
|
||||
MutableStateFlow(torSettings.automaticallyShowImages),
|
||||
MutableStateFlow(torSettings.automaticallyStartPlayback),
|
||||
MutableStateFlow(torSettings.automaticallyShowUrlPreview),
|
||||
MutableStateFlow(torSettings.automaticallyHideNavigationBars),
|
||||
MutableStateFlow(torSettings.automaticallyShowProfilePictures),
|
||||
MutableStateFlow(torSettings.dontShowPushNotificationSelector),
|
||||
MutableStateFlow(torSettings.dontAskForNotificationPermissions),
|
||||
MutableStateFlow(torSettings.featureSet),
|
||||
MutableStateFlow(torSettings.gallerySet),
|
||||
MutableStateFlow(uiSettings.theme),
|
||||
MutableStateFlow(uiSettings.preferredLanguage),
|
||||
MutableStateFlow(uiSettings.automaticallyShowImages),
|
||||
MutableStateFlow(uiSettings.automaticallyStartPlayback),
|
||||
MutableStateFlow(uiSettings.automaticallyShowUrlPreview),
|
||||
MutableStateFlow(uiSettings.automaticallyHideNavigationBars),
|
||||
MutableStateFlow(uiSettings.automaticallyShowProfilePictures),
|
||||
MutableStateFlow(uiSettings.dontShowPushNotificationSelector),
|
||||
MutableStateFlow(uiSettings.dontAskForNotificationPermissions),
|
||||
MutableStateFlow(uiSettings.featureSet),
|
||||
MutableStateFlow(uiSettings.gallerySet),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,9 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -44,10 +45,10 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class AccountCacheState(
|
||||
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||
val geolocationFlow: () -> StateFlow<LocationState.LocationResult>,
|
||||
val nwcFilterAssembler: () -> NWCPaymentFilterAssembler,
|
||||
val contentResolverFn: () -> ContentResolver,
|
||||
val otsResolverBuilder: OtsResolverBuilder,
|
||||
val otsResolverBuilder: () -> OtsResolver,
|
||||
val cache: LocalCache,
|
||||
val client: INostrClient,
|
||||
) {
|
||||
@@ -91,9 +92,11 @@ class AccountCacheState(
|
||||
val cached = accounts.value[signer.pubKey]
|
||||
if (cached != null) return cached
|
||||
|
||||
val signerWithClientTag = NostrSignerWithClientTag(signer, CLIENT_TAG_NAME)
|
||||
|
||||
return Account(
|
||||
settings = accountSettings,
|
||||
signer = signer,
|
||||
signer = signerWithClientTag,
|
||||
geolocationFlow = geolocationFlow,
|
||||
nwcFilterAssembler = nwcFilterAssembler,
|
||||
otsResolverBuilder = otsResolverBuilder,
|
||||
@@ -122,4 +125,8 @@ class AccountCacheState(
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CLIENT_TAG_NAME = "Amethyst"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class PrivateStorageRelayListState(
|
||||
|
||||
init {
|
||||
settings.backupPrivateHomeRelayList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved private home relay list ${event.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
@@ -101,7 +101,7 @@ class PrivateStorageRelayListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start")
|
||||
getPrivateOutboxRelayListFlow().collect { noteState ->
|
||||
Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating Private Home Relay List for ${signer.pubKey}" }
|
||||
(noteState.note.event as? PrivateOutboxRelayListEvent)?.let {
|
||||
settings.updatePrivateHomeRelayList(it)
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class UserMetadataState(
|
||||
|
||||
init {
|
||||
settings.backupUserMetadata?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved user metadata ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved user metadata ${it.toJson()}" }
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
@@ -142,7 +142,7 @@ class UserMetadataState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Kind 0 Collector Start")
|
||||
getUserMetadataFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 0 ${user.toBestDisplayName()}")
|
||||
Log.d("AccountRegisterObservers") { "Updating Kind 0 ${user.toBestDisplayName()}" }
|
||||
(it.note.event as? MetadataEvent)?.let {
|
||||
settings.updateUserMetadata(it)
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ class Kind3FollowListState(
|
||||
|
||||
init {
|
||||
settings.backupContactList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved ${it.tags.size} contacts")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved ${it.tags.size} contacts" }
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
@@ -172,7 +172,7 @@ class Kind3FollowListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Kind 3 Collector Start")
|
||||
getFollowListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 3 ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating Kind 3 ${signer.pubKey}" }
|
||||
(it.note.event as? ContactListEvent)?.let {
|
||||
settings.updateContactListTo(it)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class IncomingOtsEventVerifier(
|
||||
private val otsVerifCache: VerificationStateCache,
|
||||
private val otsVerifCache: () -> VerificationStateCache,
|
||||
private val cache: LocalCache,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
@@ -52,7 +52,7 @@ class IncomingOtsEventVerifier(
|
||||
suspend fun consume(note: Note) {
|
||||
note.event?.let { event ->
|
||||
if (event is OtsEvent) {
|
||||
otsVerifCache.cacheVerify(event)
|
||||
otsVerifCache().cacheVerify(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -37,7 +37,7 @@ import java.util.Base64
|
||||
class OtsState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val otsResolver: OtsResolverBuilder,
|
||||
val otsResolver: () -> OtsResolver,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
@@ -55,10 +55,10 @@ class OtsState(
|
||||
}
|
||||
|
||||
suspend fun updateAttestations(): List<OtsEvent> {
|
||||
Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations")
|
||||
Log.d("Pending Attestations") { "Updating ${settings.pendingAttestations.value.size} pending attestations" }
|
||||
|
||||
return settings.pendingAttestations.value.toList().mapNotNull { (key, value) ->
|
||||
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver.build())
|
||||
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver())
|
||||
|
||||
if (otsState != null) {
|
||||
val hint = cache.getNoteIfExists(key)?.toEventHint<Event>()
|
||||
@@ -96,7 +96,7 @@ class OtsState(
|
||||
Base64.getEncoder().encodeToString(
|
||||
OtsEvent.stamp(
|
||||
id,
|
||||
otsResolver.build(),
|
||||
otsResolver(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ fun loadRelayInfo(
|
||||
value = it
|
||||
},
|
||||
onError = { url, errorCode, exceptionMessage ->
|
||||
Log.e("RelayInfo", "Error loading relay info for ${relay.url}: $errorCode - $exceptionMessage")
|
||||
Log.e("RelayInfo") { "Error loading relay info for ${relay.url}: $errorCode - $exceptionMessage" }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class DmRelayListState(
|
||||
|
||||
init {
|
||||
settings.backupDMRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved DM Relay List ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved DM Relay List ${it.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(it)
|
||||
@@ -98,7 +98,7 @@ class DmRelayListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start")
|
||||
getDMRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating DM Relay List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating DM Relay List for ${signer.pubKey}" }
|
||||
(it.note.event as? ChatMessageRelayListEvent)?.let {
|
||||
settings.updateDMRelayList(it)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ import kotlinx.coroutines.launch
|
||||
*/
|
||||
class NwcSignerState(
|
||||
val signer: NostrSigner,
|
||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||
val nwcFilterAssembler: () -> NWCPaymentFilterAssembler,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val nip47Setup: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?>,
|
||||
@@ -163,11 +163,13 @@ class NwcSignerState(
|
||||
relay = walletService.relayUri,
|
||||
)
|
||||
|
||||
nwcFilterAssembler.subscribe(filter)
|
||||
val assembler = nwcFilterAssembler()
|
||||
|
||||
assembler.subscribe(filter)
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
delay(60000)
|
||||
nwcFilterAssembler.unsubscribe(filter)
|
||||
assembler.unsubscribe(filter)
|
||||
}
|
||||
|
||||
cache.consume(event, null, true, walletService.relayUri) {
|
||||
@@ -204,11 +206,13 @@ class NwcSignerState(
|
||||
relay = walletService.relayUri,
|
||||
)
|
||||
|
||||
nwcFilterAssembler.subscribe(filter)
|
||||
val assembler = nwcFilterAssembler()
|
||||
|
||||
assembler.subscribe(filter)
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
delay(60000) // waits 1 minute to complete payment.
|
||||
nwcFilterAssembler.unsubscribe(filter)
|
||||
assembler.unsubscribe(filter)
|
||||
}
|
||||
|
||||
cache.consume(event, zappedNote, true, walletService.relayUri) {
|
||||
|
||||
@@ -20,282 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
typealias BookmarkListState = com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkListState
|
||||
|
||||
@Stable
|
||||
class BookmarkListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
class BookmarkList(
|
||||
val public: List<Note> = emptyList(),
|
||||
val private: List<Note> = emptyList(),
|
||||
)
|
||||
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val bookmarkList = cache.getOrCreateAddressableNote(getBookmarkListAddress())
|
||||
|
||||
fun getBookmarkListAddress() = BookmarkListEvent.createBookmarkAddress(signer.pubKey)
|
||||
|
||||
fun getBookmarkListFlow(): StateFlow<NoteState> = bookmarkList.flow().metadata.stateFlow
|
||||
|
||||
fun getBookmarkList(): BookmarkListEvent? = bookmarkList.event as? BookmarkListEvent
|
||||
|
||||
fun publicBookmarks(note: Note): List<BookmarkIdTag> {
|
||||
val noteEvent = note.event as? BookmarkListEvent
|
||||
return noteEvent?.publicBookmarks() ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun privateBookmarks(note: Note): List<BookmarkIdTag> {
|
||||
val noteEvent = note.event as? BookmarkListEvent
|
||||
return noteEvent?.privateBookmarks(signer) ?: emptyList()
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val publicBookmarks: StateFlow<List<BookmarkIdTag>> =
|
||||
getBookmarkListFlow()
|
||||
.map { noteState ->
|
||||
publicBookmarks(noteState.note)
|
||||
}.onStart {
|
||||
emit(publicBookmarks(bookmarkList))
|
||||
}.debounce(100)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val privateBookmarks: StateFlow<List<BookmarkIdTag>> =
|
||||
getBookmarkListFlow()
|
||||
.map { noteState ->
|
||||
privateBookmarks(noteState.note)
|
||||
}.onStart {
|
||||
emit(privateBookmarks(bookmarkList))
|
||||
}.debounce(100)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val publicBookmarkEventIdSet =
|
||||
publicBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is EventBookmark) it.eventId else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val publicBookmarkAddressIdSet =
|
||||
publicBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is AddressBookmark) it.address else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val privateBookmarkEventIdSet =
|
||||
privateBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is EventBookmark) it.eventId else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val privateBookmarkAddressIdSet =
|
||||
privateBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is AddressBookmark) it.address else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun bookmarkList(
|
||||
privateBookmarks: List<BookmarkIdTag>,
|
||||
publicBookmarks: List<BookmarkIdTag>,
|
||||
): BookmarkList =
|
||||
BookmarkList(
|
||||
public =
|
||||
publicBookmarks
|
||||
.mapNotNull {
|
||||
when (it) {
|
||||
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
|
||||
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
|
||||
}
|
||||
}.reversed(),
|
||||
private =
|
||||
privateBookmarks
|
||||
.mapNotNull {
|
||||
when (it) {
|
||||
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
|
||||
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
|
||||
}
|
||||
}.reversed(),
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val bookmarks: StateFlow<BookmarkList> =
|
||||
combineTransform(privateBookmarks, publicBookmarks) { private, public ->
|
||||
emit(bookmarkList(private, public))
|
||||
}.onStart {
|
||||
emit(bookmarkList(privateBookmarks.value, publicBookmarks.value))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
BookmarkList(),
|
||||
)
|
||||
|
||||
fun isInPrivateBookmarks(note: Note): Boolean {
|
||||
if (!signer.isWriteable()) return false
|
||||
|
||||
return if (note is AddressableNote) {
|
||||
privateBookmarkAddressIdSet.value.contains(note.address)
|
||||
} else {
|
||||
privateBookmarkEventIdSet.value.contains(note.idHex)
|
||||
}
|
||||
}
|
||||
|
||||
fun isInPublicBookmarks(note: Note): Boolean =
|
||||
if (note is AddressableNote) {
|
||||
publicBookmarkAddressIdSet.value.contains(note.address)
|
||||
} else {
|
||||
publicBookmarkEventIdSet.value.contains(note.idHex)
|
||||
}
|
||||
|
||||
suspend fun addBookmark(
|
||||
note: Note,
|
||||
isPrivate: Boolean,
|
||||
): BookmarkListEvent {
|
||||
val bookmarkList = getBookmarkList()
|
||||
|
||||
return if (bookmarkList == null) {
|
||||
if (note is AddressableNote) {
|
||||
BookmarkListEvent.create(
|
||||
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
BookmarkListEvent.create(
|
||||
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (note is AddressableNote) {
|
||||
BookmarkListEvent.add(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
BookmarkListEvent.add(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeBookmark(
|
||||
note: Note,
|
||||
isPrivate: Boolean,
|
||||
): BookmarkListEvent? {
|
||||
val bookmarkList = getBookmarkList()
|
||||
|
||||
return if (bookmarkList != null) {
|
||||
if (note is AddressableNote) {
|
||||
BookmarkListEvent.remove(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
BookmarkListEvent.remove(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeBookmark(note: Note): BookmarkListEvent? {
|
||||
val bookmarkList = getBookmarkList()
|
||||
|
||||
return if (bookmarkList != null) {
|
||||
if (note is AddressableNote) {
|
||||
BookmarkListEvent.remove(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
BookmarkListEvent.remove(
|
||||
earlierVersion = bookmarkList,
|
||||
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
typealias OldBookmarkListState = com.vitorpamplona.amethyst.commons.model.nip51Lists.OldBookmarkListState
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@Stable
|
||||
class PinListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
val pinList = cache.getOrCreateAddressableNote(PinListEvent.createPinAddress(signer.pubKey))
|
||||
|
||||
fun getPinListFlow(): StateFlow<NoteState> = pinList.flow().metadata.stateFlow
|
||||
|
||||
fun getPinList(): PinListEvent? = pinList.event as? PinListEvent
|
||||
|
||||
fun pinnedEvents(note: Note): List<EventBookmark> {
|
||||
val noteEvent = note.event as? PinListEvent
|
||||
return noteEvent?.pinnedEvents() ?: emptyList()
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val pinnedNotes: StateFlow<List<EventBookmark>> =
|
||||
getPinListFlow()
|
||||
.map { noteState ->
|
||||
pinnedEvents(noteState.note)
|
||||
}.onStart {
|
||||
emit(pinnedEvents(pinList))
|
||||
}.debounce(100)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val pinnedEventIdSet: StateFlow<Set<String>> =
|
||||
pinnedNotes
|
||||
.map { pins ->
|
||||
pins.map { it.eventId }.toSet()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val pinnedNotesList: StateFlow<List<Note>> =
|
||||
pinnedNotes
|
||||
.map { pins ->
|
||||
pins.mapNotNull { cache.checkGetOrCreateNote(it.eventId) }.reversed()
|
||||
}.onStart {
|
||||
emit(
|
||||
pinnedNotes.value.mapNotNull { cache.checkGetOrCreateNote(it.eventId) }.reversed(),
|
||||
)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun isPinned(note: Note): Boolean = pinnedEventIdSet.value.contains(note.idHex)
|
||||
|
||||
suspend fun addPin(note: Note): PinListEvent {
|
||||
val currentList = getPinList()
|
||||
val pin = EventBookmark(note.idHex, note.relayHintUrl())
|
||||
|
||||
return if (currentList == null) {
|
||||
PinListEvent.create(
|
||||
pin = pin,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
PinListEvent.add(
|
||||
earlierVersion = currentList,
|
||||
pin = pin,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removePin(note: Note): PinListEvent? {
|
||||
val currentList = getPinList() ?: return null
|
||||
val pin = EventBookmark(note.idHex, note.relayHintUrl())
|
||||
|
||||
return PinListEvent.remove(
|
||||
earlierVersion = currentList,
|
||||
pin = pin,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ class BlockedRelayListState(
|
||||
|
||||
init {
|
||||
settings.backupBlockedRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved Blocked relay list ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved Blocked relay list ${it.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
@@ -101,7 +101,7 @@ class BlockedRelayListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Blocked Relay List Collector Start")
|
||||
getBlockedRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Blocked Relay List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating Blocked Relay List for ${signer.pubKey}" }
|
||||
(it.note.event as? BlockedRelayListEvent)?.let {
|
||||
settings.updateBlockedRelayList(it)
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class GeohashListState(
|
||||
|
||||
init {
|
||||
settings.backupGeohashList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved Geohash list ${event.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved Geohash list ${event.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
@@ -116,7 +116,7 @@ class GeohashListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Geohash List Collector Start")
|
||||
getGeohashListFlow().collect { noteState ->
|
||||
Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Geohash List for ${signer.pubKey}" }
|
||||
(noteState.note.event as? GeohashListEvent)?.let {
|
||||
settings.updateGeohashListTo(it)
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class HashtagListState(
|
||||
|
||||
init {
|
||||
settings.backupHashtagList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved Hashtag list ${event.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved Hashtag list ${event.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
@@ -116,7 +116,7 @@ class HashtagListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Hashtag List Collector Start")
|
||||
getHashtagListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Hashtag List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Hashtag List for ${signer.pubKey}" }
|
||||
(it.note.event as? HashtagListEvent)?.let {
|
||||
settings.updateHashtagListTo(it)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class IndexerRelayListState(
|
||||
|
||||
init {
|
||||
settings.backupIndexRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved index relay list ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved index relay list ${it.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
@@ -111,7 +111,7 @@ class IndexerRelayListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Index Relay List Collector Start")
|
||||
getIndexerRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Index Relay List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating Index Relay List for ${signer.pubKey}" }
|
||||
(it.note.event as? IndexerRelayListEvent)?.let {
|
||||
settings.updateIndexRelayList(it)
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class MuteListState(
|
||||
|
||||
init {
|
||||
settings.backupMuteList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved mute list ${event.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved mute list ${event.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
@@ -151,7 +151,7 @@ class MuteListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Mute List Collector Start")
|
||||
getMuteListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Mute List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating Mute List for ${signer.pubKey}" }
|
||||
(it.note.event as? MuteListEvent)?.let {
|
||||
settings.updateMuteList(it)
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ class RelayFeedListState(
|
||||
|
||||
init {
|
||||
settings.backupRelayFeedsList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved relay feeds list ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved relay feeds list ${it.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
@@ -123,7 +123,7 @@ class RelayFeedListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Relay feeds list Collector Start")
|
||||
getRelayFeedsListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Relay feeds list for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating Relay feeds list for ${signer.pubKey}" }
|
||||
(it.note.event as? RelayFeedsListEvent)?.let {
|
||||
settings.updateRelayFeedList(it)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ open class GenericRelayListCache<T : PrivateTagArrayEvent>(
|
||||
|
||||
suspend fun relays(event: T) = cachedPrivateLists.mergeTagList(event).relaySet()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun fastStartValueForRelayList(note: Note): RelayListCard {
|
||||
val noteEvent = note.event as? T
|
||||
return if (noteEvent != null) {
|
||||
@@ -52,6 +53,7 @@ open class GenericRelayListCache<T : PrivateTagArrayEvent>(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun observeDecryptedRelayList(note: Note): Flow<RelayListCard> =
|
||||
note
|
||||
.flow()
|
||||
|
||||
@@ -103,7 +103,7 @@ class SearchRelayListState(
|
||||
|
||||
init {
|
||||
settings.backupSearchRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved search relay list ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved search relay list ${it.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
@@ -111,7 +111,7 @@ class SearchRelayListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Search Relay List Collector Start")
|
||||
getSearchRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Search Relay List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating Search Relay List for ${signer.pubKey}" }
|
||||
(it.note.event as? SearchRelayListEvent)?.let {
|
||||
settings.updateSearchRelayList(it)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class TrustedRelayListState(
|
||||
|
||||
init {
|
||||
settings.backupTrustedRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved Trusted relay list ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved Trusted relay list ${it.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
@@ -98,7 +98,7 @@ class TrustedRelayListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Trusted Relay List Collector Start")
|
||||
getTrustedRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Trusted Relay List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating Trusted Relay List for ${signer.pubKey}" }
|
||||
(it.note.event as? TrustedRelayListEvent)?.let {
|
||||
settings.updateTrustedRelayList(it)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip62Vanish
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchFirst
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Stable
|
||||
data class VanishEventItem(
|
||||
val event: RequestToVanishEvent,
|
||||
val relays: List<NormalizedRelayUrl>,
|
||||
val isAllRelays: Boolean,
|
||||
val complianceResults: MutableStateFlow<Map<NormalizedRelayUrl, ComplianceStatus>> =
|
||||
MutableStateFlow(
|
||||
relays.associateWith { ComplianceStatus.UNTESTED },
|
||||
),
|
||||
)
|
||||
|
||||
enum class ComplianceStatus {
|
||||
UNTESTED,
|
||||
TESTING,
|
||||
COMPLIANT,
|
||||
NON_COMPLIANT,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
class VanishRequestsState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val client: INostrClient,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
val noteFlow =
|
||||
cache
|
||||
.observeNotes(
|
||||
Filter(
|
||||
kinds = listOf(RequestToVanishEvent.KIND),
|
||||
authors = listOf(signer.pubKey),
|
||||
),
|
||||
).stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
val testableFlow =
|
||||
noteFlow
|
||||
.map { notes ->
|
||||
notes.mapNotNull {
|
||||
val noteEvent = it.event
|
||||
if (noteEvent is RequestToVanishEvent) {
|
||||
VanishEventItem(noteEvent, noteEvent.vanishFromRelays(), noteEvent.vanishFromAllRelays())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(10000),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
suspend fun testVanishCompliance(
|
||||
item: VanishEventItem,
|
||||
relay: NormalizedRelayUrl,
|
||||
) {
|
||||
item.complianceResults.update {
|
||||
it + (relay to ComplianceStatus.TESTING)
|
||||
}
|
||||
|
||||
try {
|
||||
val foundEvent =
|
||||
withContext(Dispatchers.IO) {
|
||||
client.fetchFirst(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
authors = listOf(item.event.pubKey),
|
||||
until = item.event.createdAt - 1,
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
item.complianceResults.update {
|
||||
it + (
|
||||
relay to
|
||||
if (foundEvent != null) {
|
||||
ComplianceStatus.NON_COMPLIANT
|
||||
} else {
|
||||
ComplianceStatus.COMPLIANT
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
item.complianceResults.update {
|
||||
it + (relay to ComplianceStatus.ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ class Nip65RelayListState(
|
||||
|
||||
init {
|
||||
settings.backupNIP65RelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved nip65 relay list ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved nip65 relay list ${it.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
@@ -152,7 +152,7 @@ class Nip65RelayListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "NIP-65 Relay List Collector Start")
|
||||
getNIP65RelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating NIP-65 List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating NIP-65 List for ${signer.pubKey}" }
|
||||
(it.note.event as? AdvertisedRelayListEvent)?.let {
|
||||
settings.updateNIP65RelayList(it)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ class CommunityListState(
|
||||
|
||||
init {
|
||||
settings.backupCommunityList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved Community list ${event.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved Community list ${event.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
@@ -165,7 +165,7 @@ class CommunityListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Community List Collector Start")
|
||||
getCommunityListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Community List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Community List for ${signer.pubKey}" }
|
||||
(it.note.event as? CommunityListEvent)?.let {
|
||||
settings.updateCommunityListTo(it)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class AppSpecificState(
|
||||
init {
|
||||
if (settings.isWriteable()) {
|
||||
settings.backupAppSpecificData?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved app specific data ${event.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
@@ -84,7 +84,7 @@ class AppSpecificState(
|
||||
Log.d("AccountRegisterObservers", "AppSpecificData Collector Start")
|
||||
getAppSpecificDataFlow().collect {
|
||||
try {
|
||||
Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating AppSpecificData for ${signer.pubKey}" }
|
||||
(it.note.event as? AppSpecificDataEvent)?.let {
|
||||
val decrypted = signer.decrypt(it.content, it.pubKey)
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip86RelayManagement
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip86RelayManagement.Nip86Client
|
||||
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request
|
||||
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Response
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.coroutines.executeAsync
|
||||
|
||||
private const val CONTENT_TYPE_STRING = "application/nostr+json+rpc"
|
||||
private val CONTENT_TYPE = CONTENT_TYPE_STRING.toMediaType()
|
||||
|
||||
class Nip86Retriever(
|
||||
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
||||
) {
|
||||
suspend fun execute(
|
||||
client: Nip86Client,
|
||||
request: Nip86Request,
|
||||
): Nip86Response {
|
||||
val jsonBody = client.serializeRequest(request)
|
||||
val bodyBytes = jsonBody.encodeToByteArray()
|
||||
val authToken = client.buildAuthHeader(bodyBytes)
|
||||
|
||||
val httpRequest =
|
||||
Request
|
||||
.Builder()
|
||||
.url(client.httpUrl)
|
||||
.header("Content-Type", CONTENT_TYPE_STRING)
|
||||
.header("Accept", CONTENT_TYPE_STRING)
|
||||
.header("Authorization", authToken)
|
||||
.post(bodyBytes.toRequestBody(CONTENT_TYPE))
|
||||
.build()
|
||||
|
||||
val httpClient = okHttpClient(client.relayUrl)
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
httpClient.newCall(httpRequest).executeAsync().use { response ->
|
||||
val body = response.body.string()
|
||||
if (response.code == 401) {
|
||||
Nip86Response(error = "Unauthorized: relay rejected authentication")
|
||||
} else if (!response.isSuccessful) {
|
||||
Nip86Response(error = "HTTP ${response.code}: $body")
|
||||
} else {
|
||||
try {
|
||||
client.parseResponse(body)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Nip86Retriever", "Failed to parse response ${client.httpUrl}: $body", e)
|
||||
Nip86Response(error = "Failed to parse response: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Nip86Retriever", "Failed to reach relay ${client.relayUrl.url}", e)
|
||||
Nip86Response(error = "Failed to reach relay: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class NipA3PaymentTargetsState(
|
||||
|
||||
init {
|
||||
settings.backupNipA3PaymentTargets?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved nipA3 Payment targets ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers") { "Loading saved nipA3 Payment targets ${it.toJson()}" }
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class NipA3PaymentTargetsState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "nipA3 Payment targets Collector Start")
|
||||
getNipA3PaymentTargetsFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating nipA3 Payment targets for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "Updating nipA3 Payment targets for ${signer.pubKey}" }
|
||||
(it.note.event as? PaymentTargetsEvent)?.let { paymentTargetsEvent ->
|
||||
settings.updateNIPA3PaymentTargets(paymentTargetsEvent)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@@ -87,6 +88,10 @@ class BlossomServerListState(
|
||||
mergeServerList(blossoms)
|
||||
}.onStart {
|
||||
emit(mergeServerList(flow.value))
|
||||
}.onEach { servers ->
|
||||
if (servers.none { it == settings.defaultFileServer }) {
|
||||
settings.changeDefaultFileServer(servers.firstOrNull() ?: DEFAULT_MEDIA_SERVERS[0])
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
|
||||
@@ -32,8 +32,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@@ -58,17 +57,21 @@ class NamecoinSharedPreferences(
|
||||
companion object {
|
||||
val KEY_ENABLED = booleanPreferencesKey("namecoin.enabled")
|
||||
val KEY_CUSTOM_SERVERS = stringPreferencesKey("namecoin.customServers")
|
||||
val KEY_PINNED_CERTS = stringPreferencesKey("namecoin.pinnedCerts")
|
||||
}
|
||||
|
||||
/**
|
||||
* Current settings, loaded synchronously at init to avoid races.
|
||||
*/
|
||||
private val _settings =
|
||||
MutableStateFlow(
|
||||
runBlocking { loadFromDisk() ?: NamecoinSettings.DEFAULT },
|
||||
)
|
||||
private val _settings = MutableStateFlow(NamecoinSettings.DEFAULT)
|
||||
val settings: StateFlow<NamecoinSettings> = _settings
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
_settings.tryEmit(loadFromDisk() ?: NamecoinSettings.DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
/** Synchronous snapshot — safe to call from `serverListProvider` lambdas. */
|
||||
val current: NamecoinSettings get() = _settings.value
|
||||
|
||||
@@ -99,8 +102,48 @@ class NamecoinSharedPreferences(
|
||||
|
||||
suspend fun reset() {
|
||||
persist(NamecoinSettings.DEFAULT)
|
||||
clearPinnedCerts()
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a PEM-encoded certificate that the user accepted via Test Connection.
|
||||
* The cert is appended to the existing list and synced to the ElectrumXClient.
|
||||
*/
|
||||
suspend fun addPinnedCert(pem: String) {
|
||||
val existing = loadPinnedCertsFromDisk()
|
||||
val updated = (existing + pem).distinct()
|
||||
savePinnedCerts(updated)
|
||||
}
|
||||
|
||||
/** Load all user-pinned certs from disk (for startup sync). */
|
||||
suspend fun loadPinnedCerts(): List<String> = loadPinnedCertsFromDisk()
|
||||
|
||||
private suspend fun clearPinnedCerts() = savePinnedCerts(emptyList())
|
||||
|
||||
private suspend fun savePinnedCerts(certs: List<String>) {
|
||||
try {
|
||||
context.sharedPreferencesDataStore.edit { prefs ->
|
||||
prefs[KEY_PINNED_CERTS] = json.encodeToString(certs)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("NamecoinPrefs") { "Error writing pinned certs: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadPinnedCertsFromDisk(): List<String> =
|
||||
try {
|
||||
val prefs = context.sharedPreferencesDataStore.data.first()
|
||||
val certsJson = prefs[KEY_PINNED_CERTS]
|
||||
if (certsJson != null) {
|
||||
json.decodeFromString<List<String>>(certsJson)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
// ── Internal ───────────────────────────────────────────────────────
|
||||
|
||||
private suspend fun persist(settings: NamecoinSettings) {
|
||||
@@ -115,7 +158,7 @@ class NamecoinSharedPreferences(
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("NamecoinPrefs", "Error writing DataStore: ${e.message}")
|
||||
Log.e("NamecoinPrefs") { "Error writing DataStore: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +180,7 @@ class NamecoinSharedPreferences(
|
||||
NamecoinSettings(enabled = enabled, customServers = servers)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("NamecoinPrefs", "Error reading DataStore: ${e.message}")
|
||||
Log.e("NamecoinPrefs") { "Error reading DataStore: ${e.message}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class OtsSharedPreferences(
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("OtsPrefs", "Error writing DataStore: ${e.message}")
|
||||
Log.e("OtsPrefs") { "Error writing DataStore: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class OtsSharedPreferences(
|
||||
OtsSettings(customExplorerUrl = url)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("OtsPrefs", "Error reading DataStore: ${e.message}")
|
||||
Log.e("OtsPrefs") { "Error reading DataStore: ${e.message}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,14 +40,31 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Stable
|
||||
class TorSharedPreferences(
|
||||
prefs: TorSettings,
|
||||
val context: Context,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
// Tor Preferences. Makes sure to wait for it to avoid connecting with random IPs
|
||||
val value = TorSettingsFlow.build(prefs)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val saving =
|
||||
value.propertyWatchFlow
|
||||
.debounce(1000)
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
save(it, context)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
value.toSettings(),
|
||||
)
|
||||
|
||||
companion object {
|
||||
// loads faster when individualized
|
||||
val TOR_TYPE_KEY = stringPreferencesKey("tor.torType")
|
||||
@@ -63,74 +80,58 @@ class TorSharedPreferences(
|
||||
val MONEY_OPERATIONS_VIA_TOR_KEY = booleanPreferencesKey("tor.moneyOperationsViaTor")
|
||||
val NIP05_VERIFICATIONS_VIA_TOR_KEY = booleanPreferencesKey("tor.nip05VerificationsViaTor")
|
||||
val MEDIA_UPLOADS_VIA_TOR_KEY = booleanPreferencesKey("tor.mediaUploadsViaTor")
|
||||
}
|
||||
|
||||
// Tor Preferences. Makes sure to wait for it to avoid connecting with random IPs
|
||||
val value =
|
||||
runBlocking {
|
||||
TorSettingsFlow.build(torPreferences() ?: TorSettings())
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val saving =
|
||||
value.propertyWatchFlow
|
||||
.debounce(1000)
|
||||
.distinctUntilChanged()
|
||||
.onEach(::save)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
value.toSettings(),
|
||||
)
|
||||
|
||||
suspend fun torPreferences(): TorSettings? =
|
||||
try {
|
||||
// Get the preference flow and take the first value.
|
||||
val preferences = context.sharedPreferencesDataStore.data.first()
|
||||
TorSettings(
|
||||
torType = preferences[TOR_TYPE_KEY]?.let { TorType.valueOf(it) } ?: TorType.INTERNAL,
|
||||
externalSocksPort = preferences[EXTERNAL_SOCKS_PORT_KEY] ?: 9050,
|
||||
onionRelaysViaTor = preferences[ONION_RELAYS_VIA_TOR_KEY] ?: true,
|
||||
dmRelaysViaTor = preferences[DM_RELAYS_VIA_TOR_KEY] ?: true,
|
||||
newRelaysViaTor = preferences[NEW_RELAYS_VIA_TOR_KEY] ?: true,
|
||||
trustedRelaysViaTor = preferences[TRUSTED_RELAYS_VIA_TOR_KEY] ?: false,
|
||||
urlPreviewsViaTor = preferences[URL_PREVIEWS_VIA_TOR_KEY] ?: false,
|
||||
profilePicsViaTor = preferences[PROFILE_PICS_VIA_TOR_KEY] ?: false,
|
||||
imagesViaTor = preferences[IMAGES_VIA_TOR_KEY] ?: false,
|
||||
videosViaTor = preferences[VIDEOS_VIA_TOR_KEY] ?: false,
|
||||
moneyOperationsViaTor = preferences[MONEY_OPERATIONS_VIA_TOR_KEY] ?: false,
|
||||
nip05VerificationsViaTor = preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] ?: false,
|
||||
mediaUploadsViaTor = preferences[MEDIA_UPLOADS_VIA_TOR_KEY] ?: false,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}")
|
||||
null
|
||||
}
|
||||
|
||||
suspend fun save(torSettings: TorSettings) {
|
||||
try {
|
||||
context.sharedPreferencesDataStore.edit { preferences ->
|
||||
preferences[TOR_TYPE_KEY] = torSettings.torType.name
|
||||
preferences[EXTERNAL_SOCKS_PORT_KEY] = torSettings.externalSocksPort
|
||||
preferences[ONION_RELAYS_VIA_TOR_KEY] = torSettings.onionRelaysViaTor
|
||||
preferences[DM_RELAYS_VIA_TOR_KEY] = torSettings.dmRelaysViaTor
|
||||
preferences[NEW_RELAYS_VIA_TOR_KEY] = torSettings.newRelaysViaTor
|
||||
preferences[TRUSTED_RELAYS_VIA_TOR_KEY] = torSettings.trustedRelaysViaTor
|
||||
preferences[URL_PREVIEWS_VIA_TOR_KEY] = torSettings.urlPreviewsViaTor
|
||||
preferences[PROFILE_PICS_VIA_TOR_KEY] = torSettings.profilePicsViaTor
|
||||
preferences[IMAGES_VIA_TOR_KEY] = torSettings.imagesViaTor
|
||||
preferences[VIDEOS_VIA_TOR_KEY] = torSettings.videosViaTor
|
||||
preferences[MONEY_OPERATIONS_VIA_TOR_KEY] = torSettings.moneyOperationsViaTor
|
||||
preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] = torSettings.nip05VerificationsViaTor
|
||||
preferences[MEDIA_UPLOADS_VIA_TOR_KEY] = torSettings.mediaUploadsViaTor
|
||||
suspend fun torPreferences(context: Context): TorSettings? =
|
||||
try {
|
||||
// Get the preference flow and take the first value.
|
||||
val preferences = context.sharedPreferencesDataStore.data.first()
|
||||
TorSettings(
|
||||
torType = preferences[TOR_TYPE_KEY]?.let { TorType.valueOf(it) } ?: TorType.INTERNAL,
|
||||
externalSocksPort = preferences[EXTERNAL_SOCKS_PORT_KEY] ?: 9050,
|
||||
onionRelaysViaTor = preferences[ONION_RELAYS_VIA_TOR_KEY] ?: true,
|
||||
dmRelaysViaTor = preferences[DM_RELAYS_VIA_TOR_KEY] ?: true,
|
||||
newRelaysViaTor = preferences[NEW_RELAYS_VIA_TOR_KEY] ?: true,
|
||||
trustedRelaysViaTor = preferences[TRUSTED_RELAYS_VIA_TOR_KEY] ?: false,
|
||||
urlPreviewsViaTor = preferences[URL_PREVIEWS_VIA_TOR_KEY] ?: false,
|
||||
profilePicsViaTor = preferences[PROFILE_PICS_VIA_TOR_KEY] ?: false,
|
||||
imagesViaTor = preferences[IMAGES_VIA_TOR_KEY] ?: false,
|
||||
videosViaTor = preferences[VIDEOS_VIA_TOR_KEY] ?: false,
|
||||
moneyOperationsViaTor = preferences[MONEY_OPERATIONS_VIA_TOR_KEY] ?: false,
|
||||
nip05VerificationsViaTor = preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] ?: false,
|
||||
mediaUploadsViaTor = preferences[MEDIA_UPLOADS_VIA_TOR_KEY] ?: false,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences") { "Error reading DataStore preferences: ${e.message}" }
|
||||
null
|
||||
}
|
||||
|
||||
suspend fun save(
|
||||
torSettings: TorSettings,
|
||||
context: Context,
|
||||
) {
|
||||
try {
|
||||
context.sharedPreferencesDataStore.edit { preferences ->
|
||||
preferences[TOR_TYPE_KEY] = torSettings.torType.name
|
||||
preferences[EXTERNAL_SOCKS_PORT_KEY] = torSettings.externalSocksPort
|
||||
preferences[ONION_RELAYS_VIA_TOR_KEY] = torSettings.onionRelaysViaTor
|
||||
preferences[DM_RELAYS_VIA_TOR_KEY] = torSettings.dmRelaysViaTor
|
||||
preferences[NEW_RELAYS_VIA_TOR_KEY] = torSettings.newRelaysViaTor
|
||||
preferences[TRUSTED_RELAYS_VIA_TOR_KEY] = torSettings.trustedRelaysViaTor
|
||||
preferences[URL_PREVIEWS_VIA_TOR_KEY] = torSettings.urlPreviewsViaTor
|
||||
preferences[PROFILE_PICS_VIA_TOR_KEY] = torSettings.profilePicsViaTor
|
||||
preferences[IMAGES_VIA_TOR_KEY] = torSettings.imagesViaTor
|
||||
preferences[VIDEOS_VIA_TOR_KEY] = torSettings.videosViaTor
|
||||
preferences[MONEY_OPERATIONS_VIA_TOR_KEY] = torSettings.moneyOperationsViaTor
|
||||
preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] = torSettings.nip05VerificationsViaTor
|
||||
preferences[MEDIA_UPLOADS_VIA_TOR_KEY] = torSettings.mediaUploadsViaTor
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences") { "Error saving DataStore preferences: ${e.message}" }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,36 +49,18 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
val Context.sharedPreferencesDataStore: DataStore<Preferences> by preferencesDataStore(name = "shared_settings")
|
||||
|
||||
@Stable
|
||||
class UiSharedPreferences(
|
||||
prefs: UiSettings,
|
||||
val context: Context,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
companion object {
|
||||
// loads faster when individualized
|
||||
val UI_THEME = stringPreferencesKey("ui.theme")
|
||||
val UI_LANGUAGE = stringPreferencesKey("ui.language")
|
||||
val UI_SHOW_IMAGES = stringPreferencesKey("ui.show_images")
|
||||
val UI_START_PLAYBACK = stringPreferencesKey("ui.start_playback")
|
||||
val UI_SHOW_URL_PREVIEW = stringPreferencesKey("ui.show_url_preview")
|
||||
val UI_HIDE_NAVIGATION_BARS = stringPreferencesKey("ui.hide_navigation_bars")
|
||||
val UI_SHOW_PROFILE_PICTURES = stringPreferencesKey("ui.show_profile_pictures")
|
||||
val UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR = booleanPreferencesKey("ui.dont_show_push_notification_selector")
|
||||
val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions")
|
||||
val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set")
|
||||
val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set")
|
||||
}
|
||||
|
||||
// UI Preferences. Makes sure to wait for it to avoid blinking themes and language preferences
|
||||
val value =
|
||||
runBlocking {
|
||||
UiSettingsFlow.build(uiPreferences() ?: UiSettings())
|
||||
}
|
||||
val value = UiSettingsFlow.build(prefs)
|
||||
|
||||
val languageUpdate =
|
||||
value.preferredLanguage
|
||||
@@ -98,68 +80,87 @@ class UiSharedPreferences(
|
||||
value.propertyWatchFlow
|
||||
.debounce(1000)
|
||||
.distinctUntilChanged()
|
||||
.onEach(::save)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.onEach {
|
||||
save(it, context)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
value.toSettings(),
|
||||
)
|
||||
|
||||
suspend fun uiPreferences(): UiSettings? =
|
||||
try {
|
||||
// Get the preference flow and take the first value.
|
||||
val preferences = context.sharedPreferencesDataStore.data.first()
|
||||
|
||||
UiSettings(
|
||||
theme = preferences[UI_THEME]?.let { ThemeType.valueOf(it) } ?: ThemeType.SYSTEM,
|
||||
preferredLanguage = preferences[UI_LANGUAGE]?.ifBlank { null },
|
||||
automaticallyShowImages = preferences[UI_SHOW_IMAGES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
|
||||
automaticallyStartPlayback = preferences[UI_START_PLAYBACK]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
|
||||
automaticallyShowUrlPreview = preferences[UI_SHOW_URL_PREVIEW]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
|
||||
automaticallyHideNavigationBars = preferences[UI_HIDE_NAVIGATION_BARS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS,
|
||||
automaticallyShowProfilePictures = preferences[UI_SHOW_PROFILE_PICTURES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
|
||||
dontShowPushNotificationSelector = preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] ?: false,
|
||||
dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false,
|
||||
featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED,
|
||||
gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}")
|
||||
companion object {
|
||||
// loads faster when individualized
|
||||
val UI_THEME = stringPreferencesKey("ui.theme")
|
||||
val UI_LANGUAGE = stringPreferencesKey("ui.language")
|
||||
val UI_SHOW_IMAGES = stringPreferencesKey("ui.show_images")
|
||||
val UI_START_PLAYBACK = stringPreferencesKey("ui.start_playback")
|
||||
val UI_SHOW_URL_PREVIEW = stringPreferencesKey("ui.show_url_preview")
|
||||
val UI_HIDE_NAVIGATION_BARS = stringPreferencesKey("ui.hide_navigation_bars")
|
||||
val UI_SHOW_PROFILE_PICTURES = stringPreferencesKey("ui.show_profile_pictures")
|
||||
val UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR = booleanPreferencesKey("ui.dont_show_push_notification_selector")
|
||||
val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions")
|
||||
val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set")
|
||||
val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set")
|
||||
|
||||
suspend fun uiPreferences(context: Context): UiSettings? =
|
||||
try {
|
||||
val oldVersion = LocalPreferences.loadSharedSettings()
|
||||
if (oldVersion != null) {
|
||||
save(oldVersion)
|
||||
}
|
||||
oldVersion
|
||||
// Get the preference flow and take the first value.
|
||||
val preferences = context.sharedPreferencesDataStore.data.first()
|
||||
|
||||
UiSettings(
|
||||
theme = preferences[UI_THEME]?.let { ThemeType.valueOf(it) } ?: ThemeType.SYSTEM,
|
||||
preferredLanguage = preferences[UI_LANGUAGE]?.ifBlank { null },
|
||||
automaticallyShowImages = preferences[UI_SHOW_IMAGES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
|
||||
automaticallyStartPlayback = preferences[UI_START_PLAYBACK]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
|
||||
automaticallyShowUrlPreview = preferences[UI_SHOW_URL_PREVIEW]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
|
||||
automaticallyHideNavigationBars = preferences[UI_HIDE_NAVIGATION_BARS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS,
|
||||
automaticallyShowProfilePictures = preferences[UI_SHOW_PROFILE_PICTURES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
|
||||
dontShowPushNotificationSelector = preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] ?: false,
|
||||
dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false,
|
||||
featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED,
|
||||
gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences") { "Error reading DataStore preferences: ${e.message}" }
|
||||
|
||||
suspend fun save(sharedSettings: UiSettings) {
|
||||
try {
|
||||
context.sharedPreferencesDataStore.edit { preferences ->
|
||||
preferences[UI_THEME] = sharedSettings.theme.name
|
||||
preferences[UI_LANGUAGE] = sharedSettings.preferredLanguage ?: ""
|
||||
preferences[UI_SHOW_IMAGES] = sharedSettings.automaticallyShowImages.name
|
||||
preferences[UI_START_PLAYBACK] = sharedSettings.automaticallyStartPlayback.name
|
||||
preferences[UI_SHOW_URL_PREVIEW] = sharedSettings.automaticallyShowUrlPreview.name
|
||||
preferences[UI_HIDE_NAVIGATION_BARS] = sharedSettings.automaticallyHideNavigationBars.name
|
||||
preferences[UI_SHOW_PROFILE_PICTURES] = sharedSettings.automaticallyShowProfilePictures.name
|
||||
preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] = sharedSettings.dontShowPushNotificationSelector
|
||||
preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions
|
||||
preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name
|
||||
preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name
|
||||
try {
|
||||
val oldVersion = LocalPreferences.loadSharedSettings()
|
||||
if (oldVersion != null) {
|
||||
save(oldVersion, context)
|
||||
}
|
||||
oldVersion
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun save(
|
||||
sharedSettings: UiSettings,
|
||||
context: Context,
|
||||
) {
|
||||
try {
|
||||
context.sharedPreferencesDataStore.edit { preferences ->
|
||||
preferences[UI_THEME] = sharedSettings.theme.name
|
||||
preferences[UI_LANGUAGE] = sharedSettings.preferredLanguage ?: ""
|
||||
preferences[UI_SHOW_IMAGES] = sharedSettings.automaticallyShowImages.name
|
||||
preferences[UI_START_PLAYBACK] = sharedSettings.automaticallyStartPlayback.name
|
||||
preferences[UI_SHOW_URL_PREVIEW] = sharedSettings.automaticallyShowUrlPreview.name
|
||||
preferences[UI_HIDE_NAVIGATION_BARS] = sharedSettings.automaticallyHideNavigationBars.name
|
||||
preferences[UI_SHOW_PROFILE_PICTURES] = sharedSettings.automaticallyShowProfilePictures.name
|
||||
preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] = sharedSettings.dontShowPushNotificationSelector
|
||||
preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions
|
||||
preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name
|
||||
preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences") { "Error saving DataStore preferences: ${e.message}" }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MergedFollowListsState(
|
||||
val kind3List: Kind3FollowListState,
|
||||
val peopleList: PeopleListsState,
|
||||
|
||||
@@ -51,7 +51,7 @@ class FeedTopNavFilterState(
|
||||
val feedFilterListName: MutableStateFlow<TopFilter>,
|
||||
val kind3Follows: StateFlow<Kind3FollowListState.Kind3Follows>,
|
||||
val allFollows: StateFlow<MergedFollowListsState.AllFollows>,
|
||||
val locationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val locationFlow: () -> StateFlow<LocationState.LocationResult>,
|
||||
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val proxyRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
@@ -78,7 +78,7 @@ class FeedTopNavFilterState(
|
||||
}
|
||||
|
||||
TopFilter.AroundMe -> {
|
||||
AroundMeFeedFlow(locationFlow, followsRelays, proxyRelays)
|
||||
AroundMeFeedFlow(locationFlow(), followsRelays, proxyRelays)
|
||||
}
|
||||
|
||||
TopFilter.Chess -> {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.torState
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||
@@ -35,6 +36,7 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@Stable
|
||||
class TorRelayState(
|
||||
val okHttpClient: DualHttpClientManager,
|
||||
val torSettingsFlow: TorSettingsFlow,
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.trustedAssertions
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviderSet
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviders
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.serviceProviderSet
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.serviceProviders
|
||||
|
||||
class TrustProviderListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
|
||||
@@ -24,10 +24,10 @@ import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ProviderTypes
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ProviderTypes
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ServiceProviderTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
@@ -124,7 +124,7 @@ class TrustProviderListState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "TrustProviderList Collector Start")
|
||||
getTrustProviderListFlow().collect { noteState ->
|
||||
Log.d("AccountRegisterObservers", "TrustProviderList List for ${signer.pubKey}")
|
||||
Log.d("AccountRegisterObservers") { "TrustProviderList List for ${signer.pubKey}" }
|
||||
(noteState.note.event as? TrustProviderListEvent)?.let {
|
||||
settings.updateTrustProviderListTo(it)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.broadcast
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage
|
||||
@@ -85,12 +85,12 @@ class BroadcastTracker {
|
||||
// Add to active broadcasts and cache event for retries
|
||||
_activeBroadcasts.update { (it + broadcast).toImmutableList() }
|
||||
|
||||
Log.d(TAG, "Starting broadcast $trackingId (kind ${event.kind}) to ${relays.size} relays")
|
||||
Log.d(TAG) { "Starting broadcast $trackingId (kind ${event.kind}) to ${relays.size} relays" }
|
||||
|
||||
val resultChannel = Channel<RelayResponse>(UNLIMITED)
|
||||
|
||||
val subscription =
|
||||
object : IRelayClientListener {
|
||||
object : RelayConnectionListener {
|
||||
override fun onCannotConnect(
|
||||
relay: IRelayClient,
|
||||
errorMessage: String,
|
||||
@@ -102,7 +102,7 @@ class BroadcastTracker {
|
||||
result = RelayResult.Error(errorMessage),
|
||||
),
|
||||
)
|
||||
Log.d(TAG, "[$trackingId] Cannot connect to ${relay.url}: $errorMessage")
|
||||
Log.d(TAG) { "[$trackingId] Cannot connect to ${relay.url}: $errorMessage" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class BroadcastTracker {
|
||||
result = RelayResult.Error("Relay disconnected before completion"),
|
||||
),
|
||||
)
|
||||
Log.d(TAG, "[$trackingId] Disconnected from ${relay.url}")
|
||||
Log.d(TAG) { "[$trackingId] Disconnected from ${relay.url}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,62 +135,65 @@ class BroadcastTracker {
|
||||
RelayResult.Error(msg.message)
|
||||
}
|
||||
resultChannel.trySend(RelayResponse(relay.url, result))
|
||||
Log.d(TAG, "[$trackingId] Response from ${relay.url}: success=${msg.success} message=${msg.message}")
|
||||
Log.d(TAG) { "[$trackingId] Response from ${relay.url}: success=${msg.success} message=${msg.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.subscribe(subscription)
|
||||
try {
|
||||
client.addConnectionListener(subscription)
|
||||
|
||||
val finalBroadcast =
|
||||
coroutineScope {
|
||||
val resultCollector =
|
||||
async {
|
||||
val receivedRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
var currentBroadcast = broadcast
|
||||
val finalBroadcast =
|
||||
coroutineScope {
|
||||
val resultCollector =
|
||||
async {
|
||||
val receivedRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
var currentBroadcast = broadcast
|
||||
|
||||
withTimeoutOrNull(TIMEOUT_SECONDS * 1000) {
|
||||
while (receivedRelays.size < relays.size) {
|
||||
val response = resultChannel.receive()
|
||||
withTimeoutOrNull(TIMEOUT_SECONDS * 1000) {
|
||||
while (receivedRelays.size < relays.size) {
|
||||
val response = resultChannel.receive()
|
||||
|
||||
// Skip if already received (don't override success)
|
||||
if (response.relay in receivedRelays) continue
|
||||
// Skip if already received (don't override success)
|
||||
if (response.relay in receivedRelays) continue
|
||||
|
||||
receivedRelays.add(response.relay)
|
||||
currentBroadcast = currentBroadcast.withResult(response.relay, response.result)
|
||||
receivedRelays.add(response.relay)
|
||||
currentBroadcast = currentBroadcast.withResult(response.relay, response.result)
|
||||
|
||||
// Update active broadcasts with new progress
|
||||
_activeBroadcasts.update { list ->
|
||||
list.map { if (it.id == trackingId) currentBroadcast else it }.toImmutableList()
|
||||
// Update active broadcasts with new progress
|
||||
_activeBroadcasts.update { list ->
|
||||
list.map { if (it.id == trackingId) currentBroadcast else it }.toImmutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark remaining relays as timeout
|
||||
relays.filter { it !in receivedRelays }.forEach { relay ->
|
||||
currentBroadcast = currentBroadcast.withResult(relay, RelayResult.Timeout)
|
||||
}
|
||||
|
||||
currentBroadcast
|
||||
}
|
||||
|
||||
// Mark remaining relays as timeout
|
||||
relays.filter { it !in receivedRelays }.forEach { relay ->
|
||||
currentBroadcast = currentBroadcast.withResult(relay, RelayResult.Timeout)
|
||||
}
|
||||
// Send after setting up listener
|
||||
client.publish(event, relays)
|
||||
|
||||
currentBroadcast
|
||||
}
|
||||
resultCollector.await()
|
||||
}
|
||||
|
||||
// Send after setting up listener
|
||||
client.send(event, relays)
|
||||
resultChannel.close()
|
||||
|
||||
resultCollector.await()
|
||||
// Remove from active, emit to completed
|
||||
_activeBroadcasts.update { list ->
|
||||
list.map { if (it.id == trackingId) finalBroadcast else it }.toImmutableList()
|
||||
}
|
||||
|
||||
client.unsubscribe(subscription)
|
||||
resultChannel.close()
|
||||
|
||||
// Remove from active, emit to completed
|
||||
_activeBroadcasts.update { list ->
|
||||
list.map { if (it.id == trackingId) finalBroadcast else it }.toImmutableList()
|
||||
Log.d(TAG) { "Broadcast $trackingId complete: ${finalBroadcast.successCount}/${finalBroadcast.totalRelays} success" }
|
||||
} finally {
|
||||
client.removeConnectionListener(subscription)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Broadcast $trackingId complete: ${finalBroadcast.successCount}/${finalBroadcast.totalRelays} success")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,7 +266,7 @@ class BroadcastTracker {
|
||||
val resultChannel = Channel<RelayResponse>(UNLIMITED)
|
||||
|
||||
val subscription =
|
||||
object : IRelayClientListener {
|
||||
object : RelayConnectionListener {
|
||||
override fun onCannotConnect(
|
||||
relay: IRelayClient,
|
||||
errorMessage: String,
|
||||
@@ -275,7 +278,7 @@ class BroadcastTracker {
|
||||
result = RelayResult.Error(errorMessage),
|
||||
),
|
||||
)
|
||||
Log.d(TAG, "[${broadcast.id}] Retry cannot connect to ${relay.url}: $errorMessage")
|
||||
Log.d(TAG) { "[${broadcast.id}] Retry cannot connect to ${relay.url}: $errorMessage" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +290,7 @@ class BroadcastTracker {
|
||||
result = RelayResult.Error("Relay disconnected before completion"),
|
||||
),
|
||||
)
|
||||
Log.d(TAG, "[${broadcast.id}] Retry disconnected from ${relay.url}")
|
||||
Log.d(TAG) { "[${broadcast.id}] Retry disconnected from ${relay.url}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,14 +311,14 @@ class BroadcastTracker {
|
||||
RelayResult.Error(msg.message)
|
||||
}
|
||||
resultChannel.trySend(RelayResponse(relay.url, result))
|
||||
Log.d(TAG, "[${broadcast.id}] Retry response from ${relay.url}: success=${msg.success}")
|
||||
Log.d(TAG) { "[${broadcast.id}] Retry response from ${relay.url}: success=${msg.success}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.subscribe(subscription)
|
||||
client.addConnectionListener(subscription)
|
||||
|
||||
val finalBroadcast =
|
||||
coroutineScope {
|
||||
@@ -367,12 +370,12 @@ class BroadcastTracker {
|
||||
currentBroadcast.copy(status = newStatus)
|
||||
}
|
||||
|
||||
client.send(event, relaysToRetry)
|
||||
client.publish(event, relaysToRetry)
|
||||
|
||||
resultCollector.await()
|
||||
}
|
||||
|
||||
client.unsubscribe(subscription)
|
||||
client.removeConnectionListener(subscription)
|
||||
resultChannel.close()
|
||||
|
||||
// Update in active broadcasts
|
||||
@@ -380,7 +383,7 @@ class BroadcastTracker {
|
||||
list.map { if (it.id == broadcast.id) finalBroadcast else it }.toImmutableList()
|
||||
}
|
||||
|
||||
Log.d(TAG, "Retry complete for ${broadcast.id}: ${finalBroadcast.successCount}/${finalBroadcast.totalRelays} success")
|
||||
Log.d(TAG) { "Retry complete for ${broadcast.id}: ${finalBroadcast.successCount}/${finalBroadcast.totalRelays} success" }
|
||||
|
||||
return finalBroadcast
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class ConnectivityFlow(
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
Log.d("ConnectivityFlow", "onAvailable ${network.networkHandle}")
|
||||
Log.d("ConnectivityFlow") { "onAvailable ${network.networkHandle}" }
|
||||
connectivityManager.getNetworkCapabilities(network)?.let {
|
||||
trySend(ConnectivityStatus.Active(network.networkHandle, it.isMeteredOrMobileData()))
|
||||
}
|
||||
@@ -60,13 +60,13 @@ class ConnectivityFlow(
|
||||
) {
|
||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||
val isMobile = networkCapabilities.isMeteredOrMobileData()
|
||||
Log.d("ConnectivityFlow", "onCapabilitiesChanged ${network.networkHandle} $isMobile")
|
||||
Log.d("ConnectivityFlow") { "onCapabilitiesChanged ${network.networkHandle} $isMobile" }
|
||||
trySend(ConnectivityStatus.Active(network.networkHandle, isMobile))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
super.onLost(network)
|
||||
Log.d("ConnectivityFlow", "onLost ${network.networkHandle} ")
|
||||
Log.d("ConnectivityFlow") { "onLost ${network.networkHandle} " }
|
||||
trySend(ConnectivityStatus.Off)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
class BlossomFetcher(
|
||||
private val options: Options,
|
||||
private val data: Uri,
|
||||
private val blossomServerResolver: BlossomServerResolver,
|
||||
private val blossomServerResolver: () -> BlossomServerResolver,
|
||||
private val networkFetcher: (url: String) -> Fetcher,
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult? =
|
||||
try {
|
||||
val urlResult = blossomServerResolver.findServers(data.toString())
|
||||
val urlResult = blossomServerResolver().findServers(data.toString())
|
||||
networkFetcher(urlResult?.serverUrl ?: data.toString()).fetch()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
@@ -55,7 +55,7 @@ class BlossomFetcher(
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
class Factory(
|
||||
val blossomServerResolver: BlossomServerResolver,
|
||||
val blossomServerResolver: () -> BlossomServerResolver,
|
||||
val networkClient: (url: String) -> Call.Factory,
|
||||
) : Fetcher.Factory<Uri> {
|
||||
private val connectivityCheckerLazy = singleParameterLazy(::ConnectivityChecker)
|
||||
|
||||
@@ -64,7 +64,7 @@ class ImageLoaderSetup {
|
||||
app: Context,
|
||||
diskCache: () -> DiskCache,
|
||||
memoryCache: () -> MemoryCache,
|
||||
blossomServerResolver: BlossomServerResolver,
|
||||
blossomServerResolver: () -> BlossomServerResolver,
|
||||
callFactory: (url: String) -> Call.Factory,
|
||||
) {
|
||||
SingletonImageLoader.setUnsafe(
|
||||
|
||||
@@ -188,7 +188,7 @@ class LightningAddressResolver {
|
||||
}
|
||||
|
||||
if (errorMessage == null) {
|
||||
Log.d("LightningAddressResolver", "Error parsing LNResponse: $body")
|
||||
Log.d("LightningAddressResolver") { "Error parsing LNResponse: $body" }
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
|
||||
@@ -48,13 +48,13 @@ class LocationFlow(
|
||||
|
||||
val locationCallback =
|
||||
LocationListener { location ->
|
||||
Log.d("LocationFlow", "onLocationChanged $location")
|
||||
Log.d("LocationFlow") { "onLocationChanged $location" }
|
||||
launch { send(location) }
|
||||
}
|
||||
|
||||
locationManager.allProviders.forEach {
|
||||
val location = locationManager.getLastKnownLocation(it)
|
||||
Log.d("LocationFlow", "Last Known location is $location")
|
||||
Log.d("LocationFlow") { "Last Known location is $location" }
|
||||
if (location != null) {
|
||||
send(location)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class LocationState(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val geohashStateFlow =
|
||||
val geohashStateFlow by lazy {
|
||||
hasLocationPermission
|
||||
.transformLatest {
|
||||
if (it) {
|
||||
@@ -92,4 +92,5 @@ class LocationState(
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
latestLocation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.annotation.RequiresApi
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import java.io.IOException
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class ReverseGeolocation {
|
||||
companion object {
|
||||
fun execute(
|
||||
@@ -52,18 +53,18 @@ class ReverseGeolocation {
|
||||
val locationCallback =
|
||||
object : Geocoder.GeocodeListener {
|
||||
override fun onGeocode(addresses: List<Address>) {
|
||||
Log.d("ReverseGeoLocation", "Found ${addresses.size} new addresses")
|
||||
Log.d("ReverseGeoLocation") { "Found ${addresses.size} new addresses" }
|
||||
onReady(addresses)
|
||||
}
|
||||
|
||||
override fun onError(errorMessage: String?) {
|
||||
super.onError(errorMessage)
|
||||
Log.w("ReverseGeoLocation", "Failure $errorMessage")
|
||||
Log.w("ReverseGeoLocation") { "Failure $errorMessage" }
|
||||
onReady(null)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("ReverseGeoLocation", "Execute Async $location")
|
||||
Log.d("ReverseGeoLocation") { "Execute Async $location" }
|
||||
Geocoder(context).getFromLocation(
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
@@ -76,7 +77,7 @@ class ReverseGeolocation {
|
||||
location: Location,
|
||||
context: Context,
|
||||
): List<Address>? {
|
||||
Log.d("ReverseGeoLocation", "Execute Sync $location")
|
||||
Log.d("ReverseGeoLocation") { "Execute Sync $location" }
|
||||
return try {
|
||||
Geocoder(context).getFromLocation(
|
||||
location.latitude,
|
||||
|
||||
@@ -41,7 +41,7 @@ object ChoreographerHelper {
|
||||
if (diff > 35) {
|
||||
// Follow the frame number
|
||||
val droppedCount = (diff / 16.6).toInt()
|
||||
Log.w("block-canary", "Dropped $droppedCount frames. Skipped $diff ms")
|
||||
Log.w("block-canary") { "Dropped $droppedCount frames. Skipped $diff ms" }
|
||||
}
|
||||
lastFrameTimeNanos = frameTimeNanos
|
||||
Choreographer.getInstance().postFrameCallback(this)
|
||||
|
||||
@@ -60,7 +60,7 @@ class LogMonitor : Printer {
|
||||
val endTime = System.currentTimeMillis()
|
||||
|
||||
if (x.indexOf("com.vitorpamplona.amethyst") > 0) {
|
||||
Log.d("block-canary", "Looper ${endTime - mStartTimestamp}ms for $x")
|
||||
Log.d("block-canary") { "Looper ${endTime - mStartTimestamp}ms for $x" }
|
||||
}
|
||||
|
||||
mPrintingStarted = false
|
||||
|
||||
@@ -66,7 +66,7 @@ data class NamecoinSettings(
|
||||
* TLS is the default protocol. Append `:tcp` for plaintext
|
||||
* (useful for `.onion` addresses and local servers).
|
||||
*
|
||||
* `.onion` addresses automatically get `trustAllCerts = true`
|
||||
* `.onion` addresses automatically get `usePinnedTrustStore = true`
|
||||
* since certificate verification is meaningless over Tor.
|
||||
*/
|
||||
fun parseServerString(s: String): ElectrumxServer? {
|
||||
@@ -77,11 +77,15 @@ data class NamecoinSettings(
|
||||
if (host.isEmpty() || port <= 0 || port > 65535) return null
|
||||
val useSsl = parts.getOrNull(2)?.trim()?.lowercase() != "tcp"
|
||||
val isOnion = host.endsWith(".onion")
|
||||
// All custom servers use the pinned trust store. ElectrumX
|
||||
// servers almost universally use self-signed certs, so we
|
||||
// route them through our pinned SSLSocketFactory (hardcoded
|
||||
// defaults + TOFU-pinned certs + system CAs).
|
||||
return ElectrumxServer(
|
||||
host = host,
|
||||
port = port,
|
||||
useSsl = useSsl,
|
||||
trustAllCerts = isOnion || !useSsl,
|
||||
usePinnedTrustStore = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendChessNotification
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendReactionNotification
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -43,10 +45,14 @@ import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEven
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip64Chess.baseEvent.BaseChessEvent
|
||||
import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent
|
||||
import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.math.BigDecimal
|
||||
@@ -67,14 +73,14 @@ class EventNotificationConsumer(
|
||||
LocalPreferences.allSavedAccounts().forEach {
|
||||
if (!matchAccount && (it.hasPrivKey || it.loggedInWithExternalSigner)) {
|
||||
LocalPreferences.loadAccountConfigFromEncryptedStorage(it.npub)?.let { acc ->
|
||||
Log.d(TAG, "New Notification Testing if for ${it.npub}")
|
||||
Log.d(TAG) { "New Notification Testing if for ${it.npub}" }
|
||||
try {
|
||||
val account = Amethyst.instance.accountsCache.loadAccount(acc)
|
||||
consumeIfMatchesAccount(event, account)
|
||||
matchAccount = true
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.d(TAG, "Message was not for user ${it.npub}: ${e.message}")
|
||||
Log.d(TAG) { "Message was not for user ${it.npub}: ${e.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,20 +100,43 @@ class EventNotificationConsumer(
|
||||
account: Account,
|
||||
) {
|
||||
val consumed = LocalCache.hasConsumed(notificationEvent)
|
||||
Log.d(TAG, "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${account.signer.pubKey} consumed= $consumed")
|
||||
Log.d(TAG) { "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${account.signer.pubKey} consumed= $consumed" }
|
||||
if (!consumed) {
|
||||
Log.d(TAG, "New Notification was verified")
|
||||
if (!notificationManager().areNotificationsEnabled()) return
|
||||
Log.d(TAG, "Notifications are enabled")
|
||||
|
||||
unwrapAndConsume(notificationEvent, account.signer)?.let { innerEvent ->
|
||||
Log.d(TAG, "Unwrapped consume ${innerEvent.javaClass.simpleName}")
|
||||
Log.d(TAG) { "Unwrapped consume ${innerEvent.javaClass.simpleName}" }
|
||||
|
||||
when (innerEvent) {
|
||||
is PrivateDmEvent -> notify(innerEvent, account)
|
||||
is LnZapEvent -> notify(innerEvent, account)
|
||||
is ChatMessageEvent -> notify(innerEvent, account)
|
||||
is ChatMessageEncryptedFileHeaderEvent -> notify(innerEvent, account)
|
||||
is PrivateDmEvent -> {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
|
||||
is LnZapEvent -> {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
|
||||
is ChatMessageEvent -> {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
|
||||
is ChatMessageEncryptedFileHeaderEvent -> {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
|
||||
is ReactionEvent -> {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
|
||||
is LiveChessGameAcceptEvent -> {
|
||||
notifyChessEvent(innerEvent, account, R.string.app_notification_chess_challenge_accepted)
|
||||
}
|
||||
|
||||
is LiveChessMoveEvent -> {
|
||||
notifyChessEvent(innerEvent, account, R.string.app_notification_chess_your_turn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,14 +153,14 @@ class EventNotificationConsumer(
|
||||
LocalPreferences.allSavedAccounts().forEach {
|
||||
if (!matchAccount && (it.hasPrivKey || it.loggedInWithExternalSigner) && it.npub in npubs) {
|
||||
LocalPreferences.loadAccountConfigFromEncryptedStorage(it.npub)?.let { accountSettings ->
|
||||
Log.d(TAG, "New Notification Testing if for ${it.npub}")
|
||||
Log.d(TAG) { "New Notification Testing if for ${it.npub}" }
|
||||
try {
|
||||
val account = Amethyst.instance.accountsCache.loadAccount(accountSettings)
|
||||
consumeNotificationEvent(event, account)
|
||||
matchAccount = true
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.d(TAG, "Message was not for user ${it.npub}: ${e.message}")
|
||||
Log.d(TAG) { "Message was not for user ${it.npub}: ${e.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,7 +394,7 @@ class EventNotificationConsumer(
|
||||
account: Account,
|
||||
) {
|
||||
Log.d(TAG, "New Zap to Notify")
|
||||
Log.d(TAG, "Notify Start ${event.toNostrUri()}")
|
||||
Log.d(TAG) { "Notify Start ${event.toNostrUri()}" }
|
||||
LocalCache.getNoteIfExists(event.id) ?: return
|
||||
|
||||
Log.d(TAG, "Notify Not Notified Yet")
|
||||
@@ -378,7 +407,7 @@ class EventNotificationConsumer(
|
||||
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
|
||||
Log.d(TAG, "Notify ZapRequest $noteZapRequest zapped $noteZapped")
|
||||
Log.d(TAG) { "Notify ZapRequest $noteZapRequest zapped $noteZapped" }
|
||||
|
||||
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
|
||||
|
||||
@@ -387,11 +416,11 @@ class EventNotificationConsumer(
|
||||
if (event.isTaggedUser(account.signer.pubKey)) {
|
||||
val amount = showAmount(event.amount)
|
||||
|
||||
Log.d(TAG, "Notify Amount $amount")
|
||||
Log.d(TAG) { "Notify Amount $amount" }
|
||||
|
||||
(noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
|
||||
decryptZapContentAuthor(event, account.signer)?.let { decryptedEvent ->
|
||||
Log.d(TAG, "Notify Decrypted if Private Zap ${event.id}")
|
||||
Log.d(TAG) { "Notify Decrypted if Private Zap ${event.id}" }
|
||||
|
||||
val author = LocalCache.getOrCreateUser(decryptedEvent.pubKey)
|
||||
val senderInfo = Pair(author, decryptedEvent.content.ifBlank { null })
|
||||
@@ -428,7 +457,7 @@ class EventNotificationConsumer(
|
||||
.hexToByteArray()
|
||||
.toNpub()
|
||||
|
||||
Log.d(TAG, "Notify ${event.id} $content $title $noteUri")
|
||||
Log.d(TAG) { "Notify ${event.id} $content $title $noteUri" }
|
||||
|
||||
notificationManager()
|
||||
.sendZapNotification(
|
||||
@@ -463,7 +492,7 @@ class EventNotificationConsumer(
|
||||
.hexToByteArray()
|
||||
.toNpub()
|
||||
|
||||
Log.d(TAG, "Notify ${event.id} $title $noteUri")
|
||||
Log.d(TAG) { "Notify ${event.id} $title $noteUri" }
|
||||
|
||||
notificationManager()
|
||||
.sendZapNotification(
|
||||
@@ -481,6 +510,111 @@ class EventNotificationConsumer(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun notify(
|
||||
event: ReactionEvent,
|
||||
account: Account,
|
||||
) {
|
||||
Log.d(TAG, "New Reaction to Notify")
|
||||
|
||||
// old event being re-broadcast
|
||||
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return
|
||||
|
||||
// don't notify for own reactions
|
||||
if (event.pubKey == account.signer.pubKey) return
|
||||
|
||||
// only notify if the reaction is for the current user
|
||||
if (!event.isTaggedUser(account.signer.pubKey)) return
|
||||
|
||||
val reactedPostId = event.originalPost().firstOrNull() ?: return
|
||||
val reactedNote = LocalCache.checkGetOrCreateNote(reactedPostId)
|
||||
|
||||
val author = LocalCache.getOrCreateUser(event.pubKey)
|
||||
val user = author.toBestDisplayName()
|
||||
val userPicture = author.profilePicture()
|
||||
|
||||
val reactionContent = event.content
|
||||
val reactionSymbol =
|
||||
when {
|
||||
reactionContent == ReactionEvent.LIKE || reactionContent.isBlank() -> "\uD83E\uDD19"
|
||||
reactionContent == ReactionEvent.DISLIKE -> "\uD83D\uDC4E"
|
||||
else -> reactionContent
|
||||
}
|
||||
|
||||
val title = "$reactionSymbol $user"
|
||||
|
||||
val reactedContent =
|
||||
reactedNote
|
||||
?.event
|
||||
?.content
|
||||
?.split("\n")
|
||||
?.firstOrNull() ?: ""
|
||||
|
||||
val content =
|
||||
if (reactedContent.isNotBlank()) {
|
||||
stringRes(
|
||||
applicationContext,
|
||||
R.string.app_notification_reactions_channel_message_for,
|
||||
reactedContent,
|
||||
)
|
||||
} else {
|
||||
stringRes(
|
||||
applicationContext,
|
||||
R.string.app_notification_reactions_channel_message,
|
||||
user,
|
||||
)
|
||||
}
|
||||
|
||||
val noteUri =
|
||||
"notifications$ACCOUNT_QUERY_PARAM" +
|
||||
account.signer.pubKey
|
||||
.hexToByteArray()
|
||||
.toNpub()
|
||||
|
||||
notificationManager()
|
||||
.sendReactionNotification(
|
||||
event.id,
|
||||
content,
|
||||
title,
|
||||
event.createdAt,
|
||||
userPicture,
|
||||
noteUri,
|
||||
applicationContext,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun notifyChessEvent(
|
||||
event: BaseChessEvent,
|
||||
account: Account,
|
||||
contentStringRes: Int,
|
||||
) {
|
||||
if (
|
||||
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
|
||||
event.pubKey != account.signer.pubKey
|
||||
) {
|
||||
val author = LocalCache.getOrCreateUser(event.pubKey)
|
||||
val user = author.toBestDisplayName()
|
||||
val userPicture = author.profilePicture()
|
||||
val title = stringRes(applicationContext, R.string.app_notification_chess_channel_name)
|
||||
val content = stringRes(applicationContext, contentStringRes, user)
|
||||
val noteUri =
|
||||
"notifications$ACCOUNT_QUERY_PARAM" +
|
||||
account.signer.pubKey
|
||||
.hexToByteArray()
|
||||
.toNpub()
|
||||
|
||||
notificationManager()
|
||||
.sendChessNotification(
|
||||
event.id,
|
||||
content,
|
||||
title,
|
||||
event.createdAt,
|
||||
userPicture,
|
||||
noteUri,
|
||||
applicationContext,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun notificationManager(): NotificationManager =
|
||||
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
|
||||
as NotificationManager
|
||||
|
||||
@@ -85,7 +85,7 @@ class NotificationReplyReceiver : BroadcastReceiver() {
|
||||
notificationManager.cancel(notificationId)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("NotificationReply", "Failed to send reply: ${e.message}")
|
||||
Log.e("NotificationReply") { "Failed to send reply: ${e.message}" }
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
|
||||
|
||||
@@ -45,8 +45,14 @@ import kotlinx.coroutines.withContext
|
||||
object NotificationUtils {
|
||||
private var dmChannel: NotificationChannel? = null
|
||||
private var zapChannel: NotificationChannel? = null
|
||||
private var reactionChannel: NotificationChannel? = null
|
||||
private var chessChannel: NotificationChannel? = null
|
||||
|
||||
private const val DM_GROUP_KEY = "com.vitorpamplona.amethyst.DM_NOTIFICATION"
|
||||
private const val ZAP_GROUP_KEY = "com.vitorpamplona.amethyst.ZAP_NOTIFICATION"
|
||||
private const val REACTION_GROUP_KEY = "com.vitorpamplona.amethyst.REACTION_NOTIFICATION"
|
||||
private const val CHESS_GROUP_KEY = "com.vitorpamplona.amethyst.CHESS_NOTIFICATION"
|
||||
|
||||
const val REPLY_ACTION = "com.vitorpamplona.amethyst.REPLY_ACTION"
|
||||
const val MARK_READ_ACTION = "com.vitorpamplona.amethyst.MARK_READ_ACTION"
|
||||
const val KEY_REPLY_TEXT = "key_reply_text"
|
||||
@@ -56,6 +62,8 @@ object NotificationUtils {
|
||||
|
||||
private const val DM_SUMMARY_ID = 0x10000
|
||||
private const val ZAP_SUMMARY_ID = 0x20000
|
||||
private const val REACTION_SUMMARY_ID = 0x40000
|
||||
private const val CHESS_SUMMARY_ID = 0x30000
|
||||
|
||||
fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel {
|
||||
if (dmChannel != null) return dmChannel!!
|
||||
@@ -99,6 +107,104 @@ object NotificationUtils {
|
||||
return zapChannel!!
|
||||
}
|
||||
|
||||
fun getOrCreateReactionChannel(applicationContext: Context): NotificationChannel {
|
||||
if (reactionChannel != null) return reactionChannel!!
|
||||
|
||||
reactionChannel =
|
||||
NotificationChannel(
|
||||
stringRes(applicationContext, R.string.app_notification_reactions_channel_id),
|
||||
stringRes(applicationContext, R.string.app_notification_reactions_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
).apply {
|
||||
description =
|
||||
stringRes(applicationContext, R.string.app_notification_reactions_channel_description)
|
||||
}
|
||||
|
||||
val notificationManager: NotificationManager =
|
||||
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
notificationManager.createNotificationChannel(reactionChannel!!)
|
||||
|
||||
return reactionChannel!!
|
||||
}
|
||||
|
||||
fun getOrCreateChessChannel(applicationContext: Context): NotificationChannel {
|
||||
if (chessChannel != null) return chessChannel!!
|
||||
|
||||
chessChannel =
|
||||
NotificationChannel(
|
||||
stringRes(applicationContext, R.string.app_notification_chess_channel_id),
|
||||
stringRes(applicationContext, R.string.app_notification_chess_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
).apply {
|
||||
description =
|
||||
stringRes(applicationContext, R.string.app_notification_chess_channel_description)
|
||||
}
|
||||
|
||||
val notificationManager: NotificationManager =
|
||||
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
notificationManager.createNotificationChannel(chessChannel!!)
|
||||
|
||||
return chessChannel!!
|
||||
}
|
||||
|
||||
suspend fun NotificationManager.sendReactionNotification(
|
||||
id: String,
|
||||
messageBody: String,
|
||||
messageTitle: String,
|
||||
time: Long,
|
||||
pictureUrl: String?,
|
||||
uri: String,
|
||||
applicationContext: Context,
|
||||
) {
|
||||
getOrCreateReactionChannel(applicationContext)
|
||||
val channelId = stringRes(applicationContext, R.string.app_notification_reactions_channel_id)
|
||||
|
||||
sendNotification(
|
||||
id = id,
|
||||
messageBody = messageBody,
|
||||
messageTitle = messageTitle,
|
||||
time = time,
|
||||
pictureUrl = pictureUrl,
|
||||
uri = uri,
|
||||
channelId = channelId,
|
||||
notificationGroupKey = REACTION_GROUP_KEY,
|
||||
category = NotificationCompat.CATEGORY_SOCIAL,
|
||||
summaryId = REACTION_SUMMARY_ID,
|
||||
summaryText = stringRes(applicationContext, R.string.app_notification_reactions_summary),
|
||||
applicationContext = applicationContext,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun NotificationManager.sendChessNotification(
|
||||
id: String,
|
||||
messageBody: String,
|
||||
messageTitle: String,
|
||||
time: Long,
|
||||
pictureUrl: String?,
|
||||
uri: String,
|
||||
applicationContext: Context,
|
||||
) {
|
||||
getOrCreateChessChannel(applicationContext)
|
||||
val channelId = stringRes(applicationContext, R.string.app_notification_chess_channel_id)
|
||||
|
||||
sendNotification(
|
||||
id = id,
|
||||
messageBody = messageBody,
|
||||
messageTitle = messageTitle,
|
||||
time = time,
|
||||
pictureUrl = pictureUrl,
|
||||
uri = uri,
|
||||
channelId = channelId,
|
||||
notificationGroupKey = CHESS_GROUP_KEY,
|
||||
category = NotificationCompat.CATEGORY_SOCIAL,
|
||||
summaryId = CHESS_SUMMARY_ID,
|
||||
summaryText = stringRes(applicationContext, R.string.app_notification_chess_summary),
|
||||
applicationContext = applicationContext,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun NotificationManager.sendZapNotification(
|
||||
id: String,
|
||||
messageBody: String,
|
||||
|
||||
@@ -69,7 +69,7 @@ class PokeyReceiver : BroadcastReceiver() {
|
||||
) {
|
||||
if (intent.action == POKEY_ACTION) { // it's best practice to verify intent action before performing any operation
|
||||
val eventStr = intent.getStringExtra("EVENT")
|
||||
Log.d(TAG, "New Pokey Notification Arrived $eventStr")
|
||||
Log.d(TAG) { "New Pokey Notification Arrived $eventStr" }
|
||||
|
||||
if (eventStr == null) return
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ class RegisterAccounts(
|
||||
accounts
|
||||
.mapNotNull { account ->
|
||||
if (account.hasPrivKey || account.loggedInWithExternalSigner) {
|
||||
Log.d(tag, "Register Account ${account.npub}")
|
||||
Log.d(tag) { "Register Account ${account.npub}" }
|
||||
|
||||
val acc = LocalPreferences.loadAccountConfigFromEncryptedStorage(account.npub)
|
||||
if (acc != null && acc.isWriteable()) {
|
||||
@@ -87,10 +87,10 @@ class RegisterAccounts(
|
||||
|
||||
if (isDebug) {
|
||||
val readRelays = nip65Read.joinToString(", ") { it.url }
|
||||
Log.d(tag, "Register Account ${account.npub} NIP65 Reads $readRelays")
|
||||
Log.d(tag) { "Register Account ${account.npub} NIP65 Reads $readRelays" }
|
||||
|
||||
val dmRelays = nip17Read.joinToString(", ") { it.url }
|
||||
Log.d(tag, "Register Account ${account.npub} NIP17 Reads $dmRelays")
|
||||
Log.d(tag) { "Register Account ${account.npub} NIP17 Reads $dmRelays" }
|
||||
}
|
||||
|
||||
val relays = (nip65Read + nip17Read)
|
||||
@@ -132,7 +132,7 @@ class RegisterAccounts(
|
||||
val client = client(url)
|
||||
|
||||
client.newCall(request).executeAsync().use { response ->
|
||||
Log.i(tag, "Server registration ${response.isSuccessful}")
|
||||
Log.i(tag) { "Server registration ${response.isSuccessful}" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ class EncryptedBlobInterceptor(
|
||||
val body = peekBody(Long.MAX_VALUE)
|
||||
|
||||
// Only tries to decrypt if the content-type is a byte array
|
||||
if (body.contentType().toString() != "application/octet-stream") {
|
||||
return null
|
||||
}
|
||||
// if (body.contentType().toString() != "application/octet-stream") {
|
||||
// return null
|
||||
// }
|
||||
|
||||
val bytes = body.bytes()
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class LoggingInterceptor : Interceptor {
|
||||
val response: Response = chain.proceed(request)
|
||||
val t2 = System.nanoTime()
|
||||
|
||||
Log.d("OkHttpLog", "Req $port ${request.url} in ${(t2 - t1) / 1e6}ms")
|
||||
Log.d("OkHttpLog") { "Req $port ${request.url} in ${(t2 - t1) / 1e6}ms" }
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ fun GetVideoController(
|
||||
keepPlaying = mediaItem.src.keepPlaying,
|
||||
context = context,
|
||||
).onEach { state ->
|
||||
Log.d("PlaybackService", "Controller instance: ${state.controller}")
|
||||
Log.d("PlaybackService") { "Controller instance: ${state.controller}" }
|
||||
|
||||
if (BackgroundMedia.isPlaying()) {
|
||||
// There is a video playing, start this one on mute.
|
||||
@@ -57,7 +57,7 @@ fun GetVideoController(
|
||||
// There is no other video playing. Use the default mute state to
|
||||
// decide if sound is on or not.
|
||||
state.controller.volume = if (muted) 0f else 1f
|
||||
Log.d("PlaybackService", "OnEach $muted")
|
||||
Log.d("PlaybackService") { "OnEach $muted" }
|
||||
}
|
||||
|
||||
if (play) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
@@ -27,8 +27,6 @@ import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@@ -42,20 +40,18 @@ class VideoCache {
|
||||
|
||||
lateinit var cacheDataSourceFactory: CacheDataSource.Factory
|
||||
|
||||
suspend fun initFileCache(
|
||||
fun initFileCache(
|
||||
context: Context,
|
||||
cachePath: File,
|
||||
) {
|
||||
exoDatabaseProvider = StandaloneDatabaseProvider(context)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
simpleCache =
|
||||
SimpleCache(
|
||||
cachePath,
|
||||
leastRecentlyUsedCacheEvictor,
|
||||
exoDatabaseProvider,
|
||||
)
|
||||
}
|
||||
simpleCache =
|
||||
SimpleCache(
|
||||
cachePath,
|
||||
leastRecentlyUsedCacheEvictor,
|
||||
exoDatabaseProvider,
|
||||
)
|
||||
}
|
||||
|
||||
// This method should be called when proxy setting changes.
|
||||
|
||||
@@ -22,18 +22,15 @@ package com.vitorpamplona.amethyst.service.playback.diskCache
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.service.safeCacheDir
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class VideoCacheFactory {
|
||||
companion object {
|
||||
fun new(app: Context): VideoCache {
|
||||
val newCache = VideoCache()
|
||||
runBlocking {
|
||||
newCache.initFileCache(
|
||||
app,
|
||||
app.safeCacheDir().resolve("exoplayer"),
|
||||
)
|
||||
}
|
||||
newCache.initFileCache(
|
||||
app,
|
||||
app.safeCacheDir().resolve("exoplayer"),
|
||||
)
|
||||
return newCache
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +60,22 @@ class PipVideoActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
finishAndRemoveTask()
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
if (!isInPictureInPictureMode) {
|
||||
// User dismissed PiP (swiped away or expanded).
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
finishAndRemoveTask()
|
||||
super.finish()
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (!isInPictureInPictureMode) {
|
||||
// Only finish if we're not in PiP mode.
|
||||
// When the screen locks while in PiP, we stay alive
|
||||
// so the PlaybackService can continue audio playback.
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -32,7 +32,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.compose.ContentFrame
|
||||
import androidx.media3.ui.compose.state.rememberMuteButtonState
|
||||
@@ -51,6 +53,8 @@ fun RenderPipVideo(
|
||||
controller: MediaControllerState,
|
||||
waveformData: WaveformData?,
|
||||
) {
|
||||
KeepScreenOnWhilePlaying(controller)
|
||||
|
||||
val modifier =
|
||||
remember {
|
||||
val ratio =
|
||||
@@ -78,6 +82,31 @@ fun RenderPipVideo(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeepScreenOnWhilePlaying(controller: MediaControllerState) {
|
||||
val view = LocalView.current
|
||||
|
||||
DisposableEffect(controller.controller, view) {
|
||||
val listener =
|
||||
object : Player.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (view.keepScreenOn != isPlaying) {
|
||||
view.keepScreenOn = isPlaying
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set initial state
|
||||
view.keepScreenOn = controller.controller.isPlaying
|
||||
|
||||
controller.controller.addListener(listener)
|
||||
onDispose {
|
||||
controller.controller.removeListener(listener)
|
||||
view.keepScreenOn = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RegisterControllerReceiver(controllerState: MediaControllerState) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -67,14 +67,14 @@ object PlaybackServiceClient {
|
||||
.setConnectionHints(bundle)
|
||||
.buildAsync()
|
||||
|
||||
Log.d("PlaybackService", "Preparing Controller $id $videoUri")
|
||||
Log.d("PlaybackService") { "Preparing Controller $id $videoUri" }
|
||||
|
||||
controllerFuture.addListener(
|
||||
{
|
||||
try {
|
||||
val controller = controllerFuture.get(5, TimeUnit.SECONDS)
|
||||
|
||||
Log.d("PlaybackService", "Controller Ready $id $videoUri")
|
||||
Log.d("PlaybackService") { "Controller Ready $id $videoUri" }
|
||||
|
||||
// checks if the player is still active before engaging further
|
||||
trySend(
|
||||
@@ -92,11 +92,11 @@ object PlaybackServiceClient {
|
||||
)
|
||||
|
||||
awaitClose {
|
||||
Log.d("PlaybackService", "Releasing Controller $id $videoUri")
|
||||
Log.d("PlaybackService") { "Releasing Controller $id $videoUri" }
|
||||
try {
|
||||
MediaController.releaseFuture(controllerFuture)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Playback Client", "Failed to release Playback Client for $id $videoUri ${e.message}")
|
||||
Log.e("Playback Client") { "Failed to release Playback Client for $id $videoUri ${e.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,21 +74,29 @@ class RelayProxyClientConnector(
|
||||
if (it.connectivity is ConnectivityStatus.StartingService) {
|
||||
// ignore
|
||||
} else if (it.connectivity is ConnectivityStatus.Off) {
|
||||
Log.d("ManageRelayServices", "Connectivity Off: Pausing Relay Services ${it.connectivity}")
|
||||
Log.d("ManageRelayServices") { "Connectivity Off: Pausing Relay Services ${it.connectivity}" }
|
||||
if (client.isActive()) {
|
||||
client.disconnect()
|
||||
}
|
||||
if (it.torStatus is TorServiceStatus.Active) {
|
||||
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_DORMANT)
|
||||
Log.d("ManageRelayServices", "Pausing Tor Activity")
|
||||
try {
|
||||
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_DORMANT)
|
||||
Log.d("ManageRelayServices", "Pausing Tor Activity")
|
||||
} catch (e: Exception) {
|
||||
Log.e("ManageRelayServices") { "Failed to signal Tor dormant: ${e.message}" }
|
||||
}
|
||||
}
|
||||
} else if (it.connectivity is ConnectivityStatus.Active && !client.isActive()) {
|
||||
Log.d("ManageRelayServices", "Connectivity On: Resuming Relay Services")
|
||||
|
||||
if (it.torStatus is TorServiceStatus.Active) {
|
||||
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_ACTIVE)
|
||||
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_NEWNYM)
|
||||
Log.d("ManageRelayServices", "Resuming Tor Activity with new nym")
|
||||
try {
|
||||
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_ACTIVE)
|
||||
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_NEWNYM)
|
||||
Log.d("ManageRelayServices", "Resuming Tor Activity with new nym")
|
||||
} catch (e: Exception) {
|
||||
Log.e("ManageRelayServices") { "Failed to signal Tor active: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
// only calls this if the client is not active. Otherwise goes to the else below
|
||||
|
||||
@@ -80,7 +80,7 @@ class AuthCoordinator(
|
||||
if (account == null) return
|
||||
|
||||
if (isDebug) {
|
||||
Log.d("AuthCoordinator", "Watch $account")
|
||||
Log.d("AuthCoordinator") { "Watch $account" }
|
||||
}
|
||||
|
||||
authWithAccounts.add(account)
|
||||
@@ -91,7 +91,7 @@ class AuthCoordinator(
|
||||
if (account == null) return
|
||||
|
||||
if (isDebug) {
|
||||
Log.d("AuthCoordinator", "Unwatch $account")
|
||||
Log.d("AuthCoordinator") { "Unwatch $account" }
|
||||
}
|
||||
|
||||
authWithAccounts.remove(account)
|
||||
|
||||
@@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -68,7 +68,7 @@ abstract class PerUniqueIdEoseManager<T, U : Any>(
|
||||
|
||||
open fun newSub(key: T): Subscription =
|
||||
requestNewSubscription(
|
||||
object : IRequestListener {
|
||||
object : SubscriptionListener {
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
|
||||
@@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -69,7 +69,7 @@ abstract class PerUserAndFollowListEoseManager<T, U : Any>(
|
||||
|
||||
open fun newSub(key: T): Subscription =
|
||||
requestNewSubscription(
|
||||
object : IRequestListener {
|
||||
object : SubscriptionListener {
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
|
||||
@@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -67,7 +67,7 @@ abstract class PerUserEoseManager<T>(
|
||||
|
||||
open fun newSub(key: T): Subscription =
|
||||
requestNewSubscription(
|
||||
object : IRequestListener {
|
||||
object : SubscriptionListener {
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
|
||||
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@@ -41,7 +41,7 @@ abstract class SingleSubNoEoseCacheEoseManager<T>(
|
||||
) : BaseEoseManager<T>(client, allKeys) {
|
||||
val sub =
|
||||
requestNewSubscription(
|
||||
object : IRequestListener {
|
||||
object : SubscriptionListener {
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
|
||||
@@ -37,8 +37,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.datasource.RelayFeedFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfoNip66FilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@@ -80,9 +85,15 @@ class RelaySubscriptionsCoordinator(
|
||||
val hashtags = HashtagFilterAssembler(client)
|
||||
val geohashes = GeoHashFilterAssembler(client)
|
||||
val relayFeed = RelayFeedFilterAssembler(client)
|
||||
val relayInfoNip66 = RelayInfoNip66FilterAssembler(client)
|
||||
val followPacks = FollowPackFeedFilterAssembler(client)
|
||||
val chess = ChessFilterAssembler(client)
|
||||
|
||||
val polls = PollsFilterAssembler(client)
|
||||
val pictures = PicturesFilterAssembler(client)
|
||||
val shorts = ShortsFilterAssembler(client)
|
||||
val longs = LongsFilterAssembler(client)
|
||||
|
||||
// active when sending zaps via NWC
|
||||
val nwc = NWCPaymentFilterAssembler(client)
|
||||
|
||||
@@ -93,6 +104,10 @@ class RelaySubscriptionsCoordinator(
|
||||
chatroomList,
|
||||
video,
|
||||
discovery,
|
||||
polls,
|
||||
pictures,
|
||||
shorts,
|
||||
longs,
|
||||
channelFinder,
|
||||
eventFinder,
|
||||
userFinder,
|
||||
@@ -105,6 +120,7 @@ class RelaySubscriptionsCoordinator(
|
||||
hashtags,
|
||||
geohashes,
|
||||
relayFeed,
|
||||
relayInfoNip66,
|
||||
chess,
|
||||
nwc,
|
||||
)
|
||||
|
||||
@@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.IEoseManager
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.LocalCache.users
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState
|
||||
@@ -35,7 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineT
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.SubscriptionController
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -107,7 +106,7 @@ class AccountFollowsLoaderSubAssembler(
|
||||
val sub =
|
||||
orchestrator.requestNewSubscription(
|
||||
if (isDebug) logTag + newSubId() else newSubId(),
|
||||
object : IRequestListener {
|
||||
object : SubscriptionListener {
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metada
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState.Companion.APP_SPECIFIC_DATA_D_TAG
|
||||
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
@@ -43,6 +42,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
@@ -38,6 +37,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
|
||||
|
||||
@@ -24,13 +24,21 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
|
||||
|
||||
val ReportsAndBookmarksFromKeyKinds =
|
||||
listOf(
|
||||
ReportEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
OldBookmarkListEvent.KIND,
|
||||
LabeledBookmarkListEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
RequestToVanishEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterBookmarksAndReportsFromKey(
|
||||
|
||||
@@ -30,14 +30,16 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEve
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
|
||||
|
||||
val FollowAndMutesFromKeyKinds =
|
||||
listOf(
|
||||
PeopleListEvent.KIND,
|
||||
FollowListEvent.KIND,
|
||||
MuteListEvent.KIND,
|
||||
BadgeProfilesEvent.KIND,
|
||||
AcceptedBadgeSetEvent.KIND,
|
||||
ProfileBadgesEvent.KIND,
|
||||
EmojiPackSelectionEvent.KIND,
|
||||
EphemeralChatListEvent.KIND,
|
||||
ChannelListEvent.KIND,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user