mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
899 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a86f64fce5 | ||
|
|
075a92a709 | ||
|
|
67d6b9e350 | ||
|
|
f7fd1e79f0 | ||
|
|
01afb0aeeb | ||
|
|
d777e94aad | ||
|
|
1bd2b2168f | ||
|
|
719e34ea8d | ||
|
|
78c13a7d29 | ||
|
|
61d4f3000f | ||
|
|
22c8ecefed | ||
|
|
8995c7a51d | ||
|
|
ef267acee2 | ||
|
|
cbf9effcf4 | ||
|
|
007f512cc4 | ||
|
|
96ee6baa8f | ||
|
|
f400cc6088 | ||
|
|
23299b5ef3 | ||
|
|
88a3e2a74b | ||
|
|
e5fe63d630 | ||
|
|
e6a10e07c4 | ||
|
|
19846e7ff3 | ||
|
|
05af3a78e4 | ||
|
|
c9370f8d02 | ||
|
|
f186b78ca9 | ||
|
|
abcaf19d2b | ||
|
|
43fbf4f40f | ||
|
|
9f66cd0660 | ||
|
|
b80f24b587 | ||
|
|
ee52f10192 | ||
|
|
8d41b58167 | ||
|
|
ac291c996c | ||
|
|
29d11c8281 | ||
|
|
e2c50847d0 | ||
|
|
b339e3a245 | ||
|
|
8963024a95 | ||
|
|
27bc950afb | ||
|
|
0ddbb4616b | ||
|
|
adb484bf56 | ||
|
|
bd30b526a9 | ||
|
|
ce49725771 | ||
|
|
6c64297415 | ||
|
|
90330165d2 | ||
|
|
fe876de4c9 | ||
|
|
b17d709696 | ||
|
|
8554257329 | ||
|
|
9eee5d9e1e | ||
|
|
b4810fd12c | ||
|
|
bf803384a8 | ||
|
|
e8eb4c688c | ||
|
|
a5207c3f0a | ||
|
|
ce073553ee | ||
|
|
dd97dfc67f | ||
|
|
1cfe953a65 | ||
|
|
d4b91845d5 | ||
|
|
a114bf9dfa | ||
|
|
b14c588a34 | ||
|
|
1589b646a8 | ||
|
|
8c3ab80e47 | ||
|
|
4690f59922 | ||
|
|
255389aa3c | ||
|
|
cf756669e5 | ||
|
|
d7ca9d5a82 | ||
|
|
5f88249be6 | ||
|
|
db354b646f | ||
|
|
17fdf05847 | ||
|
|
ddfe61eb9f | ||
|
|
8bdd9d0979 | ||
|
|
bd1a70039a | ||
|
|
87599b7e59 | ||
|
|
5762490098 | ||
|
|
e634040504 | ||
|
|
15e7909222 | ||
|
|
ffaa06fed7 | ||
|
|
58128d9ad0 | ||
|
|
011aebfc27 | ||
|
|
7230f67782 | ||
|
|
93e80dda83 | ||
|
|
0505011eab | ||
|
|
ea83dc3fd7 | ||
|
|
948180157f | ||
|
|
a996065c05 | ||
|
|
c328a3aefe | ||
|
|
6ccec0681a | ||
|
|
26a6b5d219 | ||
|
|
d17f39e85b | ||
|
|
2ac5a57efe | ||
|
|
6727bd0430 | ||
|
|
34aca458ed | ||
|
|
ba082d86e2 | ||
|
|
409b3b43f7 | ||
|
|
545cd2ff6d | ||
|
|
a82d6565fa | ||
|
|
c07202944d | ||
|
|
178582f3c4 | ||
|
|
c2c3442b74 | ||
|
|
43942891b7 | ||
|
|
fc488064b7 | ||
|
|
4535966c32 | ||
|
|
83f460c01d | ||
|
|
6c630c75c2 | ||
|
|
8d33ea8f99 | ||
|
|
558d952a00 | ||
|
|
1eb2309952 | ||
|
|
0ffbbee31a | ||
|
|
ed8b285700 | ||
|
|
452082c6cb | ||
|
|
8168d353dd | ||
|
|
59cdb222d5 | ||
|
|
712965cc8f | ||
|
|
5741914c2a | ||
|
|
b8cce80d9f | ||
|
|
7dfa343ad8 | ||
|
|
be804092d7 | ||
|
|
50b551bb80 | ||
|
|
7c439f2448 | ||
|
|
5ce184d400 | ||
|
|
bda1fcadf8 | ||
|
|
379e539e89 | ||
|
|
f14b545947 | ||
|
|
b8fff0c428 | ||
|
|
942de50148 | ||
|
|
76be7b0d58 | ||
|
|
4b260cfc07 | ||
|
|
eee1487883 | ||
|
|
27380f276c | ||
|
|
f62b1667c3 | ||
|
|
eb4f0b9af9 | ||
|
|
7c374fb586 | ||
|
|
1e12decd36 | ||
|
|
1411de32a0 | ||
|
|
7ea61824c8 | ||
|
|
f0923fb393 | ||
|
|
3eb662851b | ||
|
|
f9bd660774 | ||
|
|
bdb2637803 | ||
|
|
c3c75b46a7 | ||
|
|
3c7104fb6a | ||
|
|
c111912d2f | ||
|
|
0f4e514308 | ||
|
|
ccf9d4ce80 | ||
|
|
95cc0783fd | ||
|
|
f9f7cdf12f | ||
|
|
40d417038e | ||
|
|
895562b4a9 | ||
|
|
b8511b5ac3 | ||
|
|
40ce627de0 | ||
|
|
56a856cf71 | ||
|
|
904b8c445f | ||
|
|
3a744b9c1d | ||
|
|
6d8b5fa85d | ||
|
|
a913194c41 | ||
|
|
a9ebf561e7 | ||
|
|
fb7610cafd | ||
|
|
cd35c8e30e | ||
|
|
f3209d2415 | ||
|
|
d44b24936c | ||
|
|
a9aadb60fc | ||
|
|
59f2cdcb2f | ||
|
|
00eb6ee3f1 | ||
|
|
09e4b2ea44 | ||
|
|
f8b920a128 | ||
|
|
0705ee9581 | ||
|
|
bf664ea0c5 | ||
|
|
39f3aef7b5 | ||
|
|
7254dce3a9 | ||
|
|
4e31a9a5ac | ||
|
|
d01f6a2178 | ||
|
|
2332623cde | ||
|
|
93994564f6 | ||
|
|
e1c54f52e3 | ||
|
|
371d3bf0f7 | ||
|
|
b0e7082ec6 | ||
|
|
34971211f1 | ||
|
|
b065956aec | ||
|
|
36f79ca581 | ||
|
|
7aeb4e7582 | ||
|
|
25d6562500 | ||
|
|
67e3b05a42 | ||
|
|
a157c60697 | ||
|
|
50d63a0501 | ||
|
|
e1cbfbb280 | ||
|
|
b6aec40e9a | ||
|
|
e70a191191 | ||
|
|
59a5a15866 | ||
|
|
b82145ac10 | ||
|
|
ee80361478 | ||
|
|
2d551d4adf | ||
|
|
35da3b4efe | ||
|
|
5306dd52d7 | ||
|
|
2ada3855d0 | ||
|
|
cabdfea8f9 | ||
|
|
d9c0a9af43 | ||
|
|
a7c846f641 | ||
|
|
f7ed4d2aaa | ||
|
|
031e604923 | ||
|
|
551bd344e0 | ||
|
|
c21b2ca60f | ||
|
|
db1d5c7e82 | ||
|
|
17618ac361 | ||
|
|
bf94d0d91c | ||
|
|
66096e5b98 | ||
|
|
c8a608718c | ||
|
|
df54f7bb67 | ||
|
|
5b230a4c9b | ||
|
|
d8907e3dfb | ||
|
|
889e3d63ff | ||
|
|
5f7b318647 | ||
|
|
e2b2b10994 | ||
|
|
a055f473cb | ||
|
|
c735c358bb | ||
|
|
50cbbb55b6 | ||
|
|
103845225b | ||
|
|
a27df3a9b6 | ||
|
|
d51d1f4453 | ||
|
|
f899d446bc | ||
|
|
c1295ed97f | ||
|
|
774c5412d3 | ||
|
|
38d5527b91 | ||
|
|
1ce352e69f | ||
|
|
bf3e5c4568 | ||
|
|
200db73ce4 | ||
|
|
2d1fd4a48a | ||
|
|
a0cb2a7db6 | ||
|
|
382319addf | ||
|
|
37625987c1 | ||
|
|
63bd0b6cae | ||
|
|
48a69f1d26 | ||
|
|
f663bb78aa | ||
|
|
a163c4188f | ||
|
|
fabec35593 | ||
|
|
e4ce7415a3 | ||
|
|
911fdffa95 | ||
|
|
3a73de6828 | ||
|
|
c4653b147c | ||
|
|
cbba2fe423 | ||
|
|
8aa7c57a24 | ||
|
|
e45b4b11dc | ||
|
|
e20afd39a3 | ||
|
|
4c633c7dec | ||
|
|
97fc10d6ea | ||
|
|
b8d0f057c5 | ||
|
|
990cca3d64 | ||
|
|
4a0c924d4f | ||
|
|
5d6e7d1921 | ||
|
|
563e985a2b | ||
|
|
50adf82d90 | ||
|
|
7a05f527dd | ||
|
|
0668f2caa4 | ||
|
|
4af576f5f4 | ||
|
|
fdbf261455 | ||
|
|
e422dcda50 | ||
|
|
a210bd1c8f | ||
|
|
49f77af3e8 | ||
|
|
7d22ef0d12 | ||
|
|
deee1a1c85 | ||
|
|
7a1dcbc76e | ||
|
|
68264f2c7f | ||
|
|
b1d3aa968d | ||
|
|
2ebdedafaa | ||
|
|
4ffc121003 | ||
|
|
a4f1e45aac | ||
|
|
3a3d1a6f25 | ||
|
|
85bdf67ac3 | ||
|
|
9e991f8a49 | ||
|
|
e6c841373b | ||
|
|
ba756c9acd | ||
|
|
349156f380 | ||
|
|
133c897731 | ||
|
|
13402fa5d4 | ||
|
|
2a3c31cc5b | ||
|
|
5fcf0c5938 | ||
|
|
0fab3afea2 | ||
|
|
0bc38c1fa4 | ||
|
|
0c0410d1dd | ||
|
|
fa971b1822 | ||
|
|
24f7d76dc4 | ||
|
|
973cfd24fa | ||
|
|
5c09cd0b55 | ||
|
|
5975130824 | ||
|
|
0a23e23801 | ||
|
|
a0c1b01a4b | ||
|
|
0708936c23 | ||
|
|
ac60e3d221 | ||
|
|
3c622f544c | ||
|
|
a8d01e3667 | ||
|
|
9aa5f1bd9f | ||
|
|
192a257780 | ||
|
|
0045426840 | ||
|
|
0ab7dd9797 | ||
|
|
a6e4a8d4fc | ||
|
|
5f59f1ab41 | ||
|
|
049430cd2e | ||
|
|
b546294078 | ||
|
|
456ef86c85 | ||
|
|
b8ad2961b9 | ||
|
|
0348454e36 | ||
|
|
b55cdda52a | ||
|
|
8c93560058 | ||
|
|
72b800bfb4 | ||
|
|
4a6dbd4ba5 | ||
|
|
52686c3488 | ||
|
|
d8613da3ad | ||
|
|
bea393c4ef | ||
|
|
cf4104292c | ||
|
|
deaaa30b92 | ||
|
|
1fdf869c05 | ||
|
|
701acb004f | ||
|
|
341994e505 | ||
|
|
c432a54561 | ||
|
|
55198b1546 | ||
|
|
b9d369a88c | ||
|
|
6c72c36b9a | ||
|
|
369aab2c0c | ||
|
|
e129796418 | ||
|
|
eb99febb2b | ||
|
|
da79d5b0ba | ||
|
|
0843d5db95 | ||
|
|
df77bd6633 | ||
|
|
cf8c8e47a2 | ||
|
|
26462a150e | ||
|
|
66a5412961 | ||
|
|
93f33cca70 | ||
|
|
4b959577f0 | ||
|
|
88747ad1b7 | ||
|
|
13d05dc599 | ||
|
|
b002f9f37e | ||
|
|
7994945209 | ||
|
|
9085f4bbea | ||
|
|
beff7e60b7 | ||
|
|
e418a26f41 | ||
|
|
2df575d4af | ||
|
|
88fbee84dc | ||
|
|
21c1d705a1 | ||
|
|
97668513b5 | ||
|
|
f6213c2d9a | ||
|
|
85b234d372 | ||
|
|
28df59c00d | ||
|
|
bc8e18a01b | ||
|
|
1b36d7e189 | ||
|
|
5f83379459 | ||
|
|
90b682a38f | ||
|
|
c6107dd784 | ||
|
|
fd6be80b7e | ||
|
|
f901f8ab8f | ||
|
|
b83ea61522 | ||
|
|
168caf0ec6 | ||
|
|
04f5e8738a | ||
|
|
c8f51380aa | ||
|
|
612dd175b3 | ||
|
|
a56d9631ed | ||
|
|
1fd5ba0451 | ||
|
|
132c00d434 | ||
|
|
47cc8fc4ae | ||
|
|
e12c52ec2c | ||
|
|
34cd26b004 | ||
|
|
1c82417a0a | ||
|
|
c3b87e83cb | ||
|
|
e72a588dab | ||
|
|
3033a4cbc8 | ||
|
|
ec82f89225 | ||
|
|
d7cf8305d3 | ||
|
|
7c41da0c4c | ||
|
|
59be0986eb | ||
|
|
4554813d72 | ||
|
|
7d69122409 | ||
|
|
a6e306f3de | ||
|
|
f6b3fdcab4 | ||
|
|
dfb683e00d | ||
|
|
bb1d805080 | ||
|
|
c936e91ec8 | ||
|
|
5d2e718b93 | ||
|
|
4cffd6d868 | ||
|
|
26005a062d | ||
|
|
80906f2c5a | ||
|
|
631f1d9cfc | ||
|
|
a8a6a10d6e | ||
|
|
401a3c47d9 | ||
|
|
ff0a4eb88f | ||
|
|
1384a68ee3 | ||
|
|
86e50ac984 | ||
|
|
d2d811f670 | ||
|
|
61459187d7 | ||
|
|
509d703226 | ||
|
|
194ba08df0 | ||
|
|
bf1188addf | ||
|
|
7ddce17bdd | ||
|
|
ee8ac08845 | ||
|
|
45192d1cf7 | ||
|
|
9c024e334b | ||
|
|
058278956b | ||
|
|
564cebea9d | ||
|
|
4fc9386393 | ||
|
|
6368d17d09 | ||
|
|
b694e143d0 | ||
|
|
4b60c3e20b | ||
|
|
6e4e432adf | ||
|
|
4c3eaf972c | ||
|
|
ba0fc67a94 | ||
|
|
f31292a201 | ||
|
|
1f6135d45d | ||
|
|
d04cf97418 | ||
|
|
8466020137 | ||
|
|
54afc2c4dc | ||
|
|
ed7fa2a80f | ||
|
|
06f94150f9 | ||
|
|
361e46c99f | ||
|
|
6ba60dce3f | ||
|
|
f9e0c82fc8 | ||
|
|
45b7b5601b | ||
|
|
ccf64687a8 | ||
|
|
0ba3385771 | ||
|
|
381692a0a2 | ||
|
|
206f8847fc | ||
|
|
5b969d92f3 | ||
|
|
621290ec33 | ||
|
|
94a2d9e07d | ||
|
|
8b60aeaa94 | ||
|
|
6391ddbbaa | ||
|
|
6ac2a7150f | ||
|
|
ab6d0961e8 | ||
|
|
3c87925b38 | ||
|
|
54963067aa | ||
|
|
ec9d229392 | ||
|
|
ce81157dad | ||
|
|
0499dcc647 | ||
|
|
748085078d | ||
|
|
fadd9c1940 | ||
|
|
313e2519b2 | ||
|
|
d8c403d3d5 | ||
|
|
b30d87266d | ||
|
|
f9204d37f2 | ||
|
|
d3f1db4367 | ||
|
|
9851c4b6be | ||
|
|
811ad5e00a | ||
|
|
f2ca1d78da | ||
|
|
00d5e2fed3 | ||
|
|
807eda8401 | ||
|
|
6cffda09bb | ||
|
|
1c0766cc76 | ||
|
|
5175c61171 | ||
|
|
3f8abd74ba | ||
|
|
04b8f6cca2 | ||
|
|
ec10d53195 | ||
|
|
ccc5d03d84 | ||
|
|
e5df870ace | ||
|
|
b030486e7e | ||
|
|
544d6d3933 | ||
|
|
db9160a6c2 | ||
|
|
41a233d175 | ||
|
|
b6dd6a988b | ||
|
|
9af1e10ec2 | ||
|
|
ed758ef13e | ||
|
|
947f59fa6f | ||
|
|
524ead2eac | ||
|
|
49dc63c876 | ||
|
|
8588c9a6fc | ||
|
|
f805b70ec0 | ||
|
|
f5202dd8a9 | ||
|
|
97d6c791c0 | ||
|
|
87f224b6b8 | ||
|
|
931c6681c6 | ||
|
|
96b8c77bcd | ||
|
|
366534d1d5 | ||
|
|
054cfa0de9 | ||
|
|
d93a8d94d2 | ||
|
|
0026668aca | ||
|
|
7a280a3cde | ||
|
|
3eb02cbbd8 | ||
|
|
1e18edfc02 | ||
|
|
9f5301d30a | ||
|
|
a07ebc6e12 | ||
|
|
3301371d82 | ||
|
|
7949fc66b2 | ||
|
|
1d0f584c3e | ||
|
|
6a3ebc1d95 | ||
|
|
e10c429de0 | ||
|
|
e4a8a512d2 | ||
|
|
8373f6ba19 | ||
|
|
d2d5ade84e | ||
|
|
3acdde7c53 | ||
|
|
897396ff24 | ||
|
|
ee39b3ae18 | ||
|
|
6c892ba733 | ||
|
|
64cf81b523 | ||
|
|
acade3c762 | ||
|
|
4cd86c8b1c | ||
|
|
fdf372719c | ||
|
|
50f173e406 | ||
|
|
edc7f49ce2 | ||
|
|
b20eb860ff | ||
|
|
aba60a87d6 | ||
|
|
4f3b7b24f2 | ||
|
|
6f44d7c28a | ||
|
|
a96ab764b3 | ||
|
|
1be46b7167 | ||
|
|
5c601d74b9 | ||
|
|
2027c7d77d | ||
|
|
faffe2a936 | ||
|
|
c375fa0ab8 | ||
|
|
123eca1f90 | ||
|
|
83e87c2cf4 | ||
|
|
f4501d799d | ||
|
|
5796052f36 | ||
|
|
c1012bd859 | ||
|
|
6440b86a5e | ||
|
|
dddf65e60f | ||
|
|
80c884a4d1 | ||
|
|
497b019093 | ||
|
|
03b320e5b7 | ||
|
|
00c0472155 | ||
|
|
8a66276755 | ||
|
|
6eab098f2e | ||
|
|
eaf8cb3384 | ||
|
|
cd0b1b65fb | ||
|
|
e329b096ba | ||
|
|
239cac430e | ||
|
|
24a5750155 | ||
|
|
cb5299e265 | ||
|
|
c3c8b87b91 | ||
|
|
98aaef61c7 | ||
|
|
1d3dadace2 | ||
|
|
bd70058f78 | ||
|
|
67f9557081 | ||
|
|
689066fd44 | ||
|
|
cdd5995513 | ||
|
|
0c4cd4f940 | ||
|
|
c84554576c | ||
|
|
7616d01166 | ||
|
|
60b71a5561 | ||
|
|
a7b89cb778 | ||
|
|
6b18711fe6 | ||
|
|
f0b95a3593 | ||
|
|
52cf9e1576 | ||
|
|
43f8053b30 | ||
|
|
74cc9535ac | ||
|
|
7e80ed2af9 | ||
|
|
1788dfc0b9 | ||
|
|
8f0a829a4d | ||
|
|
bfc920ca04 | ||
|
|
a21b6115c7 | ||
|
|
8e39da347a | ||
|
|
58277cf717 | ||
|
|
7ef6da1a17 | ||
|
|
ec48977a6f | ||
|
|
c5b60c3bd8 | ||
|
|
613978d7ba | ||
|
|
afd20d3f8f | ||
|
|
ea07e52e63 | ||
|
|
da469754c4 | ||
|
|
6dd2f47fc9 | ||
|
|
47d201f936 | ||
|
|
46fec1d480 | ||
|
|
557ba959e2 | ||
|
|
e121b2849c | ||
|
|
2e16a1dca6 | ||
|
|
33155efa99 | ||
|
|
bd3eed4a29 | ||
|
|
ac48705980 | ||
|
|
bc10d7184f | ||
|
|
0886af4386 | ||
|
|
3628091511 | ||
|
|
f5061e470a | ||
|
|
a0ba3802d9 | ||
|
|
4118e3d45d | ||
|
|
f850d5291e | ||
|
|
cb3bc605bc | ||
|
|
16e3cba651 | ||
|
|
ee9e102eb2 | ||
|
|
f01895b3e9 | ||
|
|
00a3918d20 | ||
|
|
35a123a185 | ||
|
|
c9dc30d976 | ||
|
|
c06c03928e | ||
|
|
1f8d0295d5 | ||
|
|
8f1027b55d | ||
|
|
4dace5aaf8 | ||
|
|
8cb9d13567 | ||
|
|
f092326dcd | ||
|
|
1f6d7d3fd2 | ||
|
|
062182a7ec | ||
|
|
9534582141 | ||
|
|
a078df6159 | ||
|
|
90a5c15ee9 | ||
|
|
bb9a03ec5b | ||
|
|
825d890921 | ||
|
|
05c364eff4 | ||
|
|
404d0ab40e | ||
|
|
bf88e376de | ||
|
|
1e74a5769d | ||
|
|
e1f7fae70e | ||
|
|
5127907acd | ||
|
|
2eda4d40e8 | ||
|
|
7f428a9976 | ||
|
|
4347a1b104 | ||
|
|
abd6e6f2ac | ||
|
|
1af5daffcf | ||
|
|
0e04425095 | ||
|
|
bbbdc05243 | ||
|
|
d4c39bd9cb | ||
|
|
11597d2448 | ||
|
|
5c3ea0a302 | ||
|
|
0c65176ecc | ||
|
|
e72bb5044b | ||
|
|
e8d04a2205 | ||
|
|
31328089cb | ||
|
|
bd41354009 | ||
|
|
bac3c575cb | ||
|
|
3c4865c4b0 | ||
|
|
ba2707e7b4 | ||
|
|
f9590055b0 | ||
|
|
dc97e5004e | ||
|
|
4fb629e893 | ||
|
|
44a34ffcfe | ||
|
|
f00207d53c | ||
|
|
d25e6cd07b | ||
|
|
bbec0d93d2 | ||
|
|
4158d3bd45 | ||
|
|
49e4f7bb46 | ||
|
|
4249b9f872 | ||
|
|
eac2e54953 | ||
|
|
2455a34987 | ||
|
|
ba0ce3366c | ||
|
|
ccf9f47305 | ||
|
|
95cf3fb6eb | ||
|
|
81dbebc5a4 | ||
|
|
26599fe24f | ||
|
|
ed1ea4d289 | ||
|
|
e0764da095 | ||
|
|
4c8456c4af | ||
|
|
ac2609b4f6 | ||
|
|
67e2189676 | ||
|
|
222615a116 | ||
|
|
8f623ebb91 | ||
|
|
adc4aa7906 | ||
|
|
ec362ea0ee | ||
|
|
e66e71a05c | ||
|
|
9be55eda07 | ||
|
|
16b5d44c3d | ||
|
|
93a639fea5 | ||
|
|
360f71c79a | ||
|
|
1f38ae5deb | ||
|
|
f633d57277 | ||
|
|
196a7de6a3 | ||
|
|
4c1d0ac60c | ||
|
|
1bdf7d9884 | ||
|
|
9e1d2c89bb | ||
|
|
a58dc09b80 | ||
|
|
91f85a1e1c | ||
|
|
cf9d630032 | ||
|
|
f55b0f528c | ||
|
|
463762f268 | ||
|
|
82770c204c | ||
|
|
0621cfb46c | ||
|
|
1f1a7eb12c | ||
|
|
93e2f66559 | ||
|
|
cc2b836cd5 | ||
|
|
fd2a227674 | ||
|
|
67937b9fbc | ||
|
|
ea58b4fa3d | ||
|
|
7087fb85f0 | ||
|
|
caf325f240 | ||
|
|
18b2fdb7e4 | ||
|
|
66eef96ccb | ||
|
|
757806341c | ||
|
|
69e3d2d0d8 | ||
|
|
a03ba7b260 | ||
|
|
51f81dce73 | ||
|
|
32d2ee698a | ||
|
|
8aaea960f0 | ||
|
|
ab9ee36263 | ||
|
|
f9cf348f8f | ||
|
|
254ac1a1cb | ||
|
|
2ffa029790 | ||
|
|
b0e27d0748 | ||
|
|
a4569675c2 | ||
|
|
88457eb101 | ||
|
|
103ef01be4 | ||
|
|
fcef6df7db | ||
|
|
19c24af592 | ||
|
|
7de119dd4f | ||
|
|
8d73bf2cc4 | ||
|
|
8131f3db39 | ||
|
|
e4e52ec4e6 | ||
|
|
43c9df23c4 | ||
|
|
85fb6dcc83 | ||
|
|
c9cdc74c91 | ||
|
|
c64e65ddb4 | ||
|
|
c46cf79501 | ||
|
|
eef0adc6f9 | ||
|
|
480ef63aed | ||
|
|
7a807e94af | ||
|
|
116bd1ae29 | ||
|
|
fb5c20d8da | ||
|
|
0bc0f503be | ||
|
|
70c0668962 | ||
|
|
f6d283dafb | ||
|
|
7048a1da50 | ||
|
|
36b541e0c7 | ||
|
|
501fa25e98 | ||
|
|
b2516bbae9 | ||
|
|
fcd56fdb3a | ||
|
|
8db90728aa | ||
|
|
35146cc794 | ||
|
|
bc395a09bd | ||
|
|
b6cd8d7b05 | ||
|
|
822edf1273 | ||
|
|
2f2919090e | ||
|
|
edf5b28e06 | ||
|
|
c828299793 | ||
|
|
cf13d24c93 | ||
|
|
56f93170e5 | ||
|
|
ed4839bc4d | ||
|
|
b8c39ca5b2 | ||
|
|
5397e4e22e | ||
|
|
ea7a2f3426 | ||
|
|
f2ae8afe3b | ||
|
|
ee235ce682 | ||
|
|
408a9725e0 | ||
|
|
1112fa96aa | ||
|
|
7da309c9cf | ||
|
|
caf4b5ca0b | ||
|
|
c8d6a46444 | ||
|
|
dd5ea51575 | ||
|
|
9d4c690760 | ||
|
|
d7a3cf94b7 | ||
|
|
a063107a15 | ||
|
|
f06fc8a9cb | ||
|
|
0c386811e0 | ||
|
|
4c34c6b8e3 | ||
|
|
4c43399cce | ||
|
|
b8ffb01073 | ||
|
|
50275548b8 | ||
|
|
64df3236ce | ||
|
|
c5a7293120 | ||
|
|
4a969cb77c | ||
|
|
8fffe3f662 | ||
|
|
f5722eb963 | ||
|
|
f5289cb3d3 | ||
|
|
0064a4c596 | ||
|
|
e9d9ef197f | ||
|
|
a5a6c875fa | ||
|
|
8e8521fd6d | ||
|
|
e387e04b6d | ||
|
|
001167ff23 | ||
|
|
449a3f541e | ||
|
|
3b59b11e8b | ||
|
|
aaae806505 | ||
|
|
458a7c7377 | ||
|
|
c95ab2d6b7 | ||
|
|
6a3aae7dbc | ||
|
|
9179e18b06 | ||
|
|
ecf530d512 | ||
|
|
51f7081214 | ||
|
|
7f7debbb73 | ||
|
|
60377c0c35 | ||
|
|
a594d741b1 | ||
|
|
3dd791fbbe | ||
|
|
87afb7f491 | ||
|
|
5f8519d737 | ||
|
|
f83166e576 | ||
|
|
df85c3969c | ||
|
|
e4311ddfa3 | ||
|
|
3a9fd925e1 | ||
|
|
dbcca9cbd2 | ||
|
|
ef250eb0f1 | ||
|
|
8c26af7d40 | ||
|
|
3cfe195561 | ||
|
|
e3cfa76843 | ||
|
|
a23bc875cf | ||
|
|
92fac53705 | ||
|
|
1b011acbd7 | ||
|
|
f651dd964d | ||
|
|
53b74e6f82 | ||
|
|
c1a5052cbb | ||
|
|
3b23b9eeef | ||
|
|
685365eb12 | ||
|
|
34a75dd28f | ||
|
|
a3be27723d | ||
|
|
62f2c8c01c | ||
|
|
c06408537a | ||
|
|
850b9afc66 | ||
|
|
909c704bda | ||
|
|
bcebfa58f1 | ||
|
|
8585620045 | ||
|
|
2238f4a807 | ||
|
|
653494e922 | ||
|
|
9e7d3146fa | ||
|
|
2ff56e8af8 | ||
|
|
b1e4f65be8 | ||
|
|
1ce829ba23 | ||
|
|
e92e36ceda | ||
|
|
b8b4377891 | ||
|
|
981486f09a | ||
|
|
7f8b2a6ccf | ||
|
|
a38d198b70 | ||
|
|
af2b2328cf | ||
|
|
94243f4060 | ||
|
|
63177b15a1 | ||
|
|
69ae42d344 | ||
|
|
0b3380ac6e | ||
|
|
af0061ae09 | ||
|
|
44b567b01f | ||
|
|
5a6cfad848 | ||
|
|
0693e4412f | ||
|
|
b52943a826 | ||
|
|
3a5e6dbe90 | ||
|
|
2aa2605b48 | ||
|
|
f97c97e00e | ||
|
|
dc22c7ea76 | ||
|
|
acc59d2b40 | ||
|
|
4d45f86e06 | ||
|
|
0dae538c31 | ||
|
|
7d90b9f87b | ||
|
|
28093256e7 | ||
|
|
d7d8554626 | ||
|
|
bfa673ca2c | ||
|
|
82f8fe96a1 | ||
|
|
c57153221c | ||
|
|
ab32518b8e | ||
|
|
24a9d7659a | ||
|
|
85fb56bb22 | ||
|
|
c1f14cea2e | ||
|
|
f2a06184b4 | ||
|
|
2ae9d272f4 | ||
|
|
1d90e37f71 | ||
|
|
c802c395a6 | ||
|
|
fa7425e114 | ||
|
|
5af094ab9b | ||
|
|
74ccbb3d07 | ||
|
|
d9c3badf8a | ||
|
|
dde59d4741 | ||
|
|
3e0f8a81b4 | ||
|
|
c9dbe7d203 | ||
|
|
aae0e1bb4b | ||
|
|
4855126d6d | ||
|
|
3b140b16eb | ||
|
|
9f23bd034c | ||
|
|
33d690c403 | ||
|
|
1dcbcc04d2 | ||
|
|
0fe6fd40be | ||
|
|
e25dce32e4 | ||
|
|
606f630cb6 | ||
|
|
c2857664d3 | ||
|
|
a300a4b25c | ||
|
|
fad545a094 | ||
|
|
a066285667 | ||
|
|
d13bf43de0 | ||
|
|
7325644689 | ||
|
|
4b7b5f1276 | ||
|
|
467b8d4184 | ||
|
|
410888b271 | ||
|
|
920241abab | ||
|
|
8ca827a816 | ||
|
|
b2dd2a2adb | ||
|
|
7166439b97 | ||
|
|
966936bba3 | ||
|
|
732ce790c2 | ||
|
|
276b6cbf46 | ||
|
|
c485f75206 | ||
|
|
883f7a975f | ||
|
|
aa64e42bfe | ||
|
|
aed914dfcf | ||
|
|
d763f43b40 | ||
|
|
2fcd6af24b | ||
|
|
4b76f56293 | ||
|
|
3ad26f0039 | ||
|
|
2366f26ac7 | ||
|
|
f2640deeaf | ||
|
|
8f798d1003 | ||
|
|
a39834dc87 | ||
|
|
00b7ace00b | ||
|
|
ee8148ae50 | ||
|
|
63654b742a | ||
|
|
f3edc2d43b | ||
|
|
0e9cc54245 | ||
|
|
3d4483ed48 | ||
|
|
ddbaca383f | ||
|
|
5b51e97594 | ||
|
|
d348e7a4ad | ||
|
|
0b014c5c7c | ||
|
|
bc7d9eef9b | ||
|
|
7dec46f3fd | ||
|
|
caad5066ff | ||
|
|
c86ce1b1ce | ||
|
|
aa1175ce1d | ||
|
|
afca104b87 | ||
|
|
c79157f415 | ||
|
|
5a38267fc1 | ||
|
|
0c9bea3c19 | ||
|
|
27d9b6384e | ||
|
|
bcd220191e | ||
|
|
1076e00aa0 | ||
|
|
9bca9187a6 | ||
|
|
bbeec18e0e | ||
|
|
3d11845396 | ||
|
|
34a6be67cf | ||
|
|
3c22129c4f | ||
|
|
c5be20b4d2 |
@@ -8,7 +8,8 @@ trim_trailing_whitespace = true
|
||||
max_line_length = 5000
|
||||
ij_kotlin_allow_trailing_comma=true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||
ktlint_function_naming_ignore_when_annotated_with=Composable, Test
|
||||
ktlint_function_naming_ignore_when_annotated_with=Composable
|
||||
ktlint_standard_no-unused-imports=enabled
|
||||
|
||||
[*.gradle]
|
||||
indent_size = 4
|
||||
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -61,13 +61,13 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Play Benchmark APK
|
||||
path: amethyst/build/outputs/apk/play/debug/amethyst-play-universal-benchmark.apk
|
||||
path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
|
||||
|
||||
- name: Upload FDroid APK Benchmark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FDroid Benchmark APK
|
||||
path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-benchmark.apk
|
||||
path: amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-universal-benchmark.apk
|
||||
|
||||
- name: Upload Compose Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -75,9 +75,6 @@ jobs:
|
||||
name: Compose Reports
|
||||
path: amethyst/build/compose_compiler
|
||||
|
||||
- name: Tests
|
||||
run: ./gradlew test --no-daemon
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
8
.github/workflows/create-release.yml
vendored
8
.github/workflows/create-release.yml
vendored
@@ -228,3 +228,11 @@ jobs:
|
||||
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
|
||||
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Publish Quartz Lib
|
||||
run: ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
|
||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
||||
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
|
||||
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,6 +17,8 @@
|
||||
/.idea/ChatHistory_schema_v2.xml
|
||||
/.idea/artifacts/*
|
||||
/.idea/kotlinNotebook.xml
|
||||
/.idea/ChatHistory_schema_v3.xml
|
||||
/.idea/markdown.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
6655
CHANGELOG.md
Normal file
6655
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
91
README.md
91
README.md
@@ -136,7 +136,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [ ] Key Migration (NIP-xx/Draft)
|
||||
- [ ] Time-based Sync (NIP-xx/Draft)
|
||||
- [x] Image Capture in the app
|
||||
- [ ] Video Capture in the app
|
||||
- [x] Video Capture in the app
|
||||
- [ ] Local Database
|
||||
- [ ] Workspaces
|
||||
- [ ] Infinity Scroll
|
||||
@@ -237,47 +237,46 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
|
||||
|
||||
## Using the Quartz library
|
||||
|
||||
Setup [JitPack.io](https://jitpack.io/#vitorpamplona/amethyst/v0.84.3) to your build file
|
||||
### Installing
|
||||
|
||||
Add `maven { url 'https://jitpack.io' }` to settings.gradle at the end of repositories:
|
||||
Add the following line to your `commonMain` dependencies:
|
||||
|
||||
```gradle
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
implementation('com.vitorpamplona.quartz:quartz:<Amethyst Version>')
|
||||
```
|
||||
|
||||
Add the dependency by using one of the versions [here](https://jitpack.io/#vitorpamplona/amethyst/)
|
||||
Variations to each platform are also available:
|
||||
|
||||
```gradle
|
||||
implementation('com.github.vitorpamplona.amethyst:quartz:<tag, commit, -SNAPSHOT>')
|
||||
implementation('com.vitorpamplona.quartz:quartz-android:<Amethyst Version>')
|
||||
implementation('com.vitorpamplona.quartz:quartz-jvm:<Amethyst Version>')
|
||||
implementation('com.vitorpamplona.quartz:quartz-iosarm64:<Amethyst Version>')
|
||||
implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:<Amethyst Version>')
|
||||
```
|
||||
|
||||
### How to use
|
||||
|
||||
Manage logged in users with the `KeyPair` class
|
||||
|
||||
```kt
|
||||
val keys = KeyPair() // creates a random key
|
||||
val keys = KeyPair("hex...".hexToByteArray())
|
||||
val keys = KeyPair("nsec1...".bechToBytes())
|
||||
val keys = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
|
||||
val keyPair = KeyPair() // creates a random key
|
||||
val keyPair = KeyPair("hex...".hexToByteArray())
|
||||
val keyPair = KeyPair("nsec1...".bechToBytes())
|
||||
val keyPair = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
|
||||
val readOnly = KeyPair(pubKey = "hex...".hexToByteArray())
|
||||
val readOnly = KeyPair(pubKey = "npub1...".bechToBytes())
|
||||
```
|
||||
|
||||
Create signers that can be internal, when you have the private key or when it is a read-only user
|
||||
or external, when it is controlled by Amber in NIP-55
|
||||
Create signers that can be Internal, when you have the private key or a read-only public key,
|
||||
or External, when it is controlled by Amber in NIP-55.
|
||||
|
||||
the `NostrSignerInternal` and `NostrSignerExternal` classes.
|
||||
Use either the `NostrSignerInternal` or `NostrSignerExternal` class:
|
||||
|
||||
```kt
|
||||
val signer = NostrSignerInternal(keyPair)
|
||||
val amberSigner = NostrSignerExternal(
|
||||
pubKey = keyPair.pubKey.toHexKey(),
|
||||
packageName = signerPackageName,
|
||||
packageName = signerPackageName, // Amber package name
|
||||
contentResolver = appContext.contentResolver,
|
||||
)
|
||||
```
|
||||
@@ -300,37 +299,63 @@ val client = NostrClient(socketBuilder, appScope)
|
||||
If you want to auth, given a logged-in `signer`:
|
||||
|
||||
```kt
|
||||
val authCoordinator = RelayAuthenticator(client, applicationIOScope) { challenge, relay ->
|
||||
val authedEvent = RelayAuthEvent.create(relayUrl, challenge, signer)
|
||||
val authCoordinator = RelayAuthenticator(client, appScope) { challenge, relay ->
|
||||
val authedEvent = RelayAuthEvent.create(relay.url, challenge, signer)
|
||||
client.sendIfExists(authedEvent, relay.url)
|
||||
}
|
||||
```
|
||||
|
||||
To manage subscriptions, the suggested approach is to use subscriptions in the Application class.
|
||||
To make a request subscription simply do:
|
||||
|
||||
```kt
|
||||
val metadataSub = RelayClientSubscription(
|
||||
client = client,
|
||||
filter = {
|
||||
val metadataSub = client.req(
|
||||
relays = listOf("wss://nos.lol", "wss://nostr.mom"),
|
||||
filters = Filter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOf(signer.pubkey)
|
||||
)
|
||||
) { event ->
|
||||
/* consume event */
|
||||
}
|
||||
```
|
||||
|
||||
The client will add the relay to the pool, connect to it and start receiving events. The
|
||||
`metadataSub` will be active until you call `metadataSub.close()`. If the client disconnects
|
||||
and reconnects, the sub will be active again.
|
||||
|
||||
To manage subscriptions that change over time, the simplest approach is to build mutable
|
||||
subscriptions with a filters lambda that you can change at will.
|
||||
|
||||
```kt
|
||||
val metadataSub = client.req(
|
||||
filters = {
|
||||
// Let's say you have a list of users that need to be rendered
|
||||
val users = pubkeysSeeingInTheScreen()
|
||||
// And a cache repository with their outbox relays
|
||||
val outboxRelays = outboxRelays(users)
|
||||
|
||||
val filters = listOf(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOf(signer.pubkey)
|
||||
authors = users
|
||||
)
|
||||
)
|
||||
|
||||
val signerOutboxRelays = listOfNotNull(
|
||||
RelayUrlNormalizer.normalizeOrNull("wss://relay1.com"),
|
||||
RelayUrlNormalizer.normalizeOrNull("wss://relay2.com")
|
||||
)
|
||||
|
||||
signerOutboxRelays.associateWith { filters }
|
||||
outboxRelays.associateWith { filters }
|
||||
}
|
||||
) { event ->
|
||||
/* consume event */
|
||||
}
|
||||
```
|
||||
|
||||
In that way, you can simply call `metadataSub.updateFilter()` when you need to update
|
||||
subscriptions to all relays. Or call `metadataSub.close()` to stop the sub
|
||||
without deleting it.
|
||||
|
||||
When your app goes to the background, you can use NostrClient's `connect` and `disconnect`
|
||||
methods to stop all communication to relays. Add the `connect` to your `onResume` and `disconnect`
|
||||
to `onPause` methods.
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues can be logged on: [https://gitworkshop.dev/repo/amethyst](https://gitworkshop.dev/repo/amethyst)
|
||||
|
||||
@@ -44,9 +44,9 @@ android {
|
||||
applicationId = "com.vitorpamplona.amethyst"
|
||||
minSdk = libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode = 426
|
||||
versionName = generateVersionName("1.02.0")
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\""
|
||||
versionCode = 429
|
||||
versionName = generateVersionName("1.04.0")
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"3a03c75d85aaf6b181d3b232d064c4d4feea5c73f0bea2bd91ed61b8da7cd6a6\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -152,6 +152,15 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this when lightcompressor uses one MP4 parser only
|
||||
packaging {
|
||||
resources {
|
||||
resources.pickFirsts.add('builddef.lst')
|
||||
resources.pickFirsts.add('META-INF/LICENSE.md')
|
||||
resources.pickFirsts.add('META-INF/LICENSE-notice.md')
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions = ["channel"]
|
||||
|
||||
productFlavors {
|
||||
@@ -304,6 +313,8 @@ dependencies {
|
||||
|
||||
// Language picker and Theme chooser
|
||||
implementation libs.androidx.appcompat
|
||||
|
||||
// Dynamically adjust between phone and tablet UI
|
||||
implementation libs.androidx.window.core.android
|
||||
|
||||
// Local model for language identification
|
||||
@@ -332,13 +343,16 @@ dependencies {
|
||||
implementation libs.audiowaveform
|
||||
|
||||
// Video compression lib
|
||||
implementation libs.abedElazizShe.image.compressor
|
||||
implementation libs.abedElazizShe.video.compressor.fork
|
||||
// Image compression lib
|
||||
implementation libs.zelory.video.compressor
|
||||
implementation libs.zelory.image.compressor
|
||||
|
||||
// Cbor for cashuB format
|
||||
implementation libs.kotlinx.serialization.cbor
|
||||
|
||||
// Kotlin serialization for the times where we need the Json tree and performance is not that important.
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation libs.tor.android
|
||||
implementation libs.jtorctl
|
||||
|
||||
@@ -350,6 +364,8 @@ dependencies {
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.junit.ktx
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
androidTestImplementation libs.androidx.ui.test.junit4
|
||||
androidTestImplementation libs.mockk.android
|
||||
|
||||
debugImplementation platform(libs.androidx.compose.bom)
|
||||
debugImplementation libs.androidx.ui.tooling
|
||||
|
||||
@@ -24,8 +24,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuParser
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@@ -38,7 +39,7 @@ class CashuBTest {
|
||||
@Test()
|
||||
fun parseCashuA() {
|
||||
runBlocking {
|
||||
val parsed = (CashuParser().parse(cashuTokenA) as GenericLoadable.Loaded<List<CashuToken>>).loaded[0]
|
||||
val parsed = (CashuParser().parse(cashuTokenA) as GenericLoadable.Loaded<ImmutableList<CashuToken>>).loaded[0]
|
||||
|
||||
assertEquals(cashuTokenA, parsed.token)
|
||||
assertEquals("https://8333.space:3338", parsed.mint)
|
||||
@@ -59,7 +60,7 @@ class CashuBTest {
|
||||
@Test()
|
||||
fun parseCashuB() =
|
||||
runBlocking {
|
||||
val parsed = (CashuParser().parse(cashuTokenB1) as GenericLoadable.Loaded<List<CashuToken>>).loaded
|
||||
val parsed = (CashuParser().parse(cashuTokenB1) as GenericLoadable.Loaded<ImmutableList<CashuToken>>).loaded
|
||||
|
||||
assertEquals(cashuTokenB1, parsed[0].token)
|
||||
assertEquals("http://localhost:3338", parsed[0].mint)
|
||||
@@ -84,7 +85,7 @@ class CashuBTest {
|
||||
@Test()
|
||||
fun parseCashuB2() =
|
||||
runBlocking {
|
||||
val parsed = (CashuParser().parse(cashuTokenB2) as GenericLoadable.Loaded<List<CashuToken>>).loaded
|
||||
val parsed = (CashuParser().parse(cashuTokenB2) as GenericLoadable.Loaded<ImmutableList<CashuToken>>).loaded
|
||||
|
||||
assertEquals(cashuTokenB2, parsed[0].token)
|
||||
assertEquals("http://lbutlh5lfggq5r7xpiwhrajdl7sxpupgagazxl65w4c5cg72wtofasad.onion:3338", parsed[0].mint)
|
||||
|
||||
@@ -24,10 +24,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.service.okhttp.EncryptedBlobInterceptor
|
||||
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import com.vitorpamplona.quartz.utils.ciphers.AESGCM
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.fail
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -49,9 +48,9 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.Request.method
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.random.Random
|
||||
@@ -248,13 +247,6 @@ class ImageUploadTesting {
|
||||
testBase(ServerName("sovbit", "https://files.sovbit.host", ServerType.NIP96))
|
||||
}
|
||||
|
||||
@Test()
|
||||
@Ignore("Not Working anymore")
|
||||
fun testVoidCat() =
|
||||
runBlocking {
|
||||
testBase(ServerName("void.cat", "https://void.cat", ServerType.NIP96))
|
||||
}
|
||||
|
||||
@Test()
|
||||
@Ignore("Returns invalid image size")
|
||||
fun testNostrPic() =
|
||||
|
||||
@@ -21,17 +21,15 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpBitcoinExplorer
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpCalendarBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpOtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@@ -45,51 +43,50 @@ class OkHttpOtsTest {
|
||||
val otsCache = OtsBlockHeightCache()
|
||||
|
||||
val resolver =
|
||||
OtsResolver(
|
||||
OkHttpBitcoinExplorer(
|
||||
baseAPI = OkHttpBitcoinExplorer.MEMPOOL_API_URL,
|
||||
client = OkHttpClient.Builder().build(),
|
||||
cache = otsCache,
|
||||
),
|
||||
OkHttpCalendarBuilder { OkHttpClient.Builder().build() },
|
||||
)
|
||||
OkHttpOtsResolverBuilder(
|
||||
otsCache,
|
||||
) { OkHttpClient.Builder().build() }.build()
|
||||
|
||||
@Test
|
||||
fun verifyNostrEvent() {
|
||||
val ots = JsonMapper.fromJson(otsEvent) as OtsEvent
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
assertEquals(1707688818L, ots.verify(resolver))
|
||||
}
|
||||
fun verifyNostrEvent() =
|
||||
runBlocking {
|
||||
val ots = OptimizedJsonMapper.fromJson(otsEvent) as OtsEvent
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
assertEquals(1707688818L, ots.verify(resolver))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyNostrEvent2() {
|
||||
val ots = JsonMapper.fromJson(otsEvent2) as OtsEvent
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
assertEquals(1706322179L, ots.verify(resolver))
|
||||
}
|
||||
fun verifyNostrEvent2() =
|
||||
runBlocking {
|
||||
val ots = OptimizedJsonMapper.fromJson(otsEvent2) as OtsEvent
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
assertEquals(1706322179L, ots.verify(resolver))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyNostrPendingEvent() {
|
||||
val ots = JsonMapper.fromJson(otsPendingEvent) as OtsEvent
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
assertEquals(null, ots.verify(resolver))
|
||||
}
|
||||
fun verifyNostrPendingEvent() =
|
||||
runBlocking {
|
||||
val ots = OptimizedJsonMapper.fromJson(otsPendingEvent) as OtsEvent
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
assertEquals(null, ots.verify(resolver))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createOTSEventAndVerify() {
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
fun createOTSEventAndVerify() =
|
||||
runBlocking {
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
|
||||
val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver)
|
||||
val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver)
|
||||
|
||||
val ots =
|
||||
runBlocking {
|
||||
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile))
|
||||
}
|
||||
val ots =
|
||||
runBlocking {
|
||||
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile))
|
||||
}
|
||||
|
||||
println(ots.toJson())
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
println(ots.toJson())
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
|
||||
// Should not be valid because we need to wait for confirmations
|
||||
assertEquals(null, ots.verify(resolver))
|
||||
}
|
||||
// Should not be valid because we need to wait for confirmations
|
||||
assertEquals(null, ots.verify(resolver))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,22 +27,24 @@ import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.verify
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.verify
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@@ -50,24 +52,30 @@ import org.junit.runner.RunWith
|
||||
class ThreadDualAxisChartAssemblerTest {
|
||||
companion object {
|
||||
val keyPair = KeyPair()
|
||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
val client =
|
||||
NostrClient(
|
||||
OkHttpWebSocket.Builder {
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.build()
|
||||
},
|
||||
scope,
|
||||
)
|
||||
|
||||
val account =
|
||||
Account(
|
||||
settings = AccountSettings(keyPair = keyPair),
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
|
||||
nwcFilterAssembler = NWCPaymentFilterAssembler(client),
|
||||
otsResolverBuilder = EmptyOtsResolverBuilder,
|
||||
cache = LocalCache,
|
||||
client =
|
||||
NostrClient(
|
||||
OkHttpWebSocket.Builder {
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.build()
|
||||
},
|
||||
CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
),
|
||||
client = client,
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
val db =
|
||||
@@ -149,7 +157,7 @@ class ThreadDualAxisChartAssemblerTest {
|
||||
fun threadOrderTest() =
|
||||
runBlocking {
|
||||
val eventArray =
|
||||
JsonMapper.mapper.readValue<ArrayList<Event>>(db) as List<Event> + Event.fromJson(header)
|
||||
JacksonMapper.mapper.readValue<List<Event>>(db) + Event.fromJson(header)
|
||||
|
||||
var counter = 0
|
||||
eventArray.forEach {
|
||||
@@ -169,7 +177,7 @@ class ThreadDualAxisChartAssemblerTest {
|
||||
val filter = ThreadFeedFilter(account, naddr.toTag())
|
||||
val calculatedFeed = filter.feed()
|
||||
|
||||
val expecteedOrder =
|
||||
val expectedOrder =
|
||||
listOf(
|
||||
"30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599",
|
||||
"e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e",
|
||||
@@ -211,8 +219,8 @@ class ThreadDualAxisChartAssemblerTest {
|
||||
"7a18dda355525d468b31bba4fa947cba98cc19048d4a3099d5e9ba045d878c26",
|
||||
)
|
||||
|
||||
for (i in expecteedOrder.indices) {
|
||||
assertEquals(expecteedOrder[i], calculatedFeed[i].idHex)
|
||||
for (i in expectedOrder.indices) {
|
||||
assertEquals(expectedOrder[i], calculatedFeed[i].idHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.navigation.findParameterValue
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.net.URI
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.notifications
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
|
||||
interface PushDistributorActions {
|
||||
@@ -53,7 +53,7 @@ object PushDistributorHandler : PushDistributorActions {
|
||||
endpointInternal = ""
|
||||
}
|
||||
|
||||
fun appContext(): Context = Amethyst.instance.applicationContext
|
||||
fun appContext(): Context = Amethyst.instance.appContext
|
||||
|
||||
override fun getSavedDistributor(): String = unifiedPush.getSavedDistributor(appContext()) ?: ""
|
||||
|
||||
|
||||
@@ -22,15 +22,13 @@ package com.vitorpamplona.amethyst.service.notifications
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -44,7 +42,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
private val TAG = "Amethyst-OSSPushReceiver"
|
||||
}
|
||||
|
||||
private val appContext = Amethyst.instance.applicationContext
|
||||
private val appContext = Amethyst.instance.appContext
|
||||
private val scope = Amethyst.instance.applicationIOScope
|
||||
private val eventCache = LruCache<String, String>(100)
|
||||
private val pushHandler = PushDistributorHandler
|
||||
@@ -95,8 +93,8 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) {
|
||||
Amethyst.instance.okHttpClients.getHttpClient(Amethyst.instance.torManager.isSocksReady())
|
||||
}
|
||||
notificationManager().getOrCreateZapChannel(appContext)
|
||||
notificationManager().getOrCreateDMChannel(appContext)
|
||||
NotificationUtils.getOrCreateZapChannel(appContext)
|
||||
NotificationUtils.getOrCreateDMChannel(appContext)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint")
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.util.Log
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -36,6 +37,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -44,33 +46,52 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
|
||||
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.checkifItNeedsToRequestNotificationPermission
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
||||
val notificationPermissionState =
|
||||
checkifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel)
|
||||
fun SelectNotificationProvider(sharedPrefs: UiSettingsFlow) {
|
||||
val notificationPermissionGranted =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
if (notificationPermissionState.status.isGranted) {
|
||||
if (!sharedPreferencesViewModel.sharedPrefs.dontShowPushNotificationSelector) {
|
||||
if (!notificationPermissionState.status.isGranted) {
|
||||
val dontAskForNotificationPermissions by sharedPrefs.dontAskForNotificationPermissions.collectAsStateWithLifecycle()
|
||||
if (!dontAskForNotificationPermissions) {
|
||||
sharedPrefs.dontAskForNotificationPermissions()
|
||||
|
||||
// This will pause the APP, including the connection with relays.
|
||||
LaunchedEffect(notificationPermissionState) {
|
||||
notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notificationPermissionState.status.isGranted
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
if (notificationPermissionGranted) {
|
||||
val pushNotificationSelector by sharedPrefs.dontShowPushNotificationSelector.collectAsStateWithLifecycle()
|
||||
if (!pushNotificationSelector) {
|
||||
val context = LocalContext.current
|
||||
var distributorPresent by remember {
|
||||
mutableStateOf(PushDistributorHandler.savedDistributorExists())
|
||||
@@ -84,8 +105,8 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
||||
onSelect = { index ->
|
||||
if (list[index] == "None") {
|
||||
PushDistributorHandler.forceRemoveDistributor(context)
|
||||
sharedPreferencesViewModel.dontAskForNotificationPermissions()
|
||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
||||
sharedPrefs.dontAskForNotificationPermissions()
|
||||
sharedPrefs.dontShowPushNotificationSelector()
|
||||
} else {
|
||||
val fullDistributorName = list[index]
|
||||
PushDistributorHandler.saveDistributor(fullDistributorName)
|
||||
@@ -107,7 +128,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
}
|
||||
|
||||
RichText(
|
||||
@@ -125,7 +146,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
||||
TextButton(
|
||||
onClick = {
|
||||
distributorPresent = true
|
||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
||||
sharedPrefs.dontShowPushNotificationSelector()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.quick_action_dont_show_again_button))
|
||||
@@ -187,19 +208,19 @@ fun LoadDistributors(onInner: @Composable (String, ImmutableList<String>, Immuta
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
||||
fun PushNotificationSettingsRow(sharedPrefs: UiSettingsFlow) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
|
||||
SettingsRow(
|
||||
R.string.push_server_title,
|
||||
R.string.push_server_explainer,
|
||||
selectedItens = readableListWithExplainer,
|
||||
selectedItems = readableListWithExplainer,
|
||||
selectedIndex = list.indexOf(currentDistributor),
|
||||
) { index ->
|
||||
if (list[index] == "None") {
|
||||
sharedPreferencesViewModel.dontAskForNotificationPermissions()
|
||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
||||
sharedPrefs.dontAskForNotificationPermissions()
|
||||
sharedPrefs.dontShowPushNotificationSelector()
|
||||
PushDistributorHandler.forceRemoveDistributor(context)
|
||||
} else {
|
||||
PushDistributorHandler.saveDistributor(list[index])
|
||||
|
||||
@@ -28,7 +28,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
|
||||
@Composable
|
||||
fun TranslatableRichTextViewer(
|
||||
@@ -59,6 +60,7 @@ fun TranslatableRichTextViewer(
|
||||
fun TranslatableRichTextViewer(
|
||||
content: String,
|
||||
id: String,
|
||||
translationMessageModifier: Modifier = MaxWidthPaddingTop5dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
displayText: @Composable (String) -> Unit,
|
||||
) = displayText(content)
|
||||
|
||||
@@ -8,8 +8,12 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="nostrsigner" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
|
||||
<!-- Doesn't require a camera -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
|
||||
@@ -85,6 +89,7 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="nostr" />
|
||||
<data android:scheme="amethyst" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Amethyst">
|
||||
@@ -99,6 +104,7 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="nostrwalletconnect" />
|
||||
<data android:scheme="nostr+walletconnect" />
|
||||
<data android:scheme="amethyst+walletconnect" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="New Post">
|
||||
|
||||
@@ -21,198 +21,37 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.util.Log
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.memory.MemoryCache
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
||||
import com.vitorpamplona.amethyst.service.crashreports.CrashReportCache
|
||||
import com.vitorpamplona.amethyst.service.crashreports.UnexpectedCrashSaver
|
||||
import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService
|
||||
import com.vitorpamplona.amethyst.service.images.ImageCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.logging.Logging
|
||||
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
|
||||
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
|
||||
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
|
||||
import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
class Amethyst : Application() {
|
||||
val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}"
|
||||
init {
|
||||
Log.d("AmethystApp", "Creating App $this")
|
||||
}
|
||||
|
||||
// Exists to avoid exceptions stopping the coroutine
|
||||
val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable)
|
||||
}
|
||||
|
||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
|
||||
|
||||
// Key cache service to download and decrypt encrypted files before caching them.
|
||||
val keyCache = EncryptionKeyCache()
|
||||
|
||||
// App services that should be run as soon as there are subscribers to their flows
|
||||
val locationManager = LocationState(this, applicationIOScope)
|
||||
val connManager = ConnectivityManager(this, applicationIOScope)
|
||||
|
||||
val torManager = TorManager(this, applicationIOScope)
|
||||
|
||||
// Service that will run at all times to receive events from Pokey
|
||||
val pokeyReceiver = PokeyReceiver()
|
||||
|
||||
// manages all the other connections separately from relays.
|
||||
val okHttpClients =
|
||||
DualHttpClientManager(
|
||||
userAgent = appAgent,
|
||||
proxyPortProvider = torManager.activePortOrNull,
|
||||
isMobileDataProvider = connManager.isMobileOrNull,
|
||||
keyCache = keyCache,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// manages all relay connections
|
||||
val okHttpClientForRelays =
|
||||
DualHttpClientManager(
|
||||
userAgent = appAgent,
|
||||
proxyPortProvider = torManager.activePortOrNull,
|
||||
isMobileDataProvider = connManager.isMobileOrNull,
|
||||
keyCache = keyCache,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// manages all relay connections
|
||||
val okHttpClientForRelaysForDms =
|
||||
DualHttpClientManager(
|
||||
userAgent = appAgent,
|
||||
proxyPortProvider = torManager.activePortOrNull,
|
||||
isMobileDataProvider = connManager.isMobileOrNull,
|
||||
keyCache = keyCache,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
val torProxySettingsAnchor = ProxySettingsAnchor()
|
||||
|
||||
// Connects the NostrClient class with okHttp
|
||||
val websocketBuilder =
|
||||
OkHttpWebSocket.Builder { url ->
|
||||
val useTor = torProxySettingsAnchor.useProxy(url)
|
||||
if (torProxySettingsAnchor.isDM(url)) {
|
||||
okHttpClientForRelaysForDms.getHttpClient(useTor)
|
||||
} else {
|
||||
okHttpClientForRelays.getHttpClient(useTor)
|
||||
}
|
||||
}
|
||||
|
||||
// Caches all events in Memory
|
||||
val cache: LocalCache = LocalCache
|
||||
|
||||
// Organizes cache clearing
|
||||
val trimmingService = MemoryTrimmingService(cache)
|
||||
|
||||
// Provides a relay pool
|
||||
val client: NostrClient = NostrClient(websocketBuilder, applicationIOScope)
|
||||
|
||||
// Watches for changes on Tor and Relay List Settings
|
||||
val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, torManager, applicationIOScope)
|
||||
|
||||
// Verifies and inserts in the cache from all relays, all subscriptions
|
||||
val cacheClientConnector = CacheClientConnector(client, cache)
|
||||
|
||||
// Show messages from the Relay and controls their dismissal
|
||||
val notifyCoordinator = NotifyCoordinator(client)
|
||||
|
||||
// Authenticates with relays.
|
||||
val authCoordinator = AuthCoordinator(client, applicationIOScope)
|
||||
|
||||
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||
|
||||
// Coordinates all subscriptions for the Nostr Client
|
||||
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope)
|
||||
|
||||
// saves the .content of NIP-95 blobs in disk to save memory
|
||||
val nip95cache: File by lazy { Nip95CacheFactory.new(this) }
|
||||
|
||||
// local video cache with disk + memory
|
||||
val videoCache: VideoCache by lazy { VideoCacheFactory.new(this) }
|
||||
|
||||
// image cache in disk for coil
|
||||
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(this) }
|
||||
|
||||
// image cache in memory for coil
|
||||
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(this) }
|
||||
|
||||
val crashReportCache: CrashReportCache by lazy { CrashReportCache(this) }
|
||||
|
||||
// Application-wide ots verification cache
|
||||
val otsVerifCache by lazy { VerificationStateCache() }
|
||||
|
||||
// Application-wide block height request cache
|
||||
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
|
||||
companion object {
|
||||
lateinit var instance: AppModules
|
||||
private set
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d("AmethystApp", "onCreate $this")
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
|
||||
|
||||
instance = this
|
||||
instance = AppModules(this)
|
||||
|
||||
if (isDebug) {
|
||||
Logging.setup()
|
||||
}
|
||||
|
||||
// initializes diskcache on an IO thread.
|
||||
applicationIOScope.launch {
|
||||
diskCache
|
||||
videoCache
|
||||
}
|
||||
|
||||
// registers to receive events
|
||||
pokeyReceiver.register(this)
|
||||
instance.initiate(this)
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
Log.d("AmethystApp", "onTerminate $this")
|
||||
|
||||
pokeyReceiver.unregister(this)
|
||||
applicationIOScope.cancel("Application onTerminate $this")
|
||||
instance.terminate(this)
|
||||
}
|
||||
|
||||
fun contentResolverFn(): ContentResolver = contentResolver
|
||||
|
||||
fun setImageLoader(shouldUseTor: (String) -> Boolean?) {
|
||||
ImageLoaderSetup.setup(this, diskCache, memoryCache) { url ->
|
||||
shouldUseTor(url)?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
|
||||
|
||||
/**
|
||||
* Release memory when the UI becomes hidden or when system resources become low.
|
||||
*
|
||||
@@ -221,13 +60,6 @@ class Amethyst : Application() {
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
Log.d("AmethystApp", "onTrimMemory $level")
|
||||
applicationIOScope.launch(Dispatchers.Default) {
|
||||
trimmingService.run(null, LocalPreferences.allSavedAccounts())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: Amethyst
|
||||
private set
|
||||
instance.trim()
|
||||
}
|
||||
}
|
||||
|
||||
305
amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt
Normal file
305
amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
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.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
|
||||
import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier
|
||||
import com.vitorpamplona.amethyst.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
|
||||
import com.vitorpamplona.amethyst.model.preferences.TorSharedPreferences
|
||||
import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences
|
||||
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.model.torState.AccountsTorStateConnector
|
||||
import com.vitorpamplona.amethyst.model.torState.TorRelayState
|
||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
||||
import com.vitorpamplona.amethyst.service.crashreports.CrashReportCache
|
||||
import com.vitorpamplona.amethyst.service.crashreports.UnexpectedCrashSaver
|
||||
import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService
|
||||
import com.vitorpamplona.amethyst.service.images.ImageCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
|
||||
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
|
||||
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.stats.RelayReqStats
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class AppModules(
|
||||
val appContext: Context,
|
||||
) {
|
||||
val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}"
|
||||
|
||||
// Exists to avoid exceptions stopping the coroutine
|
||||
val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable)
|
||||
}
|
||||
|
||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
|
||||
val applicationDefaultScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
|
||||
|
||||
// Blocking load of UI Preferences to avoid theme/language blinking
|
||||
val uiPrefs by lazy {
|
||||
UiSharedPreferences(appContext, applicationIOScope)
|
||||
}
|
||||
|
||||
// Blocking load of Tor Settings to avoid connection leaks
|
||||
val torPrefs by lazy {
|
||||
TorSharedPreferences(appContext, applicationIOScope)
|
||||
}
|
||||
|
||||
// App services that should be run as soon as there are subscribers to their flows
|
||||
val locationManager = LocationState(appContext, applicationIOScope)
|
||||
val connManager = ConnectivityManager(appContext, applicationIOScope)
|
||||
|
||||
val uiState by lazy {
|
||||
UiSettingsState(uiPrefs.value, connManager.isMobileOrFalse, applicationIOScope)
|
||||
}
|
||||
|
||||
val torManager = TorManager(torPrefs, appContext, applicationIOScope)
|
||||
|
||||
// Service that will run at all times to receive events from Pokey
|
||||
val pokeyReceiver = PokeyReceiver()
|
||||
|
||||
// Key cache service to download and decrypt encrypted files before caching them.
|
||||
val keyCache = EncryptionKeyCache()
|
||||
|
||||
// manages all the other connections separately from relays.
|
||||
val okHttpClients =
|
||||
DualHttpClientManager(
|
||||
userAgent = appAgent,
|
||||
proxyPortProvider = torManager.activePortOrNull,
|
||||
isMobileDataProvider = connManager.isMobileOrNull,
|
||||
keyCache = keyCache,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// manages all relay connections
|
||||
val okHttpClientForRelays =
|
||||
DualHttpClientManager(
|
||||
userAgent = appAgent,
|
||||
proxyPortProvider = torManager.activePortOrNull,
|
||||
isMobileDataProvider = connManager.isMobileOrNull,
|
||||
keyCache = keyCache,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// Offers easy methods to know when connections are happening through Tor or not
|
||||
val roleBasedHttpClientBuilder = RoleBasedHttpClientBuilder(okHttpClients, torPrefs.value)
|
||||
|
||||
// Application-wide block height request cache
|
||||
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
|
||||
|
||||
val otsResolverBuilder: TorAwareOkHttpOtsResolverBuilder =
|
||||
TorAwareOkHttpOtsResolverBuilder(
|
||||
roleBasedHttpClientBuilder::okHttpClientForMoney,
|
||||
roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
|
||||
otsBlockHeightCache,
|
||||
)
|
||||
|
||||
// Application-wide ots verification cache
|
||||
val otsVerifCache by lazy { VerificationStateCache(otsResolverBuilder) }
|
||||
|
||||
val torEvaluatorFlow =
|
||||
TorRelayState(
|
||||
okHttpClients,
|
||||
torPrefs.value,
|
||||
applicationDefaultScope,
|
||||
)
|
||||
|
||||
// Connects the NostrClient class with okHttp
|
||||
val websocketBuilder =
|
||||
OkHttpWebSocket.Builder { url ->
|
||||
val useTor = torEvaluatorFlow.flow.value.useTor(url)
|
||||
okHttpClientForRelays.getHttpClient(useTor)
|
||||
}
|
||||
|
||||
// Caches all events in Memory
|
||||
val cache: LocalCache = LocalCache
|
||||
|
||||
// Provides a relay pool
|
||||
val client: INostrClient = NostrClient(websocketBuilder, applicationDefaultScope)
|
||||
|
||||
// Watches for changes on Tor and Relay List Settings
|
||||
val relayProxyClientConnector =
|
||||
RelayProxyClientConnector(
|
||||
torEvaluatorFlow.flow,
|
||||
okHttpClientForRelays,
|
||||
connManager,
|
||||
torManager,
|
||||
client,
|
||||
applicationDefaultScope,
|
||||
)
|
||||
|
||||
// Verifies and inserts in the cache from all relays, all subscriptions
|
||||
val cacheClientConnector = CacheClientConnector(client, cache)
|
||||
|
||||
// Show messages from the Relay and controls their dismissal
|
||||
val notifyCoordinator = NotifyCoordinator(client)
|
||||
|
||||
// Authenticates with relays.
|
||||
val authCoordinator = AuthCoordinator(client, applicationDefaultScope)
|
||||
|
||||
// Tries to verify new OTS events when they arrive.
|
||||
val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationDefaultScope)
|
||||
|
||||
// Tracks if it is possible to connect to relays.
|
||||
val failureTracker = RelayOfflineTracker(client)
|
||||
|
||||
// Captures statistics about relays
|
||||
val relayStats = RelayStats(client)
|
||||
|
||||
// Logs debug messages when needed
|
||||
val detailedLogger = if (isDebug) RelayLogger(client, true, false) else null
|
||||
val relayReqStats = if (isDebug) RelayReqStats(client) else null
|
||||
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||
|
||||
// Coordinates all subscriptions for the Nostr Client
|
||||
val sources: RelaySubscriptionsCoordinator =
|
||||
RelaySubscriptionsCoordinator(
|
||||
LocalCache,
|
||||
client,
|
||||
authCoordinator.receiver,
|
||||
failureTracker,
|
||||
applicationDefaultScope,
|
||||
)
|
||||
|
||||
// keeps all accounts live
|
||||
val accountsCache =
|
||||
AccountCacheState(
|
||||
geolocationFlow = locationManager.geohashStateFlow,
|
||||
nwcFilterAssembler = sources.nwc,
|
||||
contentResolverFn = ::contentResolverFn,
|
||||
otsResolverBuilder = otsResolverBuilder,
|
||||
cache = cache,
|
||||
client = client,
|
||||
)
|
||||
|
||||
// Organizes cache clearing
|
||||
val trimmingService = 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, applicationDefaultScope)
|
||||
|
||||
// saves the .content of NIP-95 blobs in disk to save memory
|
||||
val nip95cache: File by lazy { Nip95CacheFactory.new(appContext) }
|
||||
|
||||
// local video cache with disk + memory
|
||||
val videoCache: VideoCache by lazy { VideoCacheFactory.new(appContext) }
|
||||
|
||||
// image cache in disk for coil
|
||||
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(appContext) }
|
||||
|
||||
// image cache in memory for coil
|
||||
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(appContext) }
|
||||
|
||||
// crash report storage
|
||||
val crashReportCache: CrashReportCache by lazy { CrashReportCache(appContext) }
|
||||
|
||||
// cache for NIP-11 documents
|
||||
val nip11Cache: Nip11CachedRetriever by lazy {
|
||||
Nip11CachedRetriever(torEvaluatorFlow::okHttpClientForRelay)
|
||||
}
|
||||
|
||||
fun contentResolverFn(): ContentResolver = appContext.contentResolver
|
||||
|
||||
fun setImageLoader() {
|
||||
ImageLoaderSetup.setup(appContext, { diskCache }, { memoryCache }) { url ->
|
||||
okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(url))
|
||||
}
|
||||
}
|
||||
|
||||
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(appContext, npub)
|
||||
|
||||
fun initiate(appContext: Context) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
|
||||
|
||||
applicationIOScope.launch {
|
||||
// loads main account quickly.
|
||||
LocalPreferences.loadAccountConfigFromEncryptedStorage()
|
||||
}
|
||||
|
||||
// forces initialization of uiPrefs in the main thread to avoid blinking themes
|
||||
uiPrefs
|
||||
|
||||
// initializes diskcache on an IO thread.
|
||||
applicationIOScope.launch {
|
||||
// preloads tor preferences
|
||||
torPrefs
|
||||
}
|
||||
|
||||
// initializes diskcache on an IO thread.
|
||||
applicationIOScope.launch {
|
||||
// Sets Coil - Tor - OkHttp link
|
||||
setImageLoader()
|
||||
}
|
||||
|
||||
// registers to receive events
|
||||
pokeyReceiver.register(appContext)
|
||||
}
|
||||
|
||||
fun terminate(appContext: Context) {
|
||||
pokeyReceiver.unregister(appContext)
|
||||
applicationIOScope.cancel("Application onTerminate $appContext")
|
||||
applicationDefaultScope.cancel("Application onTerminate $appContext")
|
||||
accountsCache.clear()
|
||||
}
|
||||
|
||||
fun trim() {
|
||||
applicationDefaultScope.launch {
|
||||
val loggedIn = accountsCache.accounts.value.values
|
||||
trimmingService.run(loggedIn, LocalPreferences.allSavedAccounts())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,43 +24,42 @@ import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Debug
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizedUrls
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
|
||||
|
||||
fun debugState(context: Context) {
|
||||
// Amethyst.instance.client
|
||||
// .allSubscriptions()
|
||||
// .forEach { Log.d("STATE DUMP", "${it.key} ${it.value.filters.joinToString { it.filter.toJson() }}") }
|
||||
private const val STATE_DUMP_TAG = "STATE DUMP"
|
||||
|
||||
fun debugState(context: Context) {
|
||||
val totalMemoryMb = Runtime.getRuntime().totalMemory() / (1024 * 1024)
|
||||
val freeMemoryMb = Runtime.getRuntime().freeMemory() / (1024 * 1024)
|
||||
val maxMemoryMb = Runtime.getRuntime().maxMemory() / (1024 * 1024)
|
||||
|
||||
val jvmHeapAllocatedMb = totalMemoryMb - freeMemoryMb
|
||||
|
||||
Log.d("STATE DUMP", "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", "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", "Memory Class $memClass MB (largeHeap $isLargeHeap)")
|
||||
Log.d(STATE_DUMP_TAG, "Memory Class $memClass MB (largeHeap $isLargeHeap)")
|
||||
}
|
||||
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
STATE_DUMP_TAG,
|
||||
"Connected Relays: " +
|
||||
Amethyst.instance.client
|
||||
.relayStatusFlow()
|
||||
@@ -71,16 +70,23 @@ fun debugState(context: Context) {
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
STATE_DUMP_TAG,
|
||||
"Indexed Relays: " +
|
||||
Amethyst.instance.cache.relayHints.relayDB
|
||||
.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",
|
||||
STATE_DUMP_TAG,
|
||||
"Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB",
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
STATE_DUMP_TAG,
|
||||
"Notes: " +
|
||||
LocalCache.notes.filter { _, it -> it.flowSet != null }.size +
|
||||
" / " +
|
||||
@@ -89,7 +95,7 @@ fun debugState(context: Context) {
|
||||
LocalCache.notes.size(),
|
||||
)
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
STATE_DUMP_TAG,
|
||||
"Addressables: " +
|
||||
LocalCache.addressables.filter { _, it -> it.flowSet != null }.size +
|
||||
" / " +
|
||||
@@ -98,7 +104,7 @@ fun debugState(context: Context) {
|
||||
LocalCache.addressables.size(),
|
||||
)
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
STATE_DUMP_TAG,
|
||||
"Users: " +
|
||||
LocalCache.users.filter { _, it -> it.flowSet != null }.size +
|
||||
" / " +
|
||||
@@ -107,12 +113,48 @@ fun debugState(context: Context) {
|
||||
LocalCache.users.size(),
|
||||
)
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
STATE_DUMP_TAG,
|
||||
"Public Chat Channels: " +
|
||||
LocalCache.publicChatChannels.filter { _, it -> it.flowSet != null }.size +
|
||||
" / " +
|
||||
LocalCache.publicChatChannels.size() +
|
||||
" / " +
|
||||
LocalCache.publicChatChannels.values().sumOf { it.notes.size() },
|
||||
)
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Live Chat Channels: " +
|
||||
LocalCache.liveChatChannels.filter { _, it -> it.flowSet != null }.size +
|
||||
" / " +
|
||||
LocalCache.liveChatChannels.size() +
|
||||
" / " +
|
||||
LocalCache.liveChatChannels.values().sumOf { it.notes.size() },
|
||||
)
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Ephemeral Chat Channels: " +
|
||||
LocalCache.ephemeralChannels.filter { _, it -> it.flowSet != null }.size +
|
||||
" / " +
|
||||
LocalCache.ephemeralChannels.size() +
|
||||
" / " +
|
||||
LocalCache.ephemeralChannels.values().sumOf { it.notes.size() },
|
||||
)
|
||||
LocalCache.chatroomList.forEach { key, room ->
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Private Chats $key: " +
|
||||
room.rooms.size() +
|
||||
" / " +
|
||||
room.rooms.sumOf { key, value -> value.messages.size },
|
||||
)
|
||||
}
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Deletion Events: " +
|
||||
LocalCache.deletionIndex.size(),
|
||||
)
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
STATE_DUMP_TAG,
|
||||
"Observable Events: " +
|
||||
LocalCache.observablesByKindAndETag.size +
|
||||
" / " +
|
||||
@@ -120,15 +162,15 @@ fun debugState(context: Context) {
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
STATE_DUMP_TAG,
|
||||
"Spam: " +
|
||||
LocalCache.antiSpam.spamMessages.size() + " / " + LocalCache.antiSpam.recentEventIds.size() + " / " + LocalCache.antiSpam.recentAddressables.size(),
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
STATE_DUMP_TAG,
|
||||
"Memory used by Events: " +
|
||||
LocalCache.notes.sumOfLong { _, note -> note.event?.countMemory() ?: 0L } / (1024 * 1024) +
|
||||
LocalCache.notes.sumOf { _, note -> note.event?.countMemory() ?: 0 } / (1024 * 1024) +
|
||||
" MB",
|
||||
)
|
||||
|
||||
@@ -137,16 +179,16 @@ fun debugState(context: Context) {
|
||||
|
||||
val bytesNotes =
|
||||
LocalCache.notes
|
||||
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
|
||||
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory()?.toLong() ?: 0L })
|
||||
val bytesAddressables =
|
||||
LocalCache.addressables
|
||||
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
|
||||
.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", "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", "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 ")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,26 +23,23 @@ package com.vitorpamplona.amethyst
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.core.content.edit
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.model.UiSettings
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
@@ -55,16 +52,19 @@ import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
|
||||
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.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.File
|
||||
|
||||
// Release mode (!BuildConfig.DEBUG) always uses encrypted preferences
|
||||
@@ -74,6 +74,7 @@ private const val DEBUG_PLAINTEXT_PREFERENCES = false
|
||||
private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
|
||||
|
||||
@Immutable
|
||||
@Serializable
|
||||
data class AccountInfo(
|
||||
val npub: String,
|
||||
val hasPrivKey: Boolean,
|
||||
@@ -98,6 +99,7 @@ private object PrefKeys {
|
||||
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
|
||||
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
|
||||
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
|
||||
const val LATEST_INDEX_RELAY_LIST = "latestIndexRelayList"
|
||||
const val LATEST_BLOCKED_RELAY_LIST = "latestBlockedRelayList"
|
||||
const val LATEST_TRUSTED_RELAY_LIST = "latestTrustedRelayList"
|
||||
const val LATEST_MUTE_LIST = "latestMuteList"
|
||||
@@ -163,7 +165,7 @@ object LocalPreferences {
|
||||
with(encryptedPreferences()) {
|
||||
val newSystemOfAccounts =
|
||||
getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let {
|
||||
JsonMapper.mapper.readValue<List<AccountInfo>>(it)
|
||||
JsonMapper.fromJson<List<AccountInfo>>(it)
|
||||
}
|
||||
|
||||
if (!newSystemOfAccounts.isNullOrEmpty()) {
|
||||
@@ -183,7 +185,9 @@ object LocalPreferences {
|
||||
|
||||
savedAccounts.emit(migrated)
|
||||
|
||||
edit { putString(PrefKeys.ALL_ACCOUNT_INFO, JsonMapper.mapper.writeValueAsString(savedAccounts.value)) }
|
||||
edit {
|
||||
putString(PrefKeys.ALL_ACCOUNT_INFO, JsonMapper.toJson(migrated))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,14 +207,14 @@ object LocalPreferences {
|
||||
.edit {
|
||||
putString(
|
||||
PrefKeys.ALL_ACCOUNT_INFO,
|
||||
JsonMapper.mapper.writeValueAsString(accounts.filter { !it.isTransient }),
|
||||
JsonMapper.toJson(accounts.filter { !it.isTransient }),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val prefsDirPath: String
|
||||
get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/"
|
||||
get() = "${Amethyst.instance.appContext.filesDir.parent}/shared_prefs/"
|
||||
|
||||
private suspend fun addAccount(accInfo: AccountInfo) {
|
||||
val accounts = savedAccounts().filter { it.npub != accInfo.npub }.plus(accInfo)
|
||||
@@ -255,7 +259,7 @@ object LocalPreferences {
|
||||
return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) {
|
||||
val preferenceFile =
|
||||
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
||||
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
||||
Amethyst.instance.appContext.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
||||
} else {
|
||||
Amethyst.instance.encryptedStorage(npub)
|
||||
}
|
||||
@@ -310,7 +314,7 @@ object LocalPreferences {
|
||||
|
||||
putString(
|
||||
PrefKeys.DEFAULT_FILE_SERVER,
|
||||
JsonMapper.mapper.writeValueAsString(settings.defaultFileServer),
|
||||
JsonMapper.toJson(settings.defaultFileServer),
|
||||
)
|
||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, settings.defaultHomeFollowList.value)
|
||||
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, settings.defaultStoriesFollowList.value)
|
||||
@@ -322,72 +326,18 @@ object LocalPreferences {
|
||||
PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST,
|
||||
settings.defaultDiscoveryFollowList.value,
|
||||
)
|
||||
putString(
|
||||
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
|
||||
JsonMapper.mapper.writeValueAsString(settings.zapPaymentRequest.value?.denormalize()),
|
||||
)
|
||||
if (settings.backupContactList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_CONTACT_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupContactList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_CONTACT_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupUserMetadata != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_USER_METADATA,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupUserMetadata),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_USER_METADATA)
|
||||
}
|
||||
putOrRemove(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, settings.zapPaymentRequest.value?.denormalize())
|
||||
|
||||
if (settings.backupDMRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_DM_RELAY_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupDMRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_DM_RELAY_LIST)
|
||||
}
|
||||
putOrRemove(PrefKeys.LATEST_CONTACT_LIST, settings.backupContactList)
|
||||
|
||||
if (settings.backupNIP65RelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_NIP65_RELAY_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupNIP65RelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupSearchRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_SEARCH_RELAY_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupSearchRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupBlockedRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_BLOCKED_RELAY_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupBlockedRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupTrustedRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_TRUSTED_RELAY_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupTrustedRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
|
||||
}
|
||||
putOrRemove(PrefKeys.LATEST_USER_METADATA, settings.backupUserMetadata)
|
||||
putOrRemove(PrefKeys.LATEST_DM_RELAY_LIST, settings.backupDMRelayList)
|
||||
putOrRemove(PrefKeys.LATEST_NIP65_RELAY_LIST, settings.backupNIP65RelayList)
|
||||
putOrRemove(PrefKeys.LATEST_SEARCH_RELAY_LIST, settings.backupSearchRelayList)
|
||||
putOrRemove(PrefKeys.LATEST_INDEX_RELAY_LIST, settings.backupIndexRelayList)
|
||||
putOrRemove(PrefKeys.LATEST_BLOCKED_RELAY_LIST, settings.backupBlockedRelayList)
|
||||
putOrRemove(PrefKeys.LATEST_TRUSTED_RELAY_LIST, settings.backupTrustedRelayList)
|
||||
|
||||
if (settings.localRelayServers.value.isNotEmpty()) {
|
||||
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers.value)
|
||||
@@ -395,77 +345,15 @@ object LocalPreferences {
|
||||
remove(PrefKeys.LOCAL_RELAY_SERVERS)
|
||||
}
|
||||
|
||||
if (settings.backupMuteList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_MUTE_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupMuteList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_MUTE_LIST)
|
||||
}
|
||||
putOrRemove(PrefKeys.LATEST_MUTE_LIST, settings.backupMuteList)
|
||||
putOrRemove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, settings.backupPrivateHomeRelayList)
|
||||
putOrRemove(PrefKeys.LATEST_APP_SPECIFIC_DATA, settings.backupAppSpecificData)
|
||||
|
||||
if (settings.backupPrivateHomeRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupPrivateHomeRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupAppSpecificData != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_APP_SPECIFIC_DATA,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupAppSpecificData),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
||||
}
|
||||
|
||||
if (settings.backupChannelList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_CHANNEL_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupChannelList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_CHANNEL_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupCommunityList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_COMMUNITY_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupCommunityList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_COMMUNITY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupHashtagList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_HASHTAG_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupHashtagList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_HASHTAG_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupGeohashList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_GEOHASH_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupGeohashList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_GEOHASH_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupEphemeralChatList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_EPHEMERAL_LIST,
|
||||
JsonMapper.mapper.writeValueAsString(settings.backupEphemeralChatList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_EPHEMERAL_LIST)
|
||||
}
|
||||
putOrRemove(PrefKeys.LATEST_CHANNEL_LIST, settings.backupChannelList)
|
||||
putOrRemove(PrefKeys.LATEST_COMMUNITY_LIST, settings.backupCommunityList)
|
||||
putOrRemove(PrefKeys.LATEST_HASHTAG_LIST, settings.backupHashtagList)
|
||||
putOrRemove(PrefKeys.LATEST_GEOHASH_LIST, settings.backupGeohashList)
|
||||
putOrRemove(PrefKeys.LATEST_EPHEMERAL_LIST, settings.backupEphemeralChatList)
|
||||
|
||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
|
||||
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
|
||||
@@ -475,8 +363,6 @@ object LocalPreferences {
|
||||
remove(PrefKeys.USE_PROXY)
|
||||
remove(PrefKeys.PROXY_PORT)
|
||||
|
||||
putString(PrefKeys.TOR_SETTINGS, JsonMapper.mapper.writeValueAsString(settings.torSettings.toSettings()))
|
||||
|
||||
val regularMap =
|
||||
settings.lastReadPerRoute.value.mapValues {
|
||||
it.value.value
|
||||
@@ -484,13 +370,13 @@ object LocalPreferences {
|
||||
|
||||
putString(
|
||||
PrefKeys.LAST_READ_PER_ROUTE,
|
||||
JsonMapper.mapper.writeValueAsString(regularMap),
|
||||
JsonMapper.toJson(regularMap),
|
||||
)
|
||||
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value)
|
||||
|
||||
putString(
|
||||
PrefKeys.PENDING_ATTESTATIONS,
|
||||
JsonMapper.mapper.writeValueAsString(settings.pendingAttestations.value),
|
||||
JsonMapper.toJson(settings.pendingAttestations.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -498,23 +384,25 @@ object LocalPreferences {
|
||||
Log.d("LocalPreferences", "Saved to encrypted storage")
|
||||
}
|
||||
|
||||
suspend fun loadCurrentAccountFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) }
|
||||
suspend fun loadAccountConfigFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadAccountConfigFromEncryptedStorage(it) }
|
||||
|
||||
suspend fun saveSharedSettings(
|
||||
sharedSettings: Settings,
|
||||
fun saveSharedSettings(
|
||||
sharedSettings: UiSettings,
|
||||
prefs: SharedPreferences = encryptedPreferences(),
|
||||
) {
|
||||
Log.d("LocalPreferences", "Saving to shared settings")
|
||||
prefs.edit {
|
||||
putString(PrefKeys.SHARED_SETTINGS, JsonMapper.mapper.writeValueAsString(sharedSettings))
|
||||
putString(PrefKeys.SHARED_SETTINGS, JsonMapper.toJson(sharedSettings))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? {
|
||||
fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): UiSettings? {
|
||||
Log.d("LocalPreferences", "Load shared settings")
|
||||
with(prefs) {
|
||||
return try {
|
||||
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { JsonMapper.mapper.readValue<Settings>(it) }
|
||||
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let {
|
||||
JsonMapper.fromJson<UiSettings>(it)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
@@ -529,7 +417,7 @@ object LocalPreferences {
|
||||
|
||||
val mutex = Mutex()
|
||||
|
||||
suspend fun loadCurrentAccountFromEncryptedStorage(npub: String): AccountSettings? {
|
||||
suspend fun loadAccountConfigFromEncryptedStorage(npub: String): AccountSettings? {
|
||||
// if already loaded, return right away
|
||||
if (cachedAccounts.containsKey(npub)) {
|
||||
return cachedAccounts[npub]
|
||||
@@ -583,6 +471,7 @@ object LocalPreferences {
|
||||
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 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)
|
||||
@@ -598,8 +487,6 @@ object LocalPreferences {
|
||||
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
||||
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
|
||||
|
||||
val torSettings = parseOrNull<TorSettings>(PrefKeys.TOR_SETTINGS) ?: TorSettings()
|
||||
|
||||
val lastReadPerRoute =
|
||||
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
|
||||
MutableStateFlow(it.value)
|
||||
@@ -627,6 +514,7 @@ object LocalPreferences {
|
||||
backupNIP65RelayList = latestNip65RelayList,
|
||||
backupDMRelayList = latestDmRelayList,
|
||||
backupSearchRelayList = latestSearchRelayList,
|
||||
backupIndexRelayList = latestIndexRelayList,
|
||||
backupBlockedRelayList = latestBlockedRelayList,
|
||||
backupTrustedRelayList = latestTrustedRelayList,
|
||||
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
|
||||
@@ -637,7 +525,6 @@ object LocalPreferences {
|
||||
backupHashtagList = latestHashtagList,
|
||||
backupGeohashList = latestGeohashList,
|
||||
backupEphemeralChatList = latestEphemeralList,
|
||||
torSettings = TorSettingsFlow.build(torSettings),
|
||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
||||
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
||||
pendingAttestations = MutableStateFlow(pendingAttestations),
|
||||
@@ -648,7 +535,7 @@ object LocalPreferences {
|
||||
return result
|
||||
}
|
||||
|
||||
private inline fun <reified T> SharedPreferences.parseOrNull(key: String): T? {
|
||||
private inline fun <reified T : Any> SharedPreferences.parseOrNull(key: String): T? {
|
||||
val value = getString(key, null)
|
||||
if (value.isNullOrEmpty() || value == "null") {
|
||||
return null
|
||||
@@ -657,7 +544,7 @@ object LocalPreferences {
|
||||
if (T::class.java.isInstance(Event::class.java)) {
|
||||
Event.fromJson(value) as T?
|
||||
} else {
|
||||
JsonMapper.mapper.readValue<T?>(value)
|
||||
JsonMapper.fromJson<T>(value)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
@@ -679,4 +566,37 @@ object LocalPreferences {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.Editor.putOrRemove(
|
||||
key: String,
|
||||
event: Any?,
|
||||
) {
|
||||
if (event != null) {
|
||||
putString(key, JsonMapper.toJson(event))
|
||||
} else {
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.Editor.putOrRemove(
|
||||
key: String,
|
||||
event: Event?,
|
||||
) {
|
||||
if (event != null) {
|
||||
putString(key, OptimizedJsonMapper.toJson(event))
|
||||
} else {
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.Editor.putOrRemove(
|
||||
key: String,
|
||||
nwc: Nip47WalletConnect.Nip47URI?,
|
||||
) {
|
||||
if (nwc != null) {
|
||||
putString(key, JsonMapper.toJson(nwc))
|
||||
} else {
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
|
||||
@@ -35,9 +35,11 @@ import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountHomeRelayState
|
||||
import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState
|
||||
import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState
|
||||
import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.DeclaredFollowsPerOutboxRelay
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListReusedOutboxOrProxyRelays
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState
|
||||
import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState
|
||||
import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState
|
||||
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
|
||||
@@ -52,7 +54,6 @@ 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.blockPeopleList.BlockPeopleListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
|
||||
@@ -65,6 +66,9 @@ import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayLis
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.FollowListsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache
|
||||
@@ -78,18 +82,20 @@ import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
|
||||
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState
|
||||
import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
|
||||
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
|
||||
import com.vitorpamplona.amethyst.model.privacyOptions.PrivacyState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineWithIndexRelayListsState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineWithSearchRelayListsState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedServerListState
|
||||
import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
|
||||
import com.vitorpamplona.amethyst.model.torState.TorRelayState
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor
|
||||
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
|
||||
@@ -121,18 +127,16 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
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.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
@@ -161,8 +165,6 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
@@ -173,7 +175,6 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||
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.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||
@@ -200,12 +201,15 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.containsAny
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -218,9 +222,11 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
class Account(
|
||||
val settings: AccountSettings = AccountSettings(KeyPair()),
|
||||
val signer: NostrSigner,
|
||||
geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||
val otsResolverBuilder: OtsResolverBuilder,
|
||||
val cache: LocalCache,
|
||||
val client: NostrClient,
|
||||
val client: INostrClient,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
private var userProfileCache: User? = null
|
||||
@@ -229,7 +235,7 @@ class Account(
|
||||
|
||||
val userMetadata = UserMetadataState(signer, cache, scope, settings)
|
||||
|
||||
val nip47SignerState = NwcSignerState(signer, cache, scope, settings)
|
||||
val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings)
|
||||
|
||||
val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings)
|
||||
val localRelayList = LocalRelayListState(signer, cache, scope, settings)
|
||||
@@ -257,7 +263,7 @@ class Account(
|
||||
val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer)
|
||||
val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings)
|
||||
|
||||
val kind3FollowList = FollowListState(signer, cache, scope, settings)
|
||||
val kind3FollowList = Kind3FollowListState(signer, cache, scope, settings)
|
||||
|
||||
val ephemeralChatListDecryptionCache = EphemeralChatListDecryptionCache(signer)
|
||||
val ephemeralChatList = EphemeralChatListState(signer, cache, ephemeralChatListDecryptionCache, scope, settings)
|
||||
@@ -279,6 +285,8 @@ class Account(
|
||||
|
||||
val peopleListDecryptionCache = PeopleListDecryptionCache(signer)
|
||||
val blockPeopleList = BlockPeopleListState(signer, cache, peopleListDecryptionCache, scope)
|
||||
val peopleLists = PeopleListsState(signer, cache, peopleListDecryptionCache, scope)
|
||||
val followLists = FollowListsState(signer, cache, scope)
|
||||
|
||||
val hiddenUsers = HiddenUsersState(muteList.flow, blockPeopleList.flow, scope, settings)
|
||||
|
||||
@@ -301,13 +309,22 @@ class Account(
|
||||
|
||||
// Follows Relays
|
||||
val followOutboxesOrProxy = FollowListOutboxOrProxyRelays(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope)
|
||||
val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, indexerRelayList, scope)
|
||||
|
||||
// only follow relays that are declared in more than one user.
|
||||
val followSharedOutboxesOrProxy = FollowListReusedOutboxOrProxyRelays(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope)
|
||||
|
||||
val followPlusAllMineWithIndex = MergedFollowPlusMineWithIndexRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, indexerRelayList, scope)
|
||||
val followPlusAllMineWithSearch = MergedFollowPlusMineWithSearchRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, searchRelayList, scope)
|
||||
val defaultGlobalRelays = MergedFollowPlusMineRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, scope)
|
||||
|
||||
// keeps a cache of the declared outbox relays for each author
|
||||
val declaredFollowsPerRelay = DeclaredFollowsPerOutboxRelay(kind3FollowList, cache, scope).flow
|
||||
|
||||
// keeps a cache of the outbox relays for each author
|
||||
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow
|
||||
|
||||
// Merges all follow lists to create a single All Follows feed.
|
||||
val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope)
|
||||
val allFollows = MergedFollowListsState(kind3FollowList, peopleLists, followLists, hashtagList, geohashList, communityList, scope)
|
||||
|
||||
val privateDMDecryptionCache = PrivateDMCache(signer)
|
||||
val privateZapsDecryptionCache = PrivateZapCache(signer)
|
||||
@@ -315,17 +332,7 @@ class Account(
|
||||
|
||||
val chatroomList = cache.getOrCreateChatroomList(signer.pubKey)
|
||||
|
||||
val privacyState = PrivacyState(settings)
|
||||
val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope)
|
||||
|
||||
val otsResolverBuilder: OkHttpOtsResolverBuilder =
|
||||
OkHttpOtsResolverBuilder(
|
||||
{
|
||||
Amethyst.instance.okHttpClients.getHttpClient(privacyState.shouldUseTorForMoneyOperations(it))
|
||||
},
|
||||
privacyState::shouldUseTorForMoneyOperations,
|
||||
Amethyst.instance.otsBlockHeightCache,
|
||||
)
|
||||
val newNotesPreProcessor = EventProcessor(this, cache)
|
||||
|
||||
val otsState = OtsState(signer, cache, otsResolverBuilder, scope, settings)
|
||||
|
||||
@@ -342,9 +349,10 @@ class Account(
|
||||
val liveHomeFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||
FeedTopNavFilterState(
|
||||
feedFilterListName = settings.defaultHomeFollowList,
|
||||
kind3Follows = kind3FollowList.flow,
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = followPlusAllMine.flow,
|
||||
followsRelays = defaultGlobalRelays.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
proxyRelays = proxyRelayList.flow,
|
||||
caches = feedDecryptionCaches,
|
||||
@@ -357,9 +365,10 @@ class Account(
|
||||
val liveStoriesFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||
FeedTopNavFilterState(
|
||||
feedFilterListName = settings.defaultStoriesFollowList,
|
||||
kind3Follows = kind3FollowList.flow,
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = followPlusAllMine.flow,
|
||||
followsRelays = defaultGlobalRelays.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
proxyRelays = proxyRelayList.flow,
|
||||
caches = feedDecryptionCaches,
|
||||
@@ -372,9 +381,10 @@ class Account(
|
||||
val liveDiscoveryFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||
FeedTopNavFilterState(
|
||||
feedFilterListName = settings.defaultDiscoveryFollowList,
|
||||
kind3Follows = kind3FollowList.flow,
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = followPlusAllMine.flow,
|
||||
followsRelays = defaultGlobalRelays.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
proxyRelays = proxyRelayList.flow,
|
||||
caches = feedDecryptionCaches,
|
||||
@@ -387,9 +397,10 @@ class Account(
|
||||
val liveNotificationFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||
FeedTopNavFilterState(
|
||||
feedFilterListName = settings.defaultNotificationFollowList,
|
||||
kind3Follows = kind3FollowList.flow,
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = followPlusAllMine.flow,
|
||||
followsRelays = defaultGlobalRelays.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
proxyRelays = proxyRelayList.flow,
|
||||
caches = feedDecryptionCaches,
|
||||
@@ -399,18 +410,6 @@ class Account(
|
||||
|
||||
val liveNotificationFollowListsPerRelay = OutboxLoaderState(liveNotificationFollowLists, cache, scope).flow
|
||||
|
||||
/*
|
||||
val mergedTopFeedAuthorLists =
|
||||
MergedTopFeedAuthorListsState(
|
||||
liveHomeFollowListsPerRelay,
|
||||
liveStoriesFollowListsPerRelay,
|
||||
liveDiscoveryFollowListsPerRelay,
|
||||
liveNotificationFollowListsPerRelay,
|
||||
scope,
|
||||
).flow
|
||||
|
||||
*/
|
||||
|
||||
fun isWriteable(): Boolean = settings.isWriteable()
|
||||
|
||||
suspend fun updateWarnReports(warnReports: Boolean): Boolean {
|
||||
@@ -704,7 +703,8 @@ class Account(
|
||||
|
||||
fun computeRelayListToBroadcast(event: Event): Set<NormalizedRelayUrl> {
|
||||
if (event is MetadataEvent || event is AdvertisedRelayListEvent) {
|
||||
return followPlusAllMine.flow.value + client.relayStatusFlow().value.available
|
||||
// everywhere
|
||||
return followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available
|
||||
}
|
||||
if (event is GiftWrapEvent) {
|
||||
val receiver = event.recipientPubKey()
|
||||
@@ -813,32 +813,32 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
fun broadcast(note: Note) {
|
||||
note.event?.let {
|
||||
if (it is WrappedEvent && it.host != null) {
|
||||
suspend fun broadcast(note: Note) {
|
||||
note.event?.let { noteEvent ->
|
||||
if (noteEvent is WrappedEvent && noteEvent.host != null) {
|
||||
// download the event and send it.
|
||||
it.host?.let { host ->
|
||||
client.downloadFirstEvent(
|
||||
filters =
|
||||
note.relays.associateWith { relay ->
|
||||
listOf(
|
||||
Filter(
|
||||
ids = listOf(host.id),
|
||||
),
|
||||
)
|
||||
},
|
||||
onResponse = {
|
||||
client.send(it, computeRelayListToBroadcast(it))
|
||||
},
|
||||
)
|
||||
noteEvent.host?.let { host ->
|
||||
client
|
||||
.downloadFirstEvent(
|
||||
filters =
|
||||
note.relays.associateWith { relay ->
|
||||
listOf(
|
||||
Filter(
|
||||
ids = listOf(host.id),
|
||||
),
|
||||
)
|
||||
},
|
||||
)?.let { downloadedEvent ->
|
||||
client.send(downloadedEvent, computeRelayListToBroadcast(downloadedEvent))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client.send(it, computeRelayListToBroadcast(note))
|
||||
client.send(noteEvent, computeRelayListToBroadcast(note))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateAttestations() = sendAutomatic(otsState.updateAttestations())
|
||||
fun upgradeAttestations() = otsState.upgradeAttestationsIfNeeded(::sendAutomatic)
|
||||
|
||||
suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user))
|
||||
|
||||
@@ -904,7 +904,7 @@ class Account(
|
||||
}
|
||||
|
||||
fun sendLiterallyEverywhere(event: Event) {
|
||||
client.send(event, outboxRelays.flow.value + indexerRelayList.flow.value + client.relayStatusFlow().value.available)
|
||||
client.send(event, followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available)
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
@@ -1469,18 +1469,6 @@ class Account(
|
||||
val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) }
|
||||
|
||||
mine.forEach { giftWrap ->
|
||||
val gift = giftWrap.unwrapOrNull(signer)
|
||||
if (gift is SealedRumorEvent) {
|
||||
val rumor = gift.unsealOrNull(signer)
|
||||
if (rumor != null) {
|
||||
cache.justConsumeMyOwnEvent(rumor)
|
||||
}
|
||||
}
|
||||
|
||||
if (gift != null) {
|
||||
cache.justConsumeMyOwnEvent(gift)
|
||||
}
|
||||
|
||||
cache.justConsumeMyOwnEvent(giftWrap)
|
||||
}
|
||||
|
||||
@@ -1558,13 +1546,10 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendAuthEvent(
|
||||
relay: IRelayClient,
|
||||
suspend fun createAuthEvent(
|
||||
relay: NormalizedRelayUrl,
|
||||
challenge: String,
|
||||
) {
|
||||
val auth = RelayAuthEvent.create(relay.url, challenge, signer)
|
||||
client.sendIfExists(auth, relay.url)
|
||||
}
|
||||
): RelayAuthEvent = RelayAuthEvent.create(relay, challenge, signer)
|
||||
|
||||
suspend fun hideWord(word: String) {
|
||||
sendMyPublicAndPrivateOutbox(muteList.hideWord(word))
|
||||
@@ -1683,10 +1668,24 @@ class Account(
|
||||
note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users
|
||||
}
|
||||
|
||||
fun isDecryptedContentHidden(noteEvent: PrivateDmEvent): Boolean =
|
||||
if (hiddenUsers.flow.value.hiddenWordsCase
|
||||
.isNotEmpty()
|
||||
) {
|
||||
val decrypted = privateDMDecryptionCache.cachedDM(noteEvent)
|
||||
decrypted?.containsAny(hiddenUsers.flow.value.hiddenWordsCase) == true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
fun isFollowing(user: User): Boolean = user.pubkeyHex in followingKeySet()
|
||||
|
||||
fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
|
||||
|
||||
fun isKnown(user: User): Boolean = user.pubkeyHex in allFollows.flow.value.authors
|
||||
|
||||
fun isKnown(user: HexKey): Boolean = user in allFollows.flow.value.authors
|
||||
|
||||
fun isAcceptable(note: Note): Boolean {
|
||||
return note.author?.let { isAcceptable(it) } ?: true &&
|
||||
// if user hasn't hided this author
|
||||
@@ -1737,22 +1736,6 @@ class Account(
|
||||
|
||||
suspend fun sendBlossomServersList(servers: List<String>) = sendMyPublicAndPrivateOutbox(blossomServers.saveBlossomServersList(servers))
|
||||
|
||||
fun getAllPeopleLists(): List<AddressableNote> = getAllPeopleLists(signer.pubKey)
|
||||
|
||||
fun getAllPeopleLists(pubkey: HexKey): List<AddressableNote> =
|
||||
cache.addressables
|
||||
.filter { _, addressableNote ->
|
||||
val noteEvent = addressableNote.event
|
||||
|
||||
if (noteEvent is PeopleListEvent) {
|
||||
noteEvent.pubKey == pubkey && (noteEvent.hasAnyTaggedUser() || peopleListDecryptionCache.cachedUserIdSet(noteEvent).isNotEmpty())
|
||||
} else if (noteEvent is FollowListEvent) {
|
||||
noteEvent.pubKey == pubkey && noteEvent.hasAnyTaggedUser()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun markAsRead(
|
||||
route: String,
|
||||
timestampInSecs: Long,
|
||||
@@ -1767,7 +1750,7 @@ class Account(
|
||||
fun observeDonatedInThisVersion() =
|
||||
settings
|
||||
.observeDonatedInVersion(BuildConfig.VERSION_NAME)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Eagerly, hasDonatedInThisVersion())
|
||||
|
||||
fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME)
|
||||
@@ -1775,7 +1758,7 @@ class Account(
|
||||
init {
|
||||
Log.d("AccountRegisterObservers", "Init")
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch {
|
||||
cache.antiSpam.flowSpam.collect {
|
||||
it.cache.spamMessages.snapshot().values.forEach { spammer ->
|
||||
if (!hiddenUsers.isHidden(spammer.pubkeyHex) && spammer.shouldHide()) {
|
||||
@@ -1786,5 +1769,34 @@ class Account(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
cache.live.newEventBundles.collect { newNotes ->
|
||||
logTime("Account ${userProfile().toBestDisplayName()} newEventBundle Update with ${newNotes.size} new notes") {
|
||||
upgradeAttestations()
|
||||
newNotesPreProcessor.runNew(newNotes)
|
||||
peopleLists.newNotes(newNotes)
|
||||
followLists.newNotes(newNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
cache.live.deletedEventBundles.collect { deletedNotes ->
|
||||
logTime("Account ${userProfile().toBestDisplayName()} deletedEventBundle Update with ${deletedNotes.size} new notes") {
|
||||
newNotesPreProcessor.runDeleted(deletedNotes)
|
||||
peopleLists.deletedNotes(deletedNotes)
|
||||
followLists.deletedNotes(deletedNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
settings.saveable.debounce(1000).collect {
|
||||
if (it.accountSettings != null) {
|
||||
LocalPreferences.saveToEncryptedStorage(it.accountSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,19 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
@@ -46,10 +41,10 @@ import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||
@@ -103,6 +98,12 @@ val GLOBAL_FOLLOWS = " Global "
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val ALL_FOLLOWS = " All Follows "
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val ALL_USER_FOLLOWS = " All User Follows "
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val KIND3_FOLLOWS = " Main User Follows "
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val AROUND_ME = " Around Me "
|
||||
|
||||
@@ -126,6 +127,7 @@ class AccountSettings(
|
||||
var backupDMRelayList: ChatMessageRelayListEvent? = null,
|
||||
var backupNIP65RelayList: AdvertisedRelayListEvent? = null,
|
||||
var backupSearchRelayList: SearchRelayListEvent? = null,
|
||||
var backupIndexRelayList: IndexerRelayListEvent? = null,
|
||||
var backupBlockedRelayList: BlockedRelayListEvent? = null,
|
||||
var backupTrustedRelayList: TrustedRelayListEvent? = null,
|
||||
var backupMuteList: MuteListEvent? = null,
|
||||
@@ -136,7 +138,6 @@ class AccountSettings(
|
||||
var backupHashtagList: HashtagListEvent? = null,
|
||||
var backupGeohashList: GeohashListEvent? = null,
|
||||
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
||||
val torSettings: TorSettingsFlow = TorSettingsFlow(),
|
||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
||||
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
||||
@@ -154,21 +155,6 @@ class AccountSettings(
|
||||
|
||||
fun isWriteable(): Boolean = keyPair.privKey != null || externalSignerPackageName != null
|
||||
|
||||
fun createSigner(contentResolver: ContentResolver) =
|
||||
if (keyPair.privKey != null) {
|
||||
NostrSignerInternal(keyPair)
|
||||
} else {
|
||||
when (val packageName = externalSignerPackageName) {
|
||||
null -> NostrSignerInternal(keyPair)
|
||||
else ->
|
||||
NostrSignerExternal(
|
||||
pubKey = keyPair.pubKey.toHexKey(),
|
||||
packageName = packageName,
|
||||
contentResolver = contentResolver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// Zaps and Reactions
|
||||
// ---
|
||||
@@ -268,17 +254,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// proxy settings
|
||||
// ---
|
||||
fun setTorSettings(newTorSettings: TorSettings): Boolean =
|
||||
if (torSettings.update(newTorSettings)) {
|
||||
saveAccountSettings()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
// ---
|
||||
// language services
|
||||
// ---
|
||||
@@ -372,6 +347,16 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateIndexRelayList(newIndexRelayList: IndexerRelayListEvent?) {
|
||||
if (newIndexRelayList == null || newIndexRelayList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupIndexRelayList?.id != newIndexRelayList.id) {
|
||||
backupIndexRelayList = newIndexRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBlockedRelayList(newBlockedRelayList: BlockedRelayListEvent?) {
|
||||
if (newBlockedRelayList == null || newBlockedRelayList.tags.isEmpty()) return
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model
|
||||
import android.content.res.Resources
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Locale
|
||||
|
||||
val DefaultReactions =
|
||||
@@ -47,6 +48,7 @@ fun getLanguagesSpokenByUser(): Set<String> {
|
||||
return codedList
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class AccountSyncedSettingsInternal(
|
||||
val reactions: AccountReactionPreferencesInternal = AccountReactionPreferencesInternal(),
|
||||
val zaps: AccountZapPreferencesInternal = AccountZapPreferencesInternal(),
|
||||
@@ -54,21 +56,25 @@ class AccountSyncedSettingsInternal(
|
||||
val security: AccountSecurityPreferencesInternal = AccountSecurityPreferencesInternal(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class AccountReactionPreferencesInternal(
|
||||
var reactionChoices: List<String> = DefaultReactions,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class AccountZapPreferencesInternal(
|
||||
var zapAmountChoices: List<Long> = DefaultZapAmounts,
|
||||
val defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class AccountLanguagePreferencesInternal(
|
||||
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
||||
var languagePreferences: Map<String, String> = mapOf(),
|
||||
var translateTo: String = Locale.getDefault().language,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class AccountSecurityPreferencesInternal(
|
||||
val showSensitiveContent: Boolean? = null,
|
||||
var warnAboutPostsWithReports: Boolean = true,
|
||||
|
||||
@@ -20,18 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.ui.note.njumpLink
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
data class Spammer(
|
||||
@@ -65,7 +65,7 @@ class AntiSpamFilter {
|
||||
// really long, make it ok.
|
||||
// The idea here is to avoid considering repeated "@Bot, command" messages spam, while still
|
||||
// blocking repeated "lnbc..." invoices or fishing urls
|
||||
if (event.content.length < 180 && event.content.startsWith("nostr:") && Nip19Parser.nip19regex.matcher(event.content).find()) return false
|
||||
if (event.content.length < 180 && event.content.startsWith("nostr:") && Nip19Parser.hasAny(event.content)) return false
|
||||
|
||||
// double list strategy:
|
||||
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
|
||||
@@ -93,7 +93,9 @@ class AntiSpamFilter {
|
||||
val spammer = logOffender(hash, event)
|
||||
|
||||
if (spammer.shouldHide() && relay != null) {
|
||||
RelayStats.newSpam(relay, "$link1 $link2")
|
||||
Amethyst.instance.relayStats
|
||||
.get(relay)
|
||||
.newSpam("$link1 $link2")
|
||||
}
|
||||
|
||||
flowSpam.tryEmit(AntiSpamState(this))
|
||||
@@ -119,7 +121,9 @@ class AntiSpamFilter {
|
||||
val spammer = logOffender(hash, event)
|
||||
|
||||
if (spammer.shouldHide() && relay != null) {
|
||||
RelayStats.newSpam(relay, "$link1 $link2")
|
||||
Amethyst.instance.relayStats
|
||||
.get(relay)
|
||||
.newSpam("$link1 $link2")
|
||||
}
|
||||
|
||||
flowSpam.tryEmit(AntiSpamState(this))
|
||||
|
||||
@@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.dal.ListChange
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -43,11 +43,21 @@ abstract class Channel : NotesGatherer {
|
||||
fun changesFlow(): MutableSharedFlow<ListChange<Note>> {
|
||||
val current = changesFlow.get()
|
||||
if (current != null) return current
|
||||
val new = MutableSharedFlow<ListChange<Note>>(0, 100, BufferOverflow.DROP_OLDEST)
|
||||
val new = MutableSharedFlow<ListChange<Note>>(0, 10, BufferOverflow.DROP_OLDEST)
|
||||
changesFlow = WeakReference(new)
|
||||
return new
|
||||
}
|
||||
|
||||
open fun participatingAuthors(maxTimeLimit: Long) =
|
||||
notes.mapNotNull { key, value ->
|
||||
val createdAt = value.createdAt()
|
||||
if (createdAt != null && createdAt > maxTimeLimit) {
|
||||
value.author
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun toBestDisplayName(): String
|
||||
|
||||
open fun relays(): Set<NormalizedRelayUrl> =
|
||||
@@ -86,7 +96,7 @@ abstract class Channel : NotesGatherer {
|
||||
notes.put(note.idHex, note)
|
||||
note.addGatherer(this)
|
||||
|
||||
if ((note.createdAt() ?: 0) > (lastNote?.createdAt() ?: 0)) {
|
||||
if ((note.createdAt() ?: 0L) > (lastNote?.createdAt() ?: 0L)) {
|
||||
lastNote = note
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ object Constants {
|
||||
|
||||
val bitcoiner = RelayUrlNormalizer.normalize("wss://nostr.bitcoiner.social")
|
||||
val oxtr = RelayUrlNormalizer.normalize("wss://nostr.oxtr.dev")
|
||||
val fmtwiz = RelayUrlNormalizer.normalize("wss://nostr.fmt.wiz.biz")
|
||||
|
||||
val nostoday = RelayUrlNormalizer.normalize("wss://search.nos.today")
|
||||
|
||||
@@ -46,7 +45,8 @@ object Constants {
|
||||
val coracle = RelayUrlNormalizer.normalize("wss://indexer.coracle.social")
|
||||
val userkinds = RelayUrlNormalizer.normalize("wss://user.kindpag.es")
|
||||
|
||||
val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr, fmtwiz)
|
||||
val bootstrapInbox = setOf(band, damus, primal, mom, nos, bitcoiner, oxtr)
|
||||
val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr)
|
||||
|
||||
val defaultSearchRelaySet = setOf(band, wine, where)
|
||||
}
|
||||
|
||||
@@ -49,23 +49,24 @@ import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
||||
import com.vitorpamplona.amethyst.ui.components.HashTag
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderRegular
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderTextParagraph
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderHashTagIconsPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
ThemeComparisonColumn {
|
||||
RenderRegular(
|
||||
"Testing rendering of hashtags: #flowerstr #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate, #gamestr, #gamechain",
|
||||
EmptyTagList,
|
||||
) { word, state ->
|
||||
when (word) {
|
||||
is HashTagSegment -> HashTag(word, accountViewModel, EmptyNav)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
) { paragraph, state, spaceWidth, modifier ->
|
||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||
when (word) {
|
||||
is HashTagSegment -> HashTag(word, EmptyNav())
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,15 @@
|
||||
* 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.quartz.utils
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.quartz.utils.cache.CacheOperations
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.ConcurrentSkipListMap
|
||||
import java.util.function.BiConsumer
|
||||
|
||||
class LargeSoftCache<K, V> : CacheOperations<K, V> {
|
||||
private val cache = ConcurrentSkipListMap<K, WeakReference<V>>()
|
||||
protected val cache = ConcurrentSkipListMap<K, WeakReference<V>>()
|
||||
|
||||
fun keys() = cache.keys
|
||||
|
||||
@@ -117,15 +118,23 @@ class LargeSoftCache<K, V> : CacheOperations<K, V> {
|
||||
}
|
||||
keysToRemove.forEach { key, value ->
|
||||
cache.remove(key, value)
|
||||
println("Cleaned up entry for key: $key (object was garbage collected)")
|
||||
}
|
||||
println("Cache cleanup completed. Remaining size: ${cache.size}")
|
||||
}
|
||||
|
||||
override fun forEach(consumer: BiConsumer<K, V>) {
|
||||
cache.forEach(BiConsumerWrapper(this, consumer))
|
||||
}
|
||||
|
||||
override fun forEach(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: BiConsumer<K, V>,
|
||||
) {
|
||||
cache
|
||||
.subMap(from, true, to, true)
|
||||
.forEach(BiConsumerWrapper(this, consumer))
|
||||
}
|
||||
|
||||
class BiConsumerWrapper<K, V>(
|
||||
val cache: LargeSoftCache<K, V>,
|
||||
val inner: BiConsumer<K, V>,
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.utils.cache.CacheCollectors
|
||||
|
||||
const val START_KEY = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
const val END_KEY = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
const val START_D_TAG = ""
|
||||
const val END_D_TAG = "\uFFFF\uFFFF\uFFFF\uFFFF"
|
||||
|
||||
fun kindStart(
|
||||
kind: Int,
|
||||
pubKey: HexKey,
|
||||
) = Address(kind, pubKey, "")
|
||||
|
||||
fun kindEnd(
|
||||
kind: Int,
|
||||
pubKey: HexKey,
|
||||
) = Address(kind, pubKey, END_D_TAG)
|
||||
|
||||
fun kindStart(kind: Int) = kindStart(kind, START_KEY)
|
||||
|
||||
fun kindEnd(kind: Int) = kindEnd(kind, END_KEY)
|
||||
|
||||
val ACCEPT_ALL_FILTER = CacheCollectors.BiFilter<Address, AddressableNote> { key, note -> true }
|
||||
|
||||
fun LargeSoftCache<Address, AddressableNote>.filter(
|
||||
kind: Int,
|
||||
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
|
||||
): List<AddressableNote> = filter(kindStart(kind), kindEnd(kind), consumer)
|
||||
|
||||
fun LargeSoftCache<Address, AddressableNote>.filter(
|
||||
kinds: List<Int>,
|
||||
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
|
||||
): List<AddressableNote> {
|
||||
val set = mutableSetOf<AddressableNote>()
|
||||
kinds.forEach {
|
||||
set.addAll(filter(kindStart(it), kindEnd(it), consumer))
|
||||
}
|
||||
return set.toList()
|
||||
}
|
||||
|
||||
fun LargeSoftCache<Address, AddressableNote>.filter(
|
||||
kind: Int,
|
||||
pubKey: HexKey,
|
||||
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
|
||||
): List<AddressableNote> = filter(kindStart(kind, pubKey), kindEnd(kind, pubKey), consumer)
|
||||
|
||||
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
|
||||
kind: Int,
|
||||
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
|
||||
): Set<AddressableNote> = filterIntoSet(kindStart(kind), kindEnd(kind), consumer)
|
||||
|
||||
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
|
||||
kinds: List<Int>,
|
||||
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
|
||||
): Set<AddressableNote> {
|
||||
val set = mutableSetOf<AddressableNote>()
|
||||
kinds.forEach {
|
||||
set.addAll(filterIntoSet(kindStart(it), kindEnd(it), consumer))
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
|
||||
kind: Int,
|
||||
pubKey: HexKey,
|
||||
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
|
||||
): Set<AddressableNote> = filterIntoSet(kindStart(kind, pubKey), kindEnd(kind, pubKey), consumer)
|
||||
|
||||
fun <R> LargeSoftCache<Address, AddressableNote>.mapNotNullIntoSet(
|
||||
kind: Int,
|
||||
consumer: CacheCollectors.BiMapper<Address, AddressableNote, R?>,
|
||||
): Set<R> = mapNotNullIntoSet(kindStart(kind), kindEnd(kind), consumer)
|
||||
|
||||
fun <R> LargeSoftCache<Address, AddressableNote>.mapNotNullIntoSet(
|
||||
kind: Int,
|
||||
pubKey: HexKey,
|
||||
consumer: CacheCollectors.BiMapper<Address, AddressableNote, R?>,
|
||||
): Set<R> = mapNotNullIntoSet(kindStart(kind, pubKey), kindEnd(kind, pubKey), consumer)
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
@@ -50,36 +49,37 @@ import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.checkSignature
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.tagValueContains
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.checkSignature
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.verify
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.forEachTaggedEventId
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers
|
||||
import com.vitorpamplona.quartz.nip01Core.verify
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationState
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex
|
||||
@@ -184,9 +184,9 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import com.vitorpamplona.quartz.utils.LargeSoftCache
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -298,18 +298,19 @@ object LocalCache : ILocalCache {
|
||||
observablesByKindAndAuthor[event.kind]?.get(event.pubKey)?.updateIfMatches(event)
|
||||
}
|
||||
|
||||
fun checkGetOrCreateUser(key: String): User? {
|
||||
if (isValidHex(key)) {
|
||||
return getOrCreateUser(key)
|
||||
}
|
||||
return null
|
||||
}
|
||||
fun checkGetOrCreateUser(key: String): User? = runCatching { getOrCreateUser(key) }.getOrNull()
|
||||
|
||||
fun load(keys: List<String>): List<User> = keys.mapNotNull(::checkGetOrCreateUser)
|
||||
|
||||
fun load(keys: Set<String>): Set<User> = keys.mapNotNullTo(mutableSetOf(), ::checkGetOrCreateUser)
|
||||
|
||||
fun getOrCreateUser(key: HexKey): User {
|
||||
require(isValidHex(key = key)) { "$key is not a valid hex" }
|
||||
|
||||
return users.getOrCreate(key) {
|
||||
User(it)
|
||||
val nip65RelayListNote = getOrCreateAddressableNoteInternal(AdvertisedRelayListEvent.createAddress(key))
|
||||
val dmRelayListNote = getOrCreateAddressableNoteInternal(ChatMessageRelayListEvent.createAddress(key))
|
||||
User(it, nip65RelayListNote, dmRelayListNote)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,11 +414,8 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
|
||||
private fun isValidHex(key: String): Boolean {
|
||||
if (key.isBlank()) return false
|
||||
if (key.length != 64) return false
|
||||
if (key.contains(":")) return false
|
||||
|
||||
return Hex.isHex(key)
|
||||
return Hex.isHex64(key)
|
||||
}
|
||||
|
||||
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? =
|
||||
@@ -698,7 +696,7 @@ object LocalCache : ILocalCache {
|
||||
if (isVerified || justVerify(event)) {
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
if (event.createdAt > (note.createdAt() ?: 0L)) {
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
refreshNewNoteObservers(note)
|
||||
@@ -741,7 +739,7 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
|
||||
if (isVerified || justVerify(event)) {
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
if (event.createdAt > (note.createdAt() ?: 0L)) {
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
@@ -843,7 +841,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
if (note.event?.id == event.id) return false
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0) && (isVerified || justVerify(event))) {
|
||||
if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
val channel = getOrCreateLiveChannel(note.address)
|
||||
@@ -1042,13 +1040,13 @@ object LocalCache : ILocalCache {
|
||||
event: VideoNormalEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
) = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
private fun consume(
|
||||
event: VideoShortEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
) = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: StatusEvent,
|
||||
@@ -1071,7 +1069,7 @@ object LocalCache : ILocalCache {
|
||||
// Already processed this event.
|
||||
if (note.event?.id == event.id) return false
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0) && (isVerified || justVerify(event))) {
|
||||
if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
author.flowSet?.statuses?.invalidateData()
|
||||
@@ -1085,7 +1083,7 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: RelationshipStatusEvent,
|
||||
event: ContactCardEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
@@ -1194,7 +1192,7 @@ object LocalCache : ILocalCache {
|
||||
// Already processed this event.
|
||||
if (replaceableNote.event?.id == event.id) return isVerified
|
||||
|
||||
if (event.createdAt > (replaceableNote.createdAt() ?: 0) && (isVerified || justVerify(event))) {
|
||||
if (event.createdAt > (replaceableNote.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
|
||||
// clear index from previous tags
|
||||
replaceableNote.replyTo?.forEach {
|
||||
it.removeNote(replaceableNote)
|
||||
@@ -1255,7 +1253,7 @@ object LocalCache : ILocalCache {
|
||||
val deleteNoteEvent = deleteNote.event
|
||||
if (deleteNoteEvent is AddressableEvent) {
|
||||
val addressableNote = getAddressableNoteIfExists(deleteNoteEvent.addressTag())
|
||||
if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0) <= event.createdAt) {
|
||||
if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0L) <= event.createdAt) {
|
||||
// Counts the replies
|
||||
deleteNote(addressableNote)
|
||||
|
||||
@@ -1277,7 +1275,7 @@ object LocalCache : ILocalCache {
|
||||
.mapNotNull { getAddressableNoteIfExists(it) }
|
||||
.forEach { deleteNote ->
|
||||
// must be the same author
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) <= event.createdAt) {
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0L) <= event.createdAt) {
|
||||
// Counts the replies
|
||||
deleteNote(deleteNote)
|
||||
|
||||
@@ -1588,7 +1586,7 @@ object LocalCache : ILocalCache {
|
||||
val isVerified =
|
||||
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
||||
if (wasVerified || justVerify(event)) {
|
||||
oldChannel.updateChannelInfo(author, event)
|
||||
oldChannel.updateChannelInfo(author, event, note)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -1840,15 +1838,15 @@ object LocalCache : ILocalCache {
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
var isVerified =
|
||||
val isVerified =
|
||||
try {
|
||||
val cachePath = Amethyst.instance.nip95cache
|
||||
cachePath.mkdirs()
|
||||
val file = File(cachePath, event.id)
|
||||
if (!file.exists() && (wasVerified || justVerify(event))) {
|
||||
val stream = FileOutputStream(file)
|
||||
stream.write(event.decode())
|
||||
stream.close()
|
||||
FileOutputStream(file).use { stream ->
|
||||
stream.write(event.decode())
|
||||
}
|
||||
Log.i(
|
||||
"FileStorageEvent",
|
||||
"NIP95 File received from $relay and saved to disk as $file",
|
||||
@@ -1968,7 +1966,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
requestNote?.let { request -> zappedNote?.addZapPayment(request, note) }
|
||||
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
responseCallback(event)
|
||||
}
|
||||
|
||||
@@ -2132,7 +2130,9 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
|
||||
fun getPeopleListNotesFor(user: User): List<AddressableNote> = addressables.filter(PeopleListEvent.KIND, user.pubkeyHex)
|
||||
|
||||
fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
|
||||
checkNotInMainThread()
|
||||
|
||||
return addressables
|
||||
@@ -2151,20 +2151,36 @@ object LocalCache : ILocalCache {
|
||||
|
||||
suspend fun findEarliestOtsForNote(
|
||||
note: Note,
|
||||
resolverBuilder: OtsResolverBuilder,
|
||||
otsVerifCache: VerificationStateCache,
|
||||
): Long? {
|
||||
checkNotInMainThread()
|
||||
|
||||
var minTime: Long? = null
|
||||
val time = TimeUtils.now()
|
||||
|
||||
notes.forEach { _, item ->
|
||||
val noteEvent = item.event
|
||||
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) {
|
||||
(Amethyst.instance.otsVerifCache.cacheVerify(noteEvent, resolverBuilder) as? VerificationState.Verified)?.verifiedTime?.let { stampedTime ->
|
||||
if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) {
|
||||
minTime = stampedTime
|
||||
val candidates =
|
||||
notes.mapNotNull { _, item ->
|
||||
val noteEvent = item.event
|
||||
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) {
|
||||
val cachedTime = (otsVerifCache.justCache(noteEvent) as? VerificationState.Verified)?.verifiedTime
|
||||
if (cachedTime != null) {
|
||||
if (minTime == null || cachedTime < (minTime ?: Long.MAX_VALUE)) {
|
||||
minTime = cachedTime
|
||||
}
|
||||
null
|
||||
} else {
|
||||
// tries to verify again
|
||||
noteEvent
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
candidates.forEach { noteEvent ->
|
||||
(otsVerifCache.cacheVerify(noteEvent) as? VerificationState.Verified)?.verifiedTime?.let { stampedTime ->
|
||||
if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) {
|
||||
minTime = stampedTime
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2176,7 +2192,7 @@ object LocalCache : ILocalCache {
|
||||
|
||||
fun cachedModificationEventsForNote(note: Note): List<Note>? = modificationCache[note.idHex]
|
||||
|
||||
suspend fun findLatestModificationForNote(note: Note): List<Note> {
|
||||
fun findLatestModificationForNote(note: Note): List<Note> {
|
||||
checkNotInMainThread()
|
||||
|
||||
val noteAuthor = note.author ?: return emptyList()
|
||||
@@ -2201,9 +2217,17 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
|
||||
fun cleanMemory() {
|
||||
Log.d("LargeCache", "Notes cleanup started. Current size: ${notes.size()}")
|
||||
notes.cleanUp()
|
||||
Log.d("LargeCache", "Notes cleanup completed. Remaining size: ${notes.size()}")
|
||||
|
||||
Log.d("LargeCache", "Addressables cleanup started. Current size: ${addressables.size()}")
|
||||
addressables.cleanUp()
|
||||
Log.d("LargeCache", "Addressables cleanup completed. Remaining size: ${addressables.size()}")
|
||||
|
||||
Log.d("LargeCache", "Users cleanup started. Current size: ${users.size()}")
|
||||
users.cleanUp()
|
||||
Log.d("LargeCache", "Users cleanup completed. Remaining size: ${users.size()}")
|
||||
}
|
||||
|
||||
fun cleanObservers() {
|
||||
@@ -2330,9 +2354,9 @@ object LocalCache : ILocalCache {
|
||||
val children =
|
||||
if (noteEvent is WrappedEvent) {
|
||||
noteEvent.host?.id?.let {
|
||||
getNoteIfExists(it)?.let {
|
||||
removeFromCache(it)
|
||||
it.removeAllChildNotes()
|
||||
getNoteIfExists(it)?.let { it2 ->
|
||||
removeFromCache(it2)
|
||||
it2.removeAllChildNotes()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -2608,7 +2632,7 @@ object LocalCache : ILocalCache {
|
||||
if (isDebug) {
|
||||
Log.d("LocalCache", "Updating ${relay.url.url} with a Deletion Event ${event.id} ${deletionEvent.id} because of ${event.toJson()} with ${deletionEvent.toJson()}")
|
||||
}
|
||||
relay.send(deletionEvent)
|
||||
relay.sendIfConnected(EventCmd(deletionEvent))
|
||||
note.addRelay(relay.url)
|
||||
}
|
||||
}
|
||||
@@ -2626,7 +2650,7 @@ object LocalCache : ILocalCache {
|
||||
Log.d("LocalCache", "Updating ${relay.url.url} with a new version of ${event.kind} ${event.id} to ${existingEvent.id}")
|
||||
}
|
||||
|
||||
relay.send(existingEvent)
|
||||
relay.sendIfConnected(EventCmd(existingEvent))
|
||||
// only send once.
|
||||
note.addRelay(relay.url)
|
||||
}
|
||||
@@ -2812,7 +2836,7 @@ object LocalCache : ILocalCache {
|
||||
is PeopleListEvent -> consume(event, relay, wasVerified)
|
||||
is PollNoteEvent -> consume(event, relay, wasVerified)
|
||||
is ReactionEvent -> consume(event, relay, wasVerified)
|
||||
is RelationshipStatusEvent -> consume(event, relay, wasVerified)
|
||||
is ContactCardEvent -> consume(event, relay, wasVerified)
|
||||
is RelaySetEvent -> consume(event, relay, wasVerified)
|
||||
is ReportEvent -> consume(event, relay, wasVerified)
|
||||
is RepostEvent -> consume(event, relay, wasVerified)
|
||||
@@ -2882,10 +2906,10 @@ class LocalCacheFlow {
|
||||
val deletedEventBundles = _deletedEventBundles.asSharedFlow() // read-only public view
|
||||
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledInsert<Note>(1000, Dispatchers.Default)
|
||||
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
|
||||
|
||||
// Refreshes observers in batches.
|
||||
private val bundler2 = BundledInsert<Note>(1000, Dispatchers.Default)
|
||||
private val bundler2 = BundledInsert<Note>(1000, Dispatchers.IO)
|
||||
|
||||
fun newNote(newNote: Note) {
|
||||
bundler.invalidateList(newNote) { bundledNewNotes ->
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
@@ -160,7 +161,7 @@ fun parseResourceBundleOrNull(json: String): FhirElementDatabase? {
|
||||
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
|
||||
return try {
|
||||
val resource = mapper.readValue(json, Resource::class.java)
|
||||
val resource = mapper.readValue<Resource>(json)
|
||||
|
||||
val db =
|
||||
when (resource) {
|
||||
|
||||
@@ -31,18 +31,17 @@ import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue
|
||||
import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward
|
||||
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.anyHashTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
@@ -72,7 +71,9 @@ import com.vitorpamplona.quartz.utils.containsAny
|
||||
import com.vitorpamplona.quartz.utils.launchAndWaitAll
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import java.math.BigDecimal
|
||||
|
||||
@@ -101,14 +102,6 @@ class AddressableNote(
|
||||
|
||||
fun dTag(): String = address.dTag
|
||||
|
||||
override fun wasOrShouldBeDeletedBy(
|
||||
deletionEvents: Set<HexKey>,
|
||||
deletionAddressables: Set<Address>,
|
||||
): Boolean {
|
||||
val thisEvent = event
|
||||
return deletionAddressables.contains(address) || (thisEvent != null && deletionEvents.contains(thisEvent.id))
|
||||
}
|
||||
|
||||
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrl())
|
||||
|
||||
fun toATag() = ATag(address, relayHintUrl())
|
||||
@@ -755,7 +748,7 @@ open class Note(
|
||||
val dayAgo = TimeUtils.oneDayAgo()
|
||||
return reports.isNotEmpty() ||
|
||||
(
|
||||
author?.reports?.any { it.value.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null }
|
||||
author?.reports?.any { it.value.firstOrNull { (it.createdAt() ?: 0L) > dayAgo } != null }
|
||||
?: false
|
||||
)
|
||||
}
|
||||
@@ -787,14 +780,14 @@ open class Note(
|
||||
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
||||
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
|
||||
return boosts.any {
|
||||
it.author == loggedIn && (it.createdAt() ?: 0) > fiveMinsAgo
|
||||
it.author == loggedIn && (it.createdAt() ?: 0L) > fiveMinsAgo
|
||||
}
|
||||
}
|
||||
|
||||
fun hasBoostedInTheLast5Minutes(loggedIn: HexKey): Boolean {
|
||||
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
|
||||
return boosts.any {
|
||||
(it.createdAt() ?: 0) > fiveMinsAgo && it.author?.pubkeyHex == loggedIn
|
||||
(it.createdAt() ?: 0L) > fiveMinsAgo && it.author?.pubkeyHex == loggedIn
|
||||
}
|
||||
}
|
||||
|
||||
@@ -911,14 +904,6 @@ open class Note(
|
||||
}
|
||||
}
|
||||
|
||||
open fun wasOrShouldBeDeletedBy(
|
||||
deletionEvents: Set<HexKey>,
|
||||
deletionAddressables: Set<Address>,
|
||||
): Boolean {
|
||||
val thisEvent = event
|
||||
return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.address()))
|
||||
}
|
||||
|
||||
fun toETag(): ETag {
|
||||
val noteEvent = event
|
||||
return if (noteEvent != null) {
|
||||
@@ -1011,3 +996,38 @@ class NoteBundledRefresherFlow(
|
||||
class NoteState(
|
||||
val note: Note,
|
||||
)
|
||||
|
||||
fun List<AddressableNote>.eventIdSet() = mapNotNullTo(mutableSetOf<HexKey>()) { it.event?.id }
|
||||
|
||||
fun <T : Event> Array<NoteState>.events() = mapNotNull { it.note.event as? T }
|
||||
|
||||
fun <T : Event> List<AddressableNote>.events() = mapNotNull { it.event as? T }
|
||||
|
||||
fun <T : Event> List<AddressableNote>.updateFlow(): Flow<List<T>> =
|
||||
if (this.isEmpty()) {
|
||||
MutableStateFlow(emptyList())
|
||||
} else {
|
||||
combine(
|
||||
flows = this.map { it.flow().metadata.stateFlow },
|
||||
) {
|
||||
it.events()
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun <T> Iterable<Note>.anyEvent(predicate: (T) -> Boolean): Boolean {
|
||||
if (this is Collection && isEmpty()) return false
|
||||
for (note in this) {
|
||||
val noteEvent = note.event as? T
|
||||
if (noteEvent != null && predicate(noteEvent)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public inline fun Iterable<Note>.anyNotNullEvent(predicate: (Event) -> Boolean): Boolean {
|
||||
if (this is Collection && isEmpty()) return false
|
||||
for (note in this) {
|
||||
val noteEvent = note.event
|
||||
if (noteEvent != null && predicate(noteEvent)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@@ -22,9 +22,11 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
data class Settings(
|
||||
@Serializable
|
||||
data class UiSettings(
|
||||
val theme: ThemeType = ThemeType.SYSTEM,
|
||||
val preferredLanguage: String? = null,
|
||||
val automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
class UiSettingsFlow(
|
||||
val theme: MutableStateFlow<ThemeType> = MutableStateFlow(ThemeType.SYSTEM),
|
||||
val preferredLanguage: MutableStateFlow<String?> = MutableStateFlow(null),
|
||||
val automaticallyShowImages: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
|
||||
val automaticallyStartPlayback: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
|
||||
val automaticallyShowUrlPreview: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
|
||||
val automaticallyHideNavigationBars: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||
val automaticallyShowProfilePictures: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
|
||||
val dontShowPushNotificationSelector: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val dontAskForNotificationPermissions: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val featureSet: MutableStateFlow<FeatureSetType> = MutableStateFlow(FeatureSetType.SIMPLIFIED),
|
||||
val gallerySet: MutableStateFlow<ProfileGalleryType> = MutableStateFlow(ProfileGalleryType.CLASSIC),
|
||||
) {
|
||||
// emits at every change in any of the propertyes.
|
||||
val propertyWatchFlow =
|
||||
combine(
|
||||
listOf(
|
||||
theme,
|
||||
preferredLanguage,
|
||||
automaticallyShowImages,
|
||||
automaticallyStartPlayback,
|
||||
automaticallyShowUrlPreview,
|
||||
automaticallyHideNavigationBars,
|
||||
automaticallyShowProfilePictures,
|
||||
dontShowPushNotificationSelector,
|
||||
dontAskForNotificationPermissions,
|
||||
featureSet,
|
||||
gallerySet,
|
||||
),
|
||||
) { flows ->
|
||||
UiSettings(
|
||||
flows[0] as ThemeType,
|
||||
flows[1] as String?,
|
||||
flows[2] as ConnectivityType,
|
||||
flows[3] as ConnectivityType,
|
||||
flows[4] as ConnectivityType,
|
||||
flows[5] as BooleanType,
|
||||
flows[6] as ConnectivityType,
|
||||
flows[7] as Boolean,
|
||||
flows[8] as Boolean,
|
||||
flows[9] as FeatureSetType,
|
||||
flows[10] as ProfileGalleryType,
|
||||
)
|
||||
}
|
||||
|
||||
fun toSettings(): UiSettings =
|
||||
UiSettings(
|
||||
theme.value,
|
||||
preferredLanguage.value,
|
||||
automaticallyShowImages.value,
|
||||
automaticallyStartPlayback.value,
|
||||
automaticallyShowUrlPreview.value,
|
||||
automaticallyHideNavigationBars.value,
|
||||
automaticallyShowProfilePictures.value,
|
||||
dontShowPushNotificationSelector.value,
|
||||
dontAskForNotificationPermissions.value,
|
||||
featureSet.value,
|
||||
gallerySet.value,
|
||||
)
|
||||
|
||||
fun update(torSettings: UiSettings): Boolean {
|
||||
var any = false
|
||||
|
||||
if (theme.value != torSettings.theme) {
|
||||
theme.tryEmit(torSettings.theme)
|
||||
any = true
|
||||
}
|
||||
if (preferredLanguage.value != torSettings.preferredLanguage) {
|
||||
preferredLanguage.tryEmit(torSettings.preferredLanguage)
|
||||
any = true
|
||||
}
|
||||
if (automaticallyShowImages.value != torSettings.automaticallyShowImages) {
|
||||
automaticallyShowImages.tryEmit(torSettings.automaticallyShowImages)
|
||||
any = true
|
||||
}
|
||||
if (automaticallyStartPlayback.value != torSettings.automaticallyStartPlayback) {
|
||||
automaticallyStartPlayback.tryEmit(torSettings.automaticallyStartPlayback)
|
||||
any = true
|
||||
}
|
||||
if (automaticallyShowUrlPreview.value != torSettings.automaticallyShowUrlPreview) {
|
||||
automaticallyShowUrlPreview.tryEmit(torSettings.automaticallyShowUrlPreview)
|
||||
any = true
|
||||
}
|
||||
if (automaticallyHideNavigationBars.value != torSettings.automaticallyHideNavigationBars) {
|
||||
automaticallyHideNavigationBars.tryEmit(torSettings.automaticallyHideNavigationBars)
|
||||
any = true
|
||||
}
|
||||
if (automaticallyShowProfilePictures.value != torSettings.automaticallyShowProfilePictures) {
|
||||
automaticallyShowProfilePictures.tryEmit(torSettings.automaticallyShowProfilePictures)
|
||||
any = true
|
||||
}
|
||||
if (dontShowPushNotificationSelector.value != torSettings.dontShowPushNotificationSelector) {
|
||||
dontShowPushNotificationSelector.tryEmit(torSettings.dontShowPushNotificationSelector)
|
||||
any = true
|
||||
}
|
||||
if (dontAskForNotificationPermissions.value != torSettings.dontAskForNotificationPermissions) {
|
||||
dontAskForNotificationPermissions.tryEmit(torSettings.dontAskForNotificationPermissions)
|
||||
any = true
|
||||
}
|
||||
if (featureSet.value != torSettings.featureSet) {
|
||||
featureSet.tryEmit(torSettings.featureSet)
|
||||
any = true
|
||||
}
|
||||
if (gallerySet.value != torSettings.gallerySet) {
|
||||
gallerySet.tryEmit(torSettings.gallerySet)
|
||||
any = true
|
||||
}
|
||||
|
||||
return any
|
||||
}
|
||||
|
||||
fun dontShowPushNotificationSelector() {
|
||||
if (!dontShowPushNotificationSelector.value) {
|
||||
dontShowPushNotificationSelector.tryEmit(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun dontAskForNotificationPermissions() {
|
||||
if (!dontAskForNotificationPermissions.value) {
|
||||
dontAskForNotificationPermissions.tryEmit(true)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun build(torSettings: 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.quartz.lightning.Lud06
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -33,7 +34,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
@@ -50,6 +50,8 @@ import java.math.BigDecimal
|
||||
@Stable
|
||||
class User(
|
||||
val pubkeyHex: String,
|
||||
val nip65RelayListNote: Note,
|
||||
val dmRelayListNote: Note,
|
||||
) {
|
||||
var info: UserMetadata? = null
|
||||
|
||||
@@ -72,9 +74,9 @@ class User(
|
||||
|
||||
fun pubkeyDisplayHex() = pubkeyNpub().toShortDisplay()
|
||||
|
||||
fun dmInboxRelayList() = (LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(pubkeyHex))?.event as? ChatMessageRelayListEvent)
|
||||
fun dmInboxRelayList() = dmRelayListNote.event as? ChatMessageRelayListEvent
|
||||
|
||||
fun authorRelayList() = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)
|
||||
fun authorRelayList() = nip65RelayListNote.event as? AdvertisedRelayListEvent
|
||||
|
||||
fun toNProfile() = NProfile.create(pubkeyHex, relayHints())
|
||||
|
||||
@@ -139,8 +141,6 @@ class User(
|
||||
?.followers
|
||||
?.invalidateData()
|
||||
}
|
||||
|
||||
flowSet?.relays?.invalidateData()
|
||||
}
|
||||
|
||||
fun addReport(note: Note) {
|
||||
@@ -227,7 +227,7 @@ class User(
|
||||
here.counter++
|
||||
}
|
||||
|
||||
flowSet?.relayInfo?.invalidateData()
|
||||
flowSet?.usedRelays?.invalidateData()
|
||||
}
|
||||
|
||||
fun updateUserInfo(
|
||||
@@ -339,20 +339,18 @@ class UserFlowSet(
|
||||
// Observers line up here.
|
||||
val metadata = UserBundledRefresherFlow(u)
|
||||
val follows = UserBundledRefresherFlow(u)
|
||||
val relays = UserBundledRefresherFlow(u)
|
||||
val followers = UserBundledRefresherFlow(u)
|
||||
val reports = UserBundledRefresherFlow(u)
|
||||
val relayInfo = UserBundledRefresherFlow(u)
|
||||
val usedRelays = UserBundledRefresherFlow(u)
|
||||
val zaps = UserBundledRefresherFlow(u)
|
||||
val statuses = UserBundledRefresherFlow(u)
|
||||
|
||||
fun isInUse(): Boolean =
|
||||
metadata.hasObservers() ||
|
||||
relays.hasObservers() ||
|
||||
follows.hasObservers() ||
|
||||
followers.hasObservers() ||
|
||||
reports.hasObservers() ||
|
||||
relayInfo.hasObservers() ||
|
||||
usedRelays.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
statuses.hasObservers()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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.accountsCache
|
||||
|
||||
import android.content.ContentResolver
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
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.nip55AndroidSigner.client.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class AccountCacheState(
|
||||
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||
val contentResolverFn: () -> ContentResolver,
|
||||
val otsResolverBuilder: OtsResolverBuilder,
|
||||
val cache: LocalCache,
|
||||
val client: INostrClient,
|
||||
) {
|
||||
val accounts = MutableStateFlow<Map<HexKey, Account>>(emptyMap())
|
||||
|
||||
fun removeAccount(pubkey: HexKey) {
|
||||
accounts.update { existingAccounts ->
|
||||
val oldValue = existingAccounts[pubkey]
|
||||
oldValue?.scope?.cancel()
|
||||
existingAccounts.minus(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAccount(accountSettings: AccountSettings): Account =
|
||||
loadAccount(
|
||||
signer =
|
||||
if (accountSettings.keyPair.privKey != null) {
|
||||
NostrSignerInternal(accountSettings.keyPair)
|
||||
} else {
|
||||
when (val packageName = accountSettings.externalSignerPackageName) {
|
||||
null -> NostrSignerInternal(accountSettings.keyPair)
|
||||
else ->
|
||||
NostrSignerExternal(
|
||||
pubKey = accountSettings.keyPair.pubKey.toHexKey(),
|
||||
packageName = packageName,
|
||||
contentResolver = contentResolverFn(),
|
||||
)
|
||||
}
|
||||
},
|
||||
accountSettings = accountSettings,
|
||||
)
|
||||
|
||||
fun loadAccount(
|
||||
signer: NostrSigner,
|
||||
accountSettings: AccountSettings,
|
||||
): Account {
|
||||
val cached = accounts.value[signer.pubKey]
|
||||
if (cached != null) return cached
|
||||
|
||||
return Account(
|
||||
settings = accountSettings,
|
||||
signer = signer,
|
||||
geolocationFlow = geolocationFlow,
|
||||
nwcFilterAssembler = nwcFilterAssembler,
|
||||
otsResolverBuilder = otsResolverBuilder,
|
||||
cache = cache,
|
||||
client = client,
|
||||
scope =
|
||||
CoroutineScope(
|
||||
Dispatchers.IO +
|
||||
SupervisorJob() +
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e("AccountCacheState", "Account ${signer.pubKey} caught exception: ${throwable.message}", throwable)
|
||||
},
|
||||
),
|
||||
).also { newAccount ->
|
||||
accounts.update { existingAccounts ->
|
||||
existingAccounts.plus(Pair(signer.pubKey, newAccount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
accounts.update { existingAccounts ->
|
||||
existingAccounts.forEach {
|
||||
it.value.scope.cancel()
|
||||
}
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,15 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.edits
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
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.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -48,13 +47,14 @@ class PrivateStorageRelayListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val privateOutboxListNote = cache.getOrCreateAddressableNote(getPrivateOutboxRelayListAddress())
|
||||
|
||||
fun getPrivateOutboxRelayListAddress() = PrivateOutboxRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getPrivateOutboxRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getPrivateOutboxRelayListAddress())
|
||||
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = privateOutboxListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = getPrivateOutboxRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
|
||||
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = privateOutboxListNote.event as? PrivateOutboxRelayListEvent
|
||||
|
||||
suspend fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList
|
||||
@@ -65,8 +65,8 @@ class PrivateStorageRelayListState(
|
||||
getPrivateOutboxRelayListFlow()
|
||||
.map { normalizePrivateOutboxRelayListWithBackup(it.note) }
|
||||
.onStart {
|
||||
emit(normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
emit(normalizePrivateOutboxRelayListWithBackup(privateOutboxListNote))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -99,7 +99,7 @@ class PrivateStorageRelayListState(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
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}")
|
||||
|
||||
@@ -20,15 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.emphChat
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
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.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -49,13 +48,14 @@ class EphemeralChatListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val ephemeralChatListNote = cache.getOrCreateAddressableNote(getEphemeralChatListAddress())
|
||||
|
||||
fun getEphemeralChatListAddress() = EphemeralChatListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getEphemeralChatListNote(): AddressableNote = cache.getOrCreateAddressableNote(getEphemeralChatListAddress())
|
||||
fun getEphemeralChatListFlow(): StateFlow<NoteState> = ephemeralChatListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getEphemeralChatListFlow(): StateFlow<NoteState> = getEphemeralChatListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getEphemeralChatList(): EphemeralChatListEvent? = getEphemeralChatListNote().event as? EphemeralChatListEvent
|
||||
fun getEphemeralChatList(): EphemeralChatListEvent? = ephemeralChatListNote.event as? EphemeralChatListEvent
|
||||
|
||||
suspend fun ephemeralChatListWithBackup(note: Note): Set<RoomId> {
|
||||
val event = note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList
|
||||
@@ -68,8 +68,8 @@ class EphemeralChatListState(
|
||||
.transformLatest { noteState ->
|
||||
emit(ephemeralChatListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(ephemeralChatListWithBackup(getEphemeralChatListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
emit(ephemeralChatListWithBackup(ephemeralChatListNote))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -117,7 +117,7 @@ class EphemeralChatListState(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "EphemeralChatList Collector Start")
|
||||
getEphemeralChatListFlow().collect { noteState ->
|
||||
Log.d("AccountRegisterObservers", "EphemeralChatList List for ${signer.pubKey}")
|
||||
|
||||
@@ -43,7 +43,7 @@ class LocalRelayListState(
|
||||
val flow =
|
||||
settings.localRelayServers
|
||||
.map { normalizeLocalRelayListWithBackup(it) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -43,7 +43,7 @@ class AccountHomeRelayState(
|
||||
local.flow,
|
||||
) { nip65Outbox, privateOutBox, localRelays ->
|
||||
nip65Outbox + privateOutBox + localRelays
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -46,7 +46,7 @@ class AccountOutboxRelayState(
|
||||
broadcast.flow,
|
||||
) { nip65Outbox, privateOutBox, localRelays, broadcastRelays ->
|
||||
nip65Outbox + privateOutBox + localRelays + broadcastRelays
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -40,7 +40,7 @@ class NotificationInboxRelayState(
|
||||
localRelayList.flow,
|
||||
) { nip65Inbox, localRelays ->
|
||||
nip65Inbox + localRelays
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -20,13 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip01UserMetadata
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -40,13 +39,14 @@ class UserMetadataState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val user = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun getUserMetadataUser(): User = cache.getOrCreateUser(signer.pubKey)
|
||||
fun getUserMetadataFlow(): StateFlow<UserState> = user.flow().metadata.stateFlow
|
||||
|
||||
fun getUserMetadataFlow(): StateFlow<UserState> = getUserMetadataUser().flow().metadata.stateFlow
|
||||
|
||||
fun getUserMetadataEvent(): MetadataEvent? = getUserMetadataUser().latestMetadata
|
||||
fun getUserMetadataEvent(): MetadataEvent? = user.latestMetadata
|
||||
|
||||
suspend fun sendNewUserMetadata(
|
||||
name: String? = null,
|
||||
@@ -112,7 +112,7 @@ class UserMetadataState(
|
||||
}
|
||||
|
||||
// saves contact list for the next time.
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Kind 0 Collector Start")
|
||||
getUserMetadataFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 0 ${it.user.toBestDisplayName()}")
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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.nip02FollowLists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
class DeclaredFollowsPerOutboxRelay(
|
||||
kind3Follows: Kind3FollowListState,
|
||||
val cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
val calculator = OutboxRelayLoader(true)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||
kind3Follows.flow
|
||||
.transformLatest {
|
||||
emitAll(
|
||||
calculator.toAuthorsPerRelayFlow(it.authors, cache) { it },
|
||||
)
|
||||
}.onStart {
|
||||
emit(
|
||||
calculator.authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it },
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyMap(),
|
||||
)
|
||||
}
|
||||
@@ -20,18 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -44,49 +40,27 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlin.collections.flatten
|
||||
|
||||
class FollowListOutboxOrProxyRelays(
|
||||
kind3Follows: FollowListState,
|
||||
kind3Follows: Kind3FollowListState,
|
||||
blockedRelayList: BlockedRelayListState,
|
||||
proxyRelayList: ProxyRelayListState,
|
||||
val cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey)
|
||||
|
||||
fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey))
|
||||
|
||||
fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow<NoteState> = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow
|
||||
|
||||
fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent
|
||||
|
||||
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
|
||||
|
||||
fun combineAllFlows(flows: List<StateFlow<NoteState>>): Flow<Set<NormalizedRelayUrl>> =
|
||||
combine(flows) { relayListNotes: Array<NoteState> ->
|
||||
relayListNotes
|
||||
.mapNotNull {
|
||||
(it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()
|
||||
}.flatten()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val outboxRelayFlow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
kind3Follows.flow
|
||||
.transformLatest {
|
||||
emitAll(combineAllFlows(allRelayListFlows(it.authors)))
|
||||
.transformLatest { follows ->
|
||||
emitAll(
|
||||
OutboxRelayLoader(true).toAuthorsPerRelayFlow(follows.authors, cache) { it.keys },
|
||||
)
|
||||
}.onStart {
|
||||
emit(
|
||||
kind3Follows.flow.value.authors
|
||||
.mapNotNull {
|
||||
getNIP65RelayList(it)?.writeRelaysNorm()
|
||||
}.flatten()
|
||||
.toSet(),
|
||||
OutboxRelayLoader(true).authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it.keys },
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -98,7 +72,7 @@ class FollowListOutboxOrProxyRelays(
|
||||
followList.minus(blockedRelays)
|
||||
}.onStart {
|
||||
emit(outboxRelayFlow.value.minus(blockedRelayList.flow.value.toSet()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -120,7 +94,7 @@ class FollowListOutboxOrProxyRelays(
|
||||
outboxRelayMinusBlockedFlow.value
|
||||
},
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -134,7 +108,7 @@ class FollowListOutboxOrProxyRelays(
|
||||
relayList.mapTo(mutableSetOf()) { it.url }
|
||||
}.onStart {
|
||||
emit(flow.value.mapTo(mutableSetOf()) { it.url })
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 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.nip02FollowLists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
class FollowListReusedOutboxOrProxyRelays(
|
||||
kind3Follows: Kind3FollowListState,
|
||||
blockedRelayList: BlockedRelayListState,
|
||||
proxyRelayList: ProxyRelayListState,
|
||||
val cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val outboxRelayFlow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
kind3Follows.flow
|
||||
.transformLatest { follows ->
|
||||
emitAll(
|
||||
OutboxRelayLoader(true).toAuthorsPerRelayFlow(follows.authors, cache) { authorsPerRelay ->
|
||||
authorsPerRelay
|
||||
.mapNotNull {
|
||||
if (it.value.size > 1) {
|
||||
it.key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toSet()
|
||||
},
|
||||
)
|
||||
}.onStart {
|
||||
emit(
|
||||
OutboxRelayLoader(true).authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { authorsPerRelay ->
|
||||
authorsPerRelay
|
||||
.mapNotNull {
|
||||
if (it.value.size > 1) {
|
||||
it.key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toSet()
|
||||
},
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
val outboxRelayMinusBlockedFlow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
combine(outboxRelayFlow, blockedRelayList.flow) { followList, blockedRelays ->
|
||||
followList.minus(blockedRelays)
|
||||
}.onStart {
|
||||
emit(outboxRelayFlow.value.minus(blockedRelayList.flow.value.toSet()))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
proxyRelayList.flow
|
||||
.flatMapLatest { proxyRelays ->
|
||||
if (proxyRelays.isEmpty()) {
|
||||
outboxRelayMinusBlockedFlow
|
||||
} else {
|
||||
MutableStateFlow(proxyRelays)
|
||||
}
|
||||
}.onStart {
|
||||
emit(
|
||||
proxyRelayList.flow.value.ifEmpty {
|
||||
outboxRelayMinusBlockedFlow.value
|
||||
},
|
||||
)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flowSet: StateFlow<Set<String>> =
|
||||
flow
|
||||
.map { relayList ->
|
||||
relayList.mapTo(mutableSetOf()) { it.url }
|
||||
}.onStart {
|
||||
emit(flow.value.mapTo(mutableSetOf()) { it.url })
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
@@ -20,20 +20,15 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Constants
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -45,63 +40,27 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlin.collections.ifEmpty
|
||||
|
||||
class FollowsPerOutboxRelay(
|
||||
kind3Follows: FollowListState,
|
||||
kind3Follows: Kind3FollowListState,
|
||||
blockedRelayList: BlockedRelayListState,
|
||||
proxyRelayList: ProxyRelayListState,
|
||||
val cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey)
|
||||
|
||||
fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey))
|
||||
|
||||
fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow<NoteState> = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow
|
||||
|
||||
fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent
|
||||
|
||||
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
|
||||
|
||||
fun combineAllRelayListFlows(flows: List<StateFlow<NoteState>>): Flow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||
combine(flows) { relayListNotes: Array<NoteState> ->
|
||||
mapOfSet {
|
||||
relayListNotes.forEach { noteState ->
|
||||
noteState.note.author?.pubkeyHex?.let { authorHex ->
|
||||
val outboxRelayList =
|
||||
getNIP65RelayList(authorHex)?.writeRelaysNorm()
|
||||
?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null }
|
||||
?: Constants.eventFinderRelays
|
||||
outboxRelayList.forEach { relay ->
|
||||
add(relay, authorHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val outboxPerRelayFlow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||
kind3Follows.flow
|
||||
.transformLatest {
|
||||
emitAll(combineAllRelayListFlows(allRelayListFlows(it.authors)))
|
||||
emitAll(
|
||||
OutboxRelayLoader().toAuthorsPerRelayFlow(it.authors, cache) { it },
|
||||
)
|
||||
}.onStart {
|
||||
emit(
|
||||
mapOfSet {
|
||||
kind3Follows.flow.value.authors.map { authorHex ->
|
||||
val outboxRelayList =
|
||||
getNIP65RelayList(authorHex)?.writeRelaysNorm()
|
||||
?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null }
|
||||
?: Constants.eventFinderRelays
|
||||
outboxRelayList.forEach { relay ->
|
||||
add(relay, authorHex)
|
||||
}
|
||||
}
|
||||
},
|
||||
OutboxRelayLoader().authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it },
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -113,7 +72,7 @@ class FollowsPerOutboxRelay(
|
||||
followList.minus(blockedRelays)
|
||||
}.onStart {
|
||||
emit(outboxPerRelayFlow.value.minus(blockedRelayList.flow.value.toSet()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -137,7 +96,7 @@ class FollowsPerOutboxRelay(
|
||||
} else {
|
||||
emit(proxyRelayList.flow.value.associateWith { kind3Follows.flow.value.authors })
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -20,44 +20,41 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FollowListState(
|
||||
class Kind3FollowListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val user = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun getFollowListUser(): User = cache.getOrCreateUser(signer.pubKey)
|
||||
fun getFollowListFlow(): StateFlow<UserState> = user.flow().follows.stateFlow
|
||||
|
||||
fun getFollowListFlow(): StateFlow<UserState> = getFollowListUser().flow().follows.stateFlow
|
||||
|
||||
fun getFollowListEvent(): ContactListEvent? = getFollowListUser().latestContactList
|
||||
fun getFollowListEvent(): ContactListEvent? = user.latestContactList
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val innerFlow: Flow<Kind3Follows> =
|
||||
@@ -67,7 +64,7 @@ class FollowListState(
|
||||
|
||||
val flow =
|
||||
innerFlow
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -75,20 +72,31 @@ class FollowListState(
|
||||
buildKind3Follows(getFollowListEvent() ?: settings.backupContactList),
|
||||
)
|
||||
|
||||
// Creates a long-term reference for all follows of a user
|
||||
val userList =
|
||||
flow
|
||||
.map { kind3Follows ->
|
||||
kind3Follows.authors.mapNotNull {
|
||||
cache.checkGetOrCreateUser(it)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
// this has priority.
|
||||
flow.value.authors.mapNotNull {
|
||||
cache.checkGetOrCreateUser(it)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
This contains a big OR of everything the user wants to see in the a single feed.
|
||||
*/
|
||||
@Immutable
|
||||
class Kind3Follows(
|
||||
val authors: Set<String> = emptySet(),
|
||||
val authorsPlusMe: Set<String>,
|
||||
val hashtags: Set<String> = emptySet(),
|
||||
val geotags: Set<String> = emptySet(),
|
||||
val communities: Set<String> = emptySet(),
|
||||
) {
|
||||
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
|
||||
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf<String>()) { HashtagId.toScope(it) }
|
||||
}
|
||||
val authors: Set<HexKey> = emptySet(),
|
||||
val authorsPlusMe: Set<HexKey>,
|
||||
)
|
||||
|
||||
fun buildKind3Follows(latestContactList: ContactListEvent?): Kind3Follows {
|
||||
// makes sure the output include only valid p tags
|
||||
@@ -97,19 +105,6 @@ class FollowListState(
|
||||
return Kind3Follows(
|
||||
authors = verifiedFollowingUsers,
|
||||
authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
|
||||
hashtags =
|
||||
latestContactList
|
||||
?.unverifiedFollowTagSet()
|
||||
?.map { it.lowercase() }
|
||||
?.toSet() ?: emptySet(),
|
||||
geotags =
|
||||
latestContactList
|
||||
?.geohashes()
|
||||
?.toSet() ?: emptySet(),
|
||||
communities =
|
||||
latestContactList
|
||||
?.verifiedFollowAddressSet()
|
||||
?.toSet() ?: emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -143,14 +138,14 @@ class FollowListState(
|
||||
|
||||
init {
|
||||
settings.backupContactList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved contacts ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers", "Loading saved ${it.tags.size} contacts")
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
// saves contact list for the next time.
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Kind 3 Collector Start")
|
||||
getFollowListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 3 ${signer.pubKey}")
|
||||
@@ -18,40 +18,42 @@
|
||||
* 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.quartz.nip01Core.relay.client.accessories
|
||||
package com.vitorpamplona.amethyst.model.nip03Timestamp
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class RelayAuthenticator(
|
||||
val client: NostrClient,
|
||||
val scope: CoroutineScope,
|
||||
val authenticate: suspend (challenge: String, relay: IRelayClient) -> Unit,
|
||||
class IncomingOtsEventVerifier(
|
||||
private val otsVerifCache: VerificationStateCache,
|
||||
private val cache: LocalCache,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
private val clientListener =
|
||||
object : IRelayClientListener {
|
||||
override fun onAuth(
|
||||
relay: IRelayClient,
|
||||
challenge: String,
|
||||
) {
|
||||
scope.launch {
|
||||
authenticate(challenge, relay)
|
||||
val verifying =
|
||||
cache.live.newEventBundles
|
||||
.onEach { newNotes ->
|
||||
newNotes.forEach {
|
||||
consume(it)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
null,
|
||||
)
|
||||
|
||||
suspend fun consume(note: Note) {
|
||||
note.event?.let { event ->
|
||||
if (event is OtsEvent) {
|
||||
otsVerifCache.cacheVerify(event)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d("${this.javaClass.simpleName}", "Init, Subscribe")
|
||||
client.subscribe(clientListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
// makes sure to run
|
||||
Log.d("${this.javaClass.simpleName}", "Destroy, Unsubscribe")
|
||||
client.unsubscribe(clientListener)
|
||||
}
|
||||
}
|
||||
@@ -20,25 +20,40 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip03Timestamp
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
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.ots.okhttp.OkHttpOtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Base64
|
||||
|
||||
class OtsState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val otsResolver: OkHttpOtsResolverBuilder,
|
||||
val otsResolver: OtsResolverBuilder,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
var lastTimeItTriedToUpdateAttestations: Long = 0
|
||||
|
||||
fun upgradeAttestationsIfNeeded(onReady: (List<OtsEvent>) -> Unit) {
|
||||
// only tries to upgrade every hour
|
||||
val now = TimeUtils.now()
|
||||
if (now - lastTimeItTriedToUpdateAttestations > TimeUtils.ONE_HOUR) {
|
||||
lastTimeItTriedToUpdateAttestations = now
|
||||
scope.launch {
|
||||
onReady(updateAttestations())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateAttestations(): List<OtsEvent> {
|
||||
Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations")
|
||||
|
||||
@@ -70,7 +85,7 @@ class OtsState(
|
||||
return settings.pendingAttestations.value[id] != null
|
||||
}
|
||||
|
||||
fun timestamp(note: Note) {
|
||||
suspend fun timestamp(note: Note) {
|
||||
if (note.isDraft()) return
|
||||
|
||||
val id = note.event?.id ?: note.idHex
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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.nip03Timestamp
|
||||
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpCalendar
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class TorAwareOkHttpOtsResolverBuilder(
|
||||
val okHttpClient: (url: String) -> OkHttpClient,
|
||||
val isTorActive: (url: String) -> Boolean,
|
||||
val cache: OtsBlockHeightCache,
|
||||
) : OtsResolverBuilder {
|
||||
fun getAPI(usingTor: Boolean) =
|
||||
if (usingTor) {
|
||||
OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL
|
||||
} else {
|
||||
OkHttpBitcoinExplorer.Companion.BLOCKSTREAM_API_URL
|
||||
}
|
||||
|
||||
override fun build(): OtsResolver =
|
||||
OtsResolver(
|
||||
explorer =
|
||||
OkHttpBitcoinExplorer(
|
||||
baseUrl = {
|
||||
getAPI(usingTor = isTorActive(OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL))
|
||||
},
|
||||
client = okHttpClient(OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL),
|
||||
cache = cache,
|
||||
),
|
||||
calendar = OkHttpCalendar(okHttpClient),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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.nip11RelayInfo
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
@Composable
|
||||
fun loadRelayInfo(relay: NormalizedRelayUrl): State<Nip11RelayInformation> = loadRelayInfo(relay, Amethyst.instance.nip11Cache)
|
||||
|
||||
@Composable
|
||||
fun loadRelayInfo(
|
||||
relay: NormalizedRelayUrl,
|
||||
nip11Cache: Nip11CachedRetriever,
|
||||
): State<Nip11RelayInformation> =
|
||||
produceState(
|
||||
nip11Cache.getFromCache(relay),
|
||||
relay,
|
||||
) {
|
||||
nip11Cache.loadRelayInfo(
|
||||
relay = relay,
|
||||
onInfo = {
|
||||
value = it
|
||||
},
|
||||
onError = { url, errorCode, exceptionMessage ->
|
||||
Log.e("RelayInfo", "Error loading relay info for ${relay.url}: $errorCode - $exceptionMessage")
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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.nip11RelayInfo
|
||||
|
||||
import android.util.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class Nip11CachedRetriever(
|
||||
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
||||
) {
|
||||
private val relayInformationEmptyCache = LruCache<NormalizedRelayUrl, Nip11RelayInformation>(1000)
|
||||
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult?>(1000)
|
||||
private val retriever = Nip11Retriever(okHttpClient)
|
||||
|
||||
fun getEmpty(relay: NormalizedRelayUrl): Nip11RelayInformation {
|
||||
relayInformationEmptyCache.get(relay)?.let { return it }
|
||||
|
||||
val info =
|
||||
Nip11RelayInformation(
|
||||
name = relay.displayUrl(),
|
||||
icon = relay.toHttp() + "favicon.ico",
|
||||
)
|
||||
|
||||
relayInformationEmptyCache.put(relay, info)
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
fun getFromCache(relay: NormalizedRelayUrl): Nip11RelayInformation {
|
||||
val result = relayInformationDocumentCache.get(relay)
|
||||
|
||||
if (result == null) {
|
||||
// resets the clock
|
||||
val empty = getEmpty(relay)
|
||||
relayInformationDocumentCache.put(relay, RetrieveResult.Empty(empty))
|
||||
return empty
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
is RetrieveResult.Success -> result.data
|
||||
is RetrieveResult.Error -> result.data
|
||||
is RetrieveResult.Empty -> result.data
|
||||
is RetrieveResult.Loading -> result.data
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadRelayInfo(
|
||||
relay: NormalizedRelayUrl,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
val doc = relayInformationDocumentCache.get(relay)
|
||||
if (doc != null) {
|
||||
when (doc) {
|
||||
is RetrieveResult.Success -> onInfo(doc.data)
|
||||
is RetrieveResult.Loading -> {
|
||||
if (doc.isValid()) {
|
||||
// just wait.
|
||||
} else {
|
||||
retrieve(relay, onInfo, onError)
|
||||
}
|
||||
}
|
||||
is RetrieveResult.Error -> {
|
||||
if (doc.isValid()) {
|
||||
onError(relay, doc.error, null)
|
||||
} else {
|
||||
retrieve(relay, onInfo, onError)
|
||||
}
|
||||
}
|
||||
is RetrieveResult.Empty -> retrieve(relay, onInfo, onError)
|
||||
}
|
||||
} else {
|
||||
retrieve(relay, onInfo, onError)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun retrieve(
|
||||
relay: NormalizedRelayUrl,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
relayInformationDocumentCache.put(relay, RetrieveResult.Loading(getEmpty(relay)))
|
||||
retriever.loadRelayInfo(
|
||||
relay = relay,
|
||||
onInfo = {
|
||||
relayInformationDocumentCache.put(relay, RetrieveResult.Success(it))
|
||||
relayInformationEmptyCache.remove(relay)
|
||||
onInfo(it)
|
||||
},
|
||||
onError = { relay, code, errorMsg ->
|
||||
relayInformationDocumentCache.put(relay, RetrieveResult.Error(getEmpty(relay), code, errorMsg))
|
||||
relayInformationEmptyCache.remove(relay)
|
||||
onError(relay, code, errorMsg)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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.nip11RelayInfo
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
|
||||
class Nip11Retriever(
|
||||
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
||||
) {
|
||||
enum class ErrorCode {
|
||||
FAIL_TO_ASSEMBLE_URL,
|
||||
FAIL_TO_REACH_SERVER,
|
||||
FAIL_TO_PARSE_RESULT,
|
||||
FAIL_WITH_HTTP_STATUS,
|
||||
}
|
||||
|
||||
suspend fun loadRelayInfo(
|
||||
relay: NormalizedRelayUrl,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (NormalizedRelayUrl, ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
val url = relay.toHttp()
|
||||
try {
|
||||
val request: Request =
|
||||
Request
|
||||
.Builder()
|
||||
.header("Accept", "application/nostr+json")
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
val client = okHttpClient(relay)
|
||||
|
||||
client.newCall(request).executeAsync().use { response ->
|
||||
withContext(Dispatchers.IO) {
|
||||
val body = response.body.string()
|
||||
try {
|
||||
if (response.isSuccessful) {
|
||||
if (body.startsWith("{")) {
|
||||
onInfo(Nip11RelayInformation.fromJson(body))
|
||||
} else {
|
||||
onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, body)
|
||||
}
|
||||
} else {
|
||||
onError(relay, ErrorCode.FAIL_WITH_HTTP_STATUS, response.code.toString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(
|
||||
"RelayInfoFail",
|
||||
"Resulting Message from Relay ${relay.url} in not parseable: $body",
|
||||
e,
|
||||
)
|
||||
onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("RelayInfoFail", "Invalid URL ${relay.url}", e)
|
||||
onError(relay, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.nip11RelayInfo
|
||||
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
sealed class RetrieveResult(
|
||||
val data: Nip11RelayInformation,
|
||||
val time: Long,
|
||||
) {
|
||||
class Error(
|
||||
data: Nip11RelayInformation,
|
||||
val error: Nip11Retriever.ErrorCode,
|
||||
val msg: String? = null,
|
||||
) : RetrieveResult(data, TimeUtils.now())
|
||||
|
||||
class Success(
|
||||
data: Nip11RelayInformation,
|
||||
) : RetrieveResult(data, TimeUtils.now())
|
||||
|
||||
class Loading(
|
||||
data: Nip11RelayInformation,
|
||||
) : RetrieveResult(data, TimeUtils.now())
|
||||
|
||||
class Empty(
|
||||
data: Nip11RelayInformation,
|
||||
) : RetrieveResult(data, TimeUtils.now())
|
||||
|
||||
fun isValid() = time > TimeUtils.oneHourAgo()
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class DmInboxRelayState(
|
||||
localRelayList.flow,
|
||||
) { nip65Inbox, dmRelayList, privateOutBox, localRelays ->
|
||||
nip65Inbox + dmRelayList + privateOutBox + localRelays
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -20,15 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip17Dms
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
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.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -47,13 +46,14 @@ class DmRelayListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val dmListNote = cache.getOrCreateAddressableNote(getDMRelayListAddress())
|
||||
|
||||
fun getDMRelayListAddress() = ChatMessageRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getDMRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getDMRelayListAddress())
|
||||
fun getDMRelayListFlow(): StateFlow<NoteState> = dmListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getDMRelayListFlow(): StateFlow<NoteState> = getDMRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent
|
||||
fun getDMRelayList(): ChatMessageRelayListEvent? = dmListNote.event as? ChatMessageRelayListEvent
|
||||
|
||||
fun normalizeDMRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList
|
||||
@@ -63,8 +63,8 @@ class DmRelayListState(
|
||||
val flow =
|
||||
getDMRelayListFlow()
|
||||
.map { normalizeDMRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeDMRelayListWithBackup(getDMRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeDMRelayListWithBackup(dmListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -96,7 +96,7 @@ class DmRelayListState(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
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}")
|
||||
|
||||
@@ -25,10 +25,10 @@ import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional
|
||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
@@ -40,7 +40,10 @@ class PublicChatChannel(
|
||||
) : Channel() {
|
||||
var creator: User? = null
|
||||
var event: ChannelCreateEvent? = null
|
||||
var eventNote: Note? = null
|
||||
|
||||
// Important to keep this long-term reference because LocalCache uses WeakReferences.
|
||||
var creationEventNote: Note? = null
|
||||
var updateEventNote: Note? = null
|
||||
|
||||
var info = ChannelDataNorm(null, null, null, null)
|
||||
var infoTags = EmptyTagList
|
||||
@@ -71,7 +74,7 @@ class PublicChatChannel(
|
||||
|
||||
this.infoTags = event.tags.toImmutableListOfLists()
|
||||
this.updatedMetadataAt = event.createdAt
|
||||
this.eventNote = eventNote
|
||||
this.creationEventNote = eventNote
|
||||
|
||||
updateChannelInfo()
|
||||
}
|
||||
@@ -79,12 +82,14 @@ class PublicChatChannel(
|
||||
fun updateChannelInfo(
|
||||
creator: User,
|
||||
event: ChannelMetadataEvent,
|
||||
eventNote: Note? = null,
|
||||
) {
|
||||
this.creator = creator
|
||||
this.info = event.channelInfo()
|
||||
|
||||
this.infoTags = event.tags.toImmutableListOfLists()
|
||||
this.updatedMetadataAt = event.createdAt
|
||||
this.updateEventNote = eventNote
|
||||
|
||||
super.updateChannelInfo()
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip28PublicChats
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
@@ -30,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -51,13 +50,14 @@ class PublicChatListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val publicChatListNote = cache.getOrCreateAddressableNote(getChannelListAddress())
|
||||
|
||||
fun getChannelListAddress() = ChannelListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getChannelListNote(): AddressableNote = cache.getOrCreateAddressableNote(getChannelListAddress())
|
||||
fun getChannelListFlow(): StateFlow<NoteState> = publicChatListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getChannelListFlow(): StateFlow<NoteState> = getChannelListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getChannelList(): ChannelListEvent? = getChannelListNote().event as? ChannelListEvent
|
||||
fun getChannelList(): ChannelListEvent? = publicChatListNote.event as? ChannelListEvent
|
||||
|
||||
suspend fun publicChatListWithBackup(note: Note): Set<ChannelTag> {
|
||||
val event = note.event as? ChannelListEvent ?: settings.backupChannelList
|
||||
@@ -70,8 +70,8 @@ class PublicChatListState(
|
||||
.transformLatest { noteState ->
|
||||
emit(publicChatListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(publicChatListWithBackup(getChannelListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
emit(publicChatListWithBackup(publicChatListNote))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -85,7 +85,7 @@ class PublicChatListState(
|
||||
it.mapTo(mutableSetOf()) { it.eventId }
|
||||
}.onStart {
|
||||
emit(flow.value.mapTo(mutableSetOf()) { it.eventId })
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -132,7 +132,7 @@ class PublicChatListState(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Channel List Collector Start")
|
||||
getChannelListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Channel List for ${signer.pubKey}")
|
||||
|
||||
@@ -21,12 +21,11 @@
|
||||
package com.vitorpamplona.amethyst.model.nip30CustomEmojis
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
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.nip01Core.tags.addressables.taggedAddresses
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis
|
||||
@@ -52,13 +51,14 @@ class EmojiPackState(
|
||||
val link: MediaUrlImage,
|
||||
)
|
||||
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val emojiPackListNote = cache.getOrCreateAddressableNote(getEmojiPackSelectionAddress())
|
||||
|
||||
fun getEmojiPackSelectionAddress() = EmojiPackSelectionEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getEmojiPackSelection(): EmojiPackSelectionEvent? = getEmojiPackSelectionNote().event as? EmojiPackSelectionEvent
|
||||
fun getEmojiPackSelection(): EmojiPackSelectionEvent? = emojiPackListNote.event as? EmojiPackSelectionEvent
|
||||
|
||||
fun getEmojiPackSelectionFlow(): StateFlow<NoteState> = getEmojiPackSelectionNote().flow().metadata.stateFlow
|
||||
|
||||
fun getEmojiPackSelectionNote(): AddressableNote = cache.getOrCreateAddressableNote(getEmojiPackSelectionAddress())
|
||||
fun getEmojiPackSelectionFlow(): StateFlow<NoteState> = emojiPackListNote.flow().metadata.stateFlow
|
||||
|
||||
fun convertEmojiSelectionPack(selection: EmojiPackSelectionEvent?): List<StateFlow<NoteState>>? =
|
||||
selection?.taggedAddresses()?.map {
|
||||
@@ -75,7 +75,7 @@ class EmojiPackState(
|
||||
emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent))
|
||||
}.onStart {
|
||||
emit(convertEmojiSelectionPack(getEmojiPackSelection()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -120,7 +120,7 @@ class EmojiPackState(
|
||||
)?.map { it.value }?.toTypedArray() ?: emptyArray(),
|
||||
),
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
@@ -62,6 +62,7 @@ import kotlinx.coroutines.launch
|
||||
*/
|
||||
class NwcSignerState(
|
||||
val signer: NostrSigner,
|
||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
@@ -75,7 +76,7 @@ class NwcSignerState(
|
||||
settings.zapPaymentRequest
|
||||
.map {
|
||||
buildSigner(it) ?: signer
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -90,7 +91,7 @@ class NwcSignerState(
|
||||
nip47Signer
|
||||
.map {
|
||||
NostrWalletConnectRequestCache(it)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectRequestCache(nip47Signer.value))
|
||||
|
||||
/**
|
||||
@@ -101,7 +102,7 @@ class NwcSignerState(
|
||||
nip47Signer
|
||||
.map {
|
||||
NostrWalletConnectResponseCache(it)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectResponseCache(nip47Signer.value))
|
||||
|
||||
fun buildSigner(uri: Nip47WalletConnect.Nip47URINorm?) =
|
||||
@@ -163,13 +164,11 @@ class NwcSignerState(
|
||||
relay = walletService.relayUri,
|
||||
)
|
||||
|
||||
Amethyst.instance.sources.nwc
|
||||
.subscribe(filter)
|
||||
nwcFilterAssembler.subscribe(filter)
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
delay(60000) // waits 1 minute to complete payment.
|
||||
Amethyst.instance.sources.nwc
|
||||
.unsubscribe(filter)
|
||||
nwcFilterAssembler.unsubscribe(filter)
|
||||
}
|
||||
|
||||
cache.consume(event, zappedNote, true, walletService.relayUri) {
|
||||
|
||||
@@ -51,13 +51,14 @@ class BookmarkListState(
|
||||
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 getBookmarkListNote() = cache.getOrCreateAddressableNote(getBookmarkListAddress())
|
||||
fun getBookmarkListFlow(): StateFlow<NoteState> = bookmarkList.flow().metadata.stateFlow
|
||||
|
||||
fun getBookmarkListFlow(): StateFlow<NoteState> = getBookmarkListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBookmarkList(): BookmarkListEvent? = getBookmarkListNote().event as? BookmarkListEvent
|
||||
fun getBookmarkList(): BookmarkListEvent? = bookmarkList.event as? BookmarkListEvent
|
||||
|
||||
suspend fun publicBookmarks(note: Note): List<BookmarkIdTag> {
|
||||
val noteEvent = note.event as? BookmarkListEvent
|
||||
@@ -75,9 +76,9 @@ class BookmarkListState(
|
||||
.map { noteState ->
|
||||
publicBookmarks(noteState.note)
|
||||
}.onStart {
|
||||
emit(publicBookmarks(getBookmarkListNote()))
|
||||
emit(publicBookmarks(bookmarkList))
|
||||
}.debounce(100)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -90,9 +91,9 @@ class BookmarkListState(
|
||||
.map { noteState ->
|
||||
privateBookmarks(noteState.note)
|
||||
}.onStart {
|
||||
emit(privateBookmarks(getBookmarkListNote()))
|
||||
emit(privateBookmarks(bookmarkList))
|
||||
}.debounce(100)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -106,7 +107,7 @@ class BookmarkListState(
|
||||
.mapNotNull {
|
||||
if (it is EventBookmark) it.eventId else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -120,7 +121,7 @@ class BookmarkListState(
|
||||
.mapNotNull {
|
||||
if (it is AddressBookmark) it.address else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -134,7 +135,7 @@ class BookmarkListState(
|
||||
.mapNotNull {
|
||||
if (it is EventBookmark) it.eventId else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -148,7 +149,7 @@ class BookmarkListState(
|
||||
.mapNotNull {
|
||||
if (it is AddressBookmark) it.address else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -184,7 +185,7 @@ class BookmarkListState(
|
||||
emit(bookmarkList(private, public))
|
||||
}.onStart {
|
||||
emit(bookmarkList(privateBookmarks.value, publicBookmarks.value))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -93,7 +93,7 @@ class HiddenUsersState(
|
||||
settings.syncedSettings.security.showSensitiveContent.value,
|
||||
),
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
@@ -43,13 +44,14 @@ class BlockPeopleListState(
|
||||
val decryptionCache: PeopleListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val blockListNote = cache.getOrCreateAddressableNote(getBlockListAddress())
|
||||
|
||||
fun getBlockListAddress() = PeopleListEvent.createBlockAddress(signer.pubKey)
|
||||
|
||||
fun getBlockListNote() = LocalCache.getOrCreateAddressableNote(getBlockListAddress())
|
||||
fun getBlockListFlow(): StateFlow<NoteState> = blockListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getBlockListFlow(): StateFlow<NoteState> = getBlockListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
|
||||
fun getBlockList(): PeopleListEvent? = blockListNote.event as? PeopleListEvent
|
||||
|
||||
suspend fun blockListWithBackup(note: Note): List<MuteTag> {
|
||||
val event = note.event as? PeopleListEvent
|
||||
@@ -59,8 +61,8 @@ class BlockPeopleListState(
|
||||
val flow =
|
||||
getBlockListFlow()
|
||||
.map { blockListWithBackup(it.note) }
|
||||
.onStart { emit(blockListWithBackup(getBlockListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(blockListWithBackup(blockListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
@@ -30,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -49,13 +48,14 @@ class BlockedRelayListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val blockedListNote = cache.getOrCreateAddressableNote(getBlockedRelayListAddress())
|
||||
|
||||
fun getBlockedRelayListAddress() = BlockedRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getBlockedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlockedRelayListAddress())
|
||||
fun getBlockedRelayListFlow(): StateFlow<NoteState> = blockedListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getBlockedRelayListFlow(): StateFlow<NoteState> = getBlockedRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBlockedRelayList(): BlockedRelayListEvent? = getBlockedRelayListNote().event as? BlockedRelayListEvent
|
||||
fun getBlockedRelayList(): BlockedRelayListEvent? = blockedListNote.event as? BlockedRelayListEvent
|
||||
|
||||
suspend fun normalizeBlockedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? BlockedRelayListEvent ?: settings.backupBlockedRelayList
|
||||
@@ -66,8 +66,8 @@ class BlockedRelayListState(
|
||||
getBlockedRelayListFlow()
|
||||
.map {
|
||||
normalizeBlockedRelayListWithBackup(it.note)
|
||||
}.onStart { emit(normalizeBlockedRelayListWithBackup(getBlockedRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
}.onStart { emit(normalizeBlockedRelayListWithBackup(blockedListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -99,7 +99,7 @@ class BlockedRelayListState(
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Blocked Relay List Collector Start")
|
||||
getBlockedRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Blocked Relay List for ${signer.pubKey}")
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
@@ -44,13 +43,14 @@ class BroadcastRelayListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val broadcastListNote = cache.getOrCreateAddressableNote(getBroadcastRelayListAddress())
|
||||
|
||||
fun getBroadcastRelayListAddress() = BroadcastRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getBroadcastRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getBroadcastRelayListAddress())
|
||||
fun getBroadcastRelayListFlow(): StateFlow<NoteState> = broadcastListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getBroadcastRelayListFlow(): StateFlow<NoteState> = getBroadcastRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBroadcastRelayList(): BroadcastRelayListEvent? = getBroadcastRelayListNote().event as? BroadcastRelayListEvent
|
||||
fun getBroadcastRelayList(): BroadcastRelayListEvent? = broadcastListNote.event as? BroadcastRelayListEvent
|
||||
|
||||
suspend fun normalizeBroadcastRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? BroadcastRelayListEvent
|
||||
@@ -60,8 +60,8 @@ class BroadcastRelayListState(
|
||||
val flow =
|
||||
getBroadcastRelayListFlow()
|
||||
.map { normalizeBroadcastRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeBroadcastRelayListWithBackup(getBroadcastRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeBroadcastRelayListWithBackup(broadcastListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -72,5 +72,5 @@ class GeohashListDecryptionCache(
|
||||
emit(EmptyGeohashListCard)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
@@ -20,14 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
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.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -48,17 +47,18 @@ class GeohashListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val geohashListNote = cache.getOrCreateAddressableNote(getGeohashListAddress())
|
||||
|
||||
fun getGeohashListAddress() = GeohashListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getGeohashListNote(): AddressableNote = cache.getOrCreateAddressableNote(getGeohashListAddress())
|
||||
fun getGeohashListFlow(): StateFlow<NoteState> = geohashListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getGeohashListFlow(): StateFlow<NoteState> = getGeohashListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getGeohashList(): GeohashListEvent? = getGeohashListNote().event as? GeohashListEvent
|
||||
fun getGeohashList(): GeohashListEvent? = geohashListNote.event as? GeohashListEvent
|
||||
|
||||
suspend fun geohashListWithBackup(note: Note): Set<String> {
|
||||
val event = note.event as? GeohashListEvent ?: settings.backupGeohashList
|
||||
return event?.let { decryptionCache.geohashes(it) } ?: emptySet()
|
||||
return event?.let { decryptionCache.geohashes(it).mapTo(mutableSetOf()) { it.lowercase() } } ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -67,8 +67,8 @@ class GeohashListState(
|
||||
.transformLatest { noteState ->
|
||||
emit(geohashListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(geohashListWithBackup(getGeohashListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
emit(geohashListWithBackup(geohashListNote))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -114,7 +114,7 @@ class GeohashListState(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Geohash List Collector Start")
|
||||
getGeohashListFlow().collect { noteState ->
|
||||
Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}")
|
||||
|
||||
@@ -20,14 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
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.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -48,17 +47,18 @@ class HashtagListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val hashtagListNote = cache.getOrCreateAddressableNote(getHashtagListAddress())
|
||||
|
||||
fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getHashtagListNote(): AddressableNote = cache.getOrCreateAddressableNote(getHashtagListAddress())
|
||||
fun getHashtagListFlow(): StateFlow<NoteState> = hashtagListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getHashtagListFlow(): StateFlow<NoteState> = getHashtagListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getHashtagList(): HashtagListEvent? = getHashtagListNote().event as? HashtagListEvent
|
||||
fun getHashtagList(): HashtagListEvent? = hashtagListNote.event as? HashtagListEvent
|
||||
|
||||
suspend fun hashtagListWithBackup(note: Note): Set<String> {
|
||||
val event = note.event as? HashtagListEvent ?: settings.backupHashtagList
|
||||
return event?.let { decryptionCache.hashtags(it) } ?: emptySet()
|
||||
return event?.let { decryptionCache.hashtags(it).mapTo(mutableSetOf()) { it.lowercase() } } ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -67,11 +67,11 @@ class HashtagListState(
|
||||
.transformLatest { noteState ->
|
||||
emit(hashtagListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(hashtagListWithBackup(getHashtagListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
emit(hashtagListWithBackup(hashtagListNote))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
@@ -79,9 +79,9 @@ class HashtagListState(
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
return if (hashtagList == null) {
|
||||
HashtagListEvent.Companion.create(hashtags, true, signer)
|
||||
HashtagListEvent.create(hashtags, true, signer)
|
||||
} else {
|
||||
HashtagListEvent.Companion.add(hashtagList, hashtags, true, signer)
|
||||
HashtagListEvent.add(hashtagList, hashtags, true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,9 +89,9 @@ class HashtagListState(
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
return if (hashtagList == null) {
|
||||
HashtagListEvent.Companion.create(hashtag, true, signer)
|
||||
HashtagListEvent.create(hashtag, true, signer)
|
||||
} else {
|
||||
HashtagListEvent.Companion.add(hashtagList, hashtag, true, signer)
|
||||
HashtagListEvent.add(hashtagList, hashtag, true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class HashtagListState(
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
return if (hashtagList != null) {
|
||||
HashtagListEvent.Companion.remove(hashtagList, hashtag, signer)
|
||||
HashtagListEvent.remove(hashtagList, hashtag, signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -114,7 +114,7 @@ class HashtagListState(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Hashtag List Collector Start")
|
||||
getHashtagListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Hashtag List for ${signer.pubKey}")
|
||||
|
||||
@@ -21,22 +21,25 @@
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class IndexerRelayListState(
|
||||
val signer: NostrSigner,
|
||||
@@ -45,24 +48,37 @@ class IndexerRelayListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val indexerListNote = cache.getOrCreateAddressableNote(getIndexerRelayListAddress())
|
||||
|
||||
fun getIndexerRelayListAddress() = IndexerRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getIndexerRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getIndexerRelayListAddress())
|
||||
fun getIndexerRelayListFlow(): StateFlow<NoteState> = indexerListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getIndexerRelayListFlow(): StateFlow<NoteState> = getIndexerRelayListNote().flow().metadata.stateFlow
|
||||
fun getIndexerRelayList(): IndexerRelayListEvent? = indexerListNote.event as? IndexerRelayListEvent
|
||||
|
||||
fun getIndexerRelayList(): IndexerRelayListEvent? = getIndexerRelayListNote().event as? IndexerRelayListEvent
|
||||
fun indexListEvent(note: Note) = note.event as? IndexerRelayListEvent ?: settings.backupIndexRelayList
|
||||
|
||||
suspend fun normalizeIndexerRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? IndexerRelayListEvent
|
||||
return event?.let { decryptionCache.relays(it) } ?: emptySet()
|
||||
}
|
||||
suspend fun normalizeIndexerRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = indexListEvent(note)?.let { decryptionCache.relays(it) }?.ifEmpty { null } ?: DefaultIndexerRelayList
|
||||
|
||||
suspend fun normalizeIndexerRelayListWithBackupNoDefaults(note: Note): Set<NormalizedRelayUrl> = indexListEvent(note)?.let { decryptionCache.relays(it) } ?: emptySet()
|
||||
|
||||
val flow =
|
||||
getIndexerRelayListFlow()
|
||||
.map { normalizeIndexerRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeIndexerRelayListWithBackup(getIndexerRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeIndexerRelayListWithBackup(indexerListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
val flowNoDefaults =
|
||||
getIndexerRelayListFlow()
|
||||
.map { normalizeIndexerRelayListWithBackupNoDefaults(it.note) }
|
||||
.onStart { emit(normalizeIndexerRelayListWithBackupNoDefaults(indexerListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -85,4 +101,22 @@ class IndexerRelayListState(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupIndexRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved index relay list ${it.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Index Relay List Collector Start")
|
||||
getIndexerRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Index Relay List for ${signer.pubKey}")
|
||||
(it.note.event as? IndexerRelayListEvent)?.let {
|
||||
settings.updateIndexRelayList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.muteList
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -30,6 +29,7 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -49,13 +49,14 @@ class MuteListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val muteListNote = cache.getOrCreateAddressableNote(getMuteListAddress())
|
||||
|
||||
fun getMuteListAddress() = MuteListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getMuteListNote() = cache.getOrCreateAddressableNote(getMuteListAddress())
|
||||
fun getMuteListFlow(): StateFlow<NoteState> = muteListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getMuteListFlow(): StateFlow<NoteState> = getMuteListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
|
||||
fun getMuteList(): MuteListEvent? = muteListNote.event as? MuteListEvent
|
||||
|
||||
suspend fun muteListWithBackup(note: Note): List<MuteTag> {
|
||||
val event = note.event as? MuteListEvent ?: settings.backupMuteList
|
||||
@@ -65,8 +66,8 @@ class MuteListState(
|
||||
val flow =
|
||||
getMuteListFlow()
|
||||
.map { muteListWithBackup(it.note) }
|
||||
.onStart { emit(muteListWithBackup(getMuteListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(muteListWithBackup(muteListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -148,7 +149,7 @@ class MuteListState(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Mute List Collector Start")
|
||||
getMuteListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Mute List for ${signer.pubKey}")
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* 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.peopleList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.anyNotNullEvent
|
||||
import com.vitorpamplona.amethyst.model.eventIdSet
|
||||
import com.vitorpamplona.amethyst.model.events
|
||||
import com.vitorpamplona.amethyst.model.filter
|
||||
import com.vitorpamplona.amethyst.model.updateFlow
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.update
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.description
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.image
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.person
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.personFirst
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.removePerson
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.title
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
|
||||
import com.vitorpamplona.quartz.utils.flattenToSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Maintains several stateflows for each step in processing PeopleLists
|
||||
*
|
||||
* This class must receive updates from Large Cache as it receives new events.
|
||||
*/
|
||||
class FollowListsState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
val user = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun existingPeopleListNotes() = cache.addressables.filter(FollowListEvent.KIND, user.pubkeyHex)
|
||||
|
||||
val followListVersions = MutableStateFlow(0)
|
||||
|
||||
val followListNotes =
|
||||
followListVersions
|
||||
.map { existingPeopleListNotes() }
|
||||
.onStart { emit(existingPeopleListNotes()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
||||
|
||||
val followListsEventIds =
|
||||
followListNotes
|
||||
.map { it.eventIdSet() }
|
||||
.onStart { emit(followListNotes.value.eventIdSet()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptySet())
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val latestLists: StateFlow<List<FollowListEvent>> =
|
||||
followListNotes
|
||||
.transformLatest { emitAll(it.updateFlow<FollowListEvent>()) }
|
||||
.onStart { emit(followListNotes.value.events()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
fun List<FollowListEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet()
|
||||
|
||||
val allPeopleListProfiles: StateFlow<Set<HexKey>> =
|
||||
latestLists
|
||||
.map { it.mapToUserIdSet() }
|
||||
.onStart { emit(latestLists.value.mapToUserIdSet()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptySet())
|
||||
|
||||
fun FollowListEvent.toUI() =
|
||||
PeopleList(
|
||||
identifierTag = this.dTag(),
|
||||
title = this.title() ?: this.dTag(),
|
||||
description = this.description(),
|
||||
image = this.image(),
|
||||
privateMembers = emptySet(),
|
||||
publicMembers = cache.load(this.followIdSet()),
|
||||
)
|
||||
|
||||
fun List<FollowListEvent>.toUI() = this.map { it.toUI() }
|
||||
|
||||
val uiListFlow =
|
||||
latestLists
|
||||
.map { it.toUI() }
|
||||
.onStart { emit(latestLists.value.toUI()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
||||
|
||||
fun List<PeopleList>.select(dTag: String) =
|
||||
this.firstOrNull {
|
||||
it.identifierTag == dTag
|
||||
}
|
||||
|
||||
fun selectList(dTag: String) = uiListFlow.value.select(dTag)
|
||||
|
||||
fun selectListFlow(dTag: String) =
|
||||
uiListFlow
|
||||
.map { it.select(dTag) }
|
||||
.onStart { emit(selectList(dTag)) }
|
||||
|
||||
fun isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex)
|
||||
|
||||
fun DeletionEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(FollowListEvent.Companion.KIND) || deletesAnyEventIn(followListsEventIds.value)
|
||||
|
||||
fun hasItemInNoteList(notes: Set<Note>): Boolean =
|
||||
notes.anyNotNullEvent { event ->
|
||||
if (event.pubKey == signer.pubKey) {
|
||||
event is FollowListEvent || (event is DeletionEvent && event.hasDeletedAnyFollowList())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun newNotes(newNotes: Set<Note>) {
|
||||
if (hasItemInNoteList(newNotes)) {
|
||||
forceRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun deletedNotes(deletedNotes: Set<Note>) {
|
||||
if (hasItemInNoteList(deletedNotes)) {
|
||||
forceRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun forceRefresh() {
|
||||
followListVersions.update { it + 1 }
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Updating Lists
|
||||
// --------------
|
||||
|
||||
fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier }
|
||||
|
||||
fun getPeopleList(noteIdentifier: String): FollowListEvent = getPeopleListNote(noteIdentifier)?.event as FollowListEvent
|
||||
|
||||
fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint())
|
||||
|
||||
fun Set<User>.toUserTags() = map { it.toUserTag() }
|
||||
|
||||
suspend fun addFollowList(
|
||||
name: String,
|
||||
desc: String?,
|
||||
image: String?,
|
||||
member: User? = null,
|
||||
isPrivate: Boolean = false,
|
||||
account: Account,
|
||||
): String {
|
||||
val dTag = UUID.randomUUID().toString()
|
||||
|
||||
val newListTemplate =
|
||||
FollowListEvent.build(
|
||||
name = name,
|
||||
people = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||
dTag = dTag,
|
||||
) {
|
||||
if (desc != null) description(desc)
|
||||
if (image != null) image(image)
|
||||
}
|
||||
|
||||
val newList = signer.sign(newListTemplate)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
return dTag
|
||||
}
|
||||
|
||||
suspend fun updateMetadata(
|
||||
name: String?,
|
||||
desc: String?,
|
||||
image: String?,
|
||||
peopleList: PeopleList,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(peopleList.identifierTag)
|
||||
|
||||
val template =
|
||||
listEvent.update {
|
||||
if (name != null) title(name)
|
||||
if (desc != null) description(desc)
|
||||
if (image != null) image(image)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun cloneFollowSet(
|
||||
currentFollowPack: PeopleList,
|
||||
customCloneName: String?,
|
||||
customCloneDescription: String?,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(currentFollowPack.identifierTag)
|
||||
|
||||
val template =
|
||||
listEvent.update {
|
||||
// new list
|
||||
dTag(UUID.randomUUID().toString())
|
||||
|
||||
// updates names
|
||||
if (customCloneName != null) title(customCloneName)
|
||||
if (customCloneDescription != null) description(customCloneDescription)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun deleteFollowSet(
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent)))
|
||||
account.sendMyPublicAndPrivateOutbox(deletionEvent)
|
||||
}
|
||||
|
||||
suspend fun addUserToSet(
|
||||
user: User,
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
|
||||
val template =
|
||||
followListEvent.update {
|
||||
person(user.pubkeyHex, user.bestRelayHint())
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun addUserFirstToSet(
|
||||
user: User,
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
|
||||
val template =
|
||||
followListEvent.update {
|
||||
personFirst(user.pubkeyHex, user.bestRelayHint())
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun removeUserFromSet(
|
||||
user: User,
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
|
||||
val template =
|
||||
followListEvent.update {
|
||||
removePerson(user.pubkeyHex)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.peopleList
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Stable
|
||||
data class PeopleList(
|
||||
val identifierTag: String,
|
||||
val title: String,
|
||||
val description: String?,
|
||||
val image: String?,
|
||||
val privateMembers: Set<User> = emptySet(),
|
||||
val publicMembers: Set<User> = emptySet(),
|
||||
) {
|
||||
val privateMembersList: ImmutableList<User> = privateMembers.toPersistentList()
|
||||
val publicMembersList: ImmutableList<User> = publicMembers.toPersistentList()
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
* 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.blockPeopleList
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.peopleList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
@@ -53,4 +53,8 @@ class PeopleListDecryptionCache(
|
||||
suspend fun words(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWords()
|
||||
|
||||
suspend fun wordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet()
|
||||
|
||||
suspend fun privateUsers(event: PeopleListEvent) = cachedPrivateLists.privateTags(event)?.mutedUsers() ?: emptyList()
|
||||
|
||||
suspend fun privateUserIdSet(event: PeopleListEvent) = cachedPrivateLists.privateTags(event)?.mutedUserIdSet() ?: emptySet()
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* 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.peopleList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.anyNotNullEvent
|
||||
import com.vitorpamplona.amethyst.model.eventIdSet
|
||||
import com.vitorpamplona.amethyst.model.events
|
||||
import com.vitorpamplona.amethyst.model.filter
|
||||
import com.vitorpamplona.amethyst.model.updateFlow
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.update
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.description
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
|
||||
import com.vitorpamplona.quartz.utils.flattenToSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Maintains several stateflows for each step in processing PeopleLists
|
||||
*
|
||||
* This class must receive updates from Large Cache as it receives new events.
|
||||
*/
|
||||
class PeopleListsState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val decryptionCache: PeopleListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
val user = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun existingPeopleListNotes() = cache.addressables.filter(PeopleListEvent.KIND, user.pubkeyHex)
|
||||
|
||||
val peopleListVersions = MutableStateFlow(0)
|
||||
|
||||
val peopleListNotes =
|
||||
peopleListVersions
|
||||
.map { existingPeopleListNotes() }
|
||||
.onStart { emit(existingPeopleListNotes()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val peopleListsEventIds =
|
||||
peopleListNotes
|
||||
.map { it.eventIdSet() }
|
||||
.onStart { emit(peopleListNotes.value.eventIdSet()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val latestLists: StateFlow<List<PeopleListEvent>> =
|
||||
peopleListNotes
|
||||
.transformLatest { emitAll(it.updateFlow<PeopleListEvent>()) }
|
||||
.onStart { emit(peopleListNotes.value.events()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
suspend fun PeopleListEvent.userIdSet() = decryptionCache.userIdSet(this)
|
||||
|
||||
suspend fun List<PeopleListEvent>.mapToUserIdSet() = this.map { it.userIdSet() }.flattenToSet()
|
||||
|
||||
suspend fun List<PeopleListEvent>.mapGoodUsersToIdSet() =
|
||||
this
|
||||
.mapNotNull {
|
||||
if (it.dTag() != PeopleListEvent.BLOCK_LIST_D_TAG) {
|
||||
it.userIdSet()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.flattenToSet()
|
||||
|
||||
val allGoodPeopleListProfiles: StateFlow<Set<HexKey>> =
|
||||
latestLists
|
||||
.map { it.mapGoodUsersToIdSet() }
|
||||
.onStart { emit(latestLists.value.mapGoodUsersToIdSet()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
||||
|
||||
suspend fun PeopleListEvent.toUI() =
|
||||
PeopleList(
|
||||
identifierTag = this.dTag(),
|
||||
title = this.nameOrTitle() ?: this.dTag(),
|
||||
description = this.description(),
|
||||
image = this.image(),
|
||||
privateMembers = cache.load(decryptionCache.privateUserIdSet(this)),
|
||||
publicMembers = cache.load(this.publicUsersIdSet()),
|
||||
)
|
||||
|
||||
suspend fun List<PeopleListEvent>.toUI() = this.map { it.toUI() }.sortedBy { it.title }
|
||||
|
||||
val uiListFlow =
|
||||
latestLists
|
||||
.map { it.toUI() }
|
||||
.onStart { emit(latestLists.value.toUI()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
fun List<PeopleList>.select(dTag: String) =
|
||||
this.firstOrNull {
|
||||
it.identifierTag == dTag
|
||||
}
|
||||
|
||||
fun selectList(dTag: String) = uiListFlow.value.select(dTag)
|
||||
|
||||
fun selectListFlow(dTag: String) =
|
||||
uiListFlow
|
||||
.map { it.select(dTag) }
|
||||
.onStart { emit(selectList(dTag)) }
|
||||
|
||||
fun DeletionEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(PeopleListEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value)
|
||||
|
||||
fun hasItemInNoteList(notes: Set<Note>): Boolean =
|
||||
notes.anyNotNullEvent { event ->
|
||||
if (event.pubKey == signer.pubKey) {
|
||||
event is PeopleListEvent || (event is DeletionEvent && event.hasDeletedAnyPeopleList())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun newNotes(newNotes: Set<Note>) {
|
||||
if (hasItemInNoteList(newNotes)) {
|
||||
forceRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun deletedNotes(deletedNotes: Set<Note>) {
|
||||
if (hasItemInNoteList(deletedNotes)) {
|
||||
forceRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun forceRefresh() {
|
||||
peopleListVersions.update { it + 1 }
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Updating Lists
|
||||
// --------------
|
||||
|
||||
fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier }
|
||||
|
||||
fun getPeopleList(noteIdentifier: String): PeopleListEvent = getPeopleListNote(noteIdentifier)?.event as PeopleListEvent
|
||||
|
||||
fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint())
|
||||
|
||||
fun Set<User>.toUserTags() = map { it.toUserTag() }
|
||||
|
||||
suspend fun addFollowList(
|
||||
listName: String,
|
||||
listDescription: String?,
|
||||
listImage: String?,
|
||||
member: User? = null,
|
||||
isPrivate: Boolean = false,
|
||||
account: Account,
|
||||
): String {
|
||||
val dTag = UUID.randomUUID().toString()
|
||||
val newListTemplate =
|
||||
PeopleListEvent.build(
|
||||
dTag = dTag,
|
||||
name = listName,
|
||||
publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||
privateMembers = if (isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||
signer = account.signer,
|
||||
) {
|
||||
if (listDescription != null) description(listDescription)
|
||||
if (listImage != null) image(listImage)
|
||||
}
|
||||
|
||||
val newList = signer.sign(newListTemplate)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
return dTag
|
||||
}
|
||||
|
||||
suspend fun updateMetadata(
|
||||
listName: String?,
|
||||
listDescription: String?,
|
||||
listImage: String?,
|
||||
peopleList: PeopleList,
|
||||
account: Account,
|
||||
) {
|
||||
val listEvent = getPeopleList(peopleList.identifierTag)
|
||||
|
||||
val template =
|
||||
listEvent.update {
|
||||
if (listName != null) name(listName)
|
||||
if (listDescription != null) description(listDescription)
|
||||
if (listImage != null) image(listImage)
|
||||
}
|
||||
|
||||
val newList = signer.sign(template)
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun cloneFollowSet(
|
||||
currentPeopleList: PeopleList,
|
||||
customCloneName: String?,
|
||||
customCloneDescription: String?,
|
||||
account: Account,
|
||||
) {
|
||||
val newList =
|
||||
PeopleListEvent.createListWithDescription(
|
||||
dTag = UUID.randomUUID().toString(),
|
||||
title = customCloneName ?: currentPeopleList.title,
|
||||
description = customCloneDescription ?: currentPeopleList.description,
|
||||
publicMembers = currentPeopleList.publicMembers.toUserTags(),
|
||||
privateMembers = currentPeopleList.privateMembers.toUserTags(),
|
||||
signer = account.signer,
|
||||
)
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun deleteFollowSet(
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent)))
|
||||
account.sendMyPublicAndPrivateOutbox(deletionEvent)
|
||||
}
|
||||
|
||||
suspend fun addUserToSet(
|
||||
user: User,
|
||||
identifierTag: String,
|
||||
shouldBePrivateMember: Boolean,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
val newList =
|
||||
PeopleListEvent.addUser(
|
||||
earlierVersion = followListEvent,
|
||||
pubKeyHex = user.pubkeyHex,
|
||||
relayHint = user.bestRelayHint(),
|
||||
isPrivate = shouldBePrivateMember,
|
||||
signer = account.signer,
|
||||
)
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun addUserFirstToSet(
|
||||
user: User,
|
||||
identifierTag: String,
|
||||
shouldBePrivateMember: Boolean,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
val newList =
|
||||
PeopleListEvent.addUserFirst(
|
||||
earlierVersion = followListEvent,
|
||||
pubKeyHex = user.pubkeyHex,
|
||||
relayHint = user.bestRelayHint(),
|
||||
isPrivate = shouldBePrivateMember,
|
||||
signer = account.signer,
|
||||
)
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
|
||||
suspend fun removeUserFromSet(
|
||||
user: User,
|
||||
isPrivate: Boolean,
|
||||
identifierTag: String,
|
||||
account: Account,
|
||||
) {
|
||||
val followListEvent = getPeopleList(identifierTag)
|
||||
val newList =
|
||||
PeopleListEvent.removeUser(
|
||||
earlierVersion = followListEvent,
|
||||
pubKeyHex = user.pubkeyHex,
|
||||
isUserPrivate = isPrivate,
|
||||
signer = account.signer,
|
||||
)
|
||||
account.sendMyPublicAndPrivateOutbox(newList)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
@@ -44,13 +43,14 @@ class ProxyRelayListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val proxyListNote = cache.getOrCreateAddressableNote(getProxyRelayListAddress())
|
||||
|
||||
fun getProxyRelayListAddress() = ProxyRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getProxyRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getProxyRelayListAddress())
|
||||
fun getProxyRelayListFlow(): StateFlow<NoteState> = proxyListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getProxyRelayListFlow(): StateFlow<NoteState> = getProxyRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getProxyRelayList(): ProxyRelayListEvent? = getProxyRelayListNote().event as? ProxyRelayListEvent
|
||||
fun getProxyRelayList(): ProxyRelayListEvent? = proxyListNote.event as? ProxyRelayListEvent
|
||||
|
||||
suspend fun normalizeProxyRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? ProxyRelayListEvent
|
||||
@@ -60,8 +60,8 @@ class ProxyRelayListState(
|
||||
val flow =
|
||||
getProxyRelayListFlow()
|
||||
.map { normalizeProxyRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeProxyRelayListWithBackup(getProxyRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeProxyRelayListWithBackup(proxyListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -72,5 +72,5 @@ open class GenericRelayListCache<T : PrivateTagArrayEvent>(
|
||||
emit(EmptyRelayListCard)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -30,6 +28,7 @@ import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -49,13 +48,14 @@ class SearchRelayListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val searchListNote = cache.getOrCreateAddressableNote(getSearchRelayListAddress())
|
||||
|
||||
fun getSearchRelayListAddress() = SearchRelayListEvent.Companion.createAddress(signer.pubKey)
|
||||
|
||||
fun getSearchRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getSearchRelayListAddress())
|
||||
fun getSearchRelayListFlow(): StateFlow<NoteState> = searchListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getSearchRelayListFlow(): StateFlow<NoteState> = getSearchRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent
|
||||
fun getSearchRelayList(): SearchRelayListEvent? = searchListNote.event as? SearchRelayListEvent
|
||||
|
||||
fun searchListEvent(note: Note) = note.event as? SearchRelayListEvent ?: settings.backupSearchRelayList
|
||||
|
||||
@@ -66,8 +66,8 @@ class SearchRelayListState(
|
||||
val flow =
|
||||
getSearchRelayListFlow()
|
||||
.map { normalizeSearchRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeSearchRelayListWithBackup(getSearchRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeSearchRelayListWithBackup(searchListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
@@ -77,11 +77,11 @@ class SearchRelayListState(
|
||||
val flowNoDefaults =
|
||||
getSearchRelayListFlow()
|
||||
.map { normalizeSearchRelayListWithBackupNoDefaults(it.note) }
|
||||
.onStart { emit(normalizeSearchRelayListWithBackupNoDefaults(getSearchRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeSearchRelayListWithBackupNoDefaults(searchListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
@@ -89,13 +89,13 @@ class SearchRelayListState(
|
||||
val relayListForSearch = getSearchRelayList()
|
||||
|
||||
return if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) {
|
||||
SearchRelayListEvent.Companion.updateRelayList(
|
||||
SearchRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForSearch,
|
||||
relays = searchRelays,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
SearchRelayListEvent.Companion.create(
|
||||
SearchRelayListEvent.create(
|
||||
relays = searchRelays,
|
||||
signer = signer,
|
||||
)
|
||||
@@ -109,7 +109,7 @@ class SearchRelayListState(
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Search Relay List Collector Start")
|
||||
getSearchRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Search Relay List for ${signer.pubKey}")
|
||||
|
||||
@@ -20,15 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
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.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -48,13 +47,14 @@ class TrustedRelayListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getTrustedRelayListAddress() = TrustedRelayListEvent.Companion.createAddress(signer.pubKey)
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val trustedListNote = cache.getOrCreateAddressableNote(getTrustedRelayListAddress())
|
||||
|
||||
fun getTrustedRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getTrustedRelayListAddress())
|
||||
fun getTrustedRelayListAddress() = TrustedRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getTrustedRelayListFlow(): StateFlow<NoteState> = getTrustedRelayListNote().flow().metadata.stateFlow
|
||||
fun getTrustedRelayListFlow(): StateFlow<NoteState> = trustedListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getTrustedRelayList(): TrustedRelayListEvent? = getTrustedRelayListNote().event as? TrustedRelayListEvent
|
||||
fun getTrustedRelayList(): TrustedRelayListEvent? = trustedListNote.event as? TrustedRelayListEvent
|
||||
|
||||
suspend fun normalizeTrustedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? TrustedRelayListEvent ?: settings.backupTrustedRelayList
|
||||
@@ -64,8 +64,8 @@ class TrustedRelayListState(
|
||||
val flow =
|
||||
getTrustedRelayListFlow()
|
||||
.map { normalizeTrustedRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeTrustedRelayListWithBackup(getTrustedRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeTrustedRelayListWithBackup(trustedListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
@@ -96,7 +96,7 @@ class TrustedRelayListState(
|
||||
GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Trusted Relay List Collector Start")
|
||||
getTrustedRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Trusted Relay List for ${signer.pubKey}")
|
||||
|
||||
@@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
|
||||
@@ -36,11 +36,13 @@ class LiveActivitiesChannel(
|
||||
) : Channel() {
|
||||
var creator: User? = null
|
||||
var info: LiveActivitiesEvent? = null
|
||||
|
||||
// Important to keep this long-term reference because LocalCache uses WeakReferences.
|
||||
var infoNote: Note? = null
|
||||
|
||||
fun address() = address
|
||||
|
||||
override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays()
|
||||
override fun relays() = info?.allRelayUrls()?.toSet()?.ifEmpty { null } ?: super.relays()
|
||||
|
||||
fun relayHintUrl() = relays().firstOrNull()
|
||||
|
||||
@@ -57,7 +59,7 @@ class LiveActivitiesChannel(
|
||||
super.updateChannelInfo()
|
||||
}
|
||||
|
||||
override fun toBestDisplayName(): String = info?.title() ?: toNAddr().toShortDisplay()
|
||||
override fun toBestDisplayName(): String = info?.title() ?: creatorName() ?: toNAddr().toShortDisplay()
|
||||
|
||||
fun creatorName(): String? = creator?.toBestDisplayName()
|
||||
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip65RelayList
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Constants
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -31,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -49,19 +48,24 @@ class Nip65RelayListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val nip65ListNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress())
|
||||
|
||||
fun getNIP65RelayListAddress() = AdvertisedRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getNIP65RelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress())
|
||||
fun getNIP65RelayListFlow(): StateFlow<NoteState> = nip65ListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getNIP65RelayListFlow(): StateFlow<NoteState> = getNIP65RelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getNIP65RelayList(): AdvertisedRelayListEvent? = getNIP65RelayListNote().event as? AdvertisedRelayListEvent
|
||||
fun getNIP65RelayList(): AdvertisedRelayListEvent? = nip65ListNote.event as? AdvertisedRelayListEvent
|
||||
|
||||
fun nip65Event(note: Note) = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList
|
||||
|
||||
fun normalizeNIP65WriteRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.writeRelaysNorm()?.toSet() ?: Constants.eventFinderRelays
|
||||
|
||||
fun normalizeNIP65ReadRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.readRelaysNorm()?.toSet() ?: Constants.eventFinderRelays
|
||||
fun normalizeNIP65ReadRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.readRelaysNorm()?.toSet() ?: Constants.bootstrapInbox
|
||||
|
||||
fun normalizeNIP65WriteRelayListNoDefaults(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.writeRelaysNorm()?.toSet() ?: emptySet()
|
||||
|
||||
fun normalizeNIP65ReadRelayListNoDefaults(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.readRelaysNorm()?.toSet() ?: emptySet()
|
||||
|
||||
fun normalizeNIP65AllRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.relays()?.map { it.relayUrl }?.toSet() ?: Constants.eventFinderRelays
|
||||
|
||||
@@ -70,8 +74,8 @@ class Nip65RelayListState(
|
||||
val outboxFlow =
|
||||
getNIP65RelayListFlow()
|
||||
.map { normalizeNIP65WriteRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeNIP65ReadRelayListWithBackup(getNIP65RelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeNIP65WriteRelayListWithBackup(nip65ListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -81,19 +85,30 @@ class Nip65RelayListState(
|
||||
val inboxFlow =
|
||||
getNIP65RelayListFlow()
|
||||
.map { normalizeNIP65ReadRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeNIP65ReadRelayListWithBackup(getNIP65RelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeNIP65ReadRelayListWithBackup(nip65ListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
val allFlow =
|
||||
val outboxFlowNoDefaults =
|
||||
getNIP65RelayListFlow()
|
||||
.map { normalizeNIP65AllRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeNIP65AllRelayListWithBackup(getNIP65RelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.map { normalizeNIP65WriteRelayListNoDefaults(it.note) }
|
||||
.onStart { emit(normalizeNIP65WriteRelayListNoDefaults(nip65ListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
val inboxFlowNoDefaults =
|
||||
getNIP65RelayListFlow()
|
||||
.map { normalizeNIP65ReadRelayListNoDefaults(it.note) }
|
||||
.onStart { emit(normalizeNIP65ReadRelayListNoDefaults(nip65ListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -103,8 +118,8 @@ class Nip65RelayListState(
|
||||
val allFlowNoDefaults =
|
||||
getNIP65RelayListFlow()
|
||||
.map { normalizeNIP65AllRelayListWithBackupNoDefaults(it.note) }
|
||||
.onStart { emit(normalizeNIP65AllRelayListWithBackupNoDefaults(getNIP65RelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeNIP65AllRelayListWithBackupNoDefaults(nip65ListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -135,7 +150,7 @@ class Nip65RelayListState(
|
||||
GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
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}")
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip72Communities
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -30,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -51,13 +51,14 @@ class CommunityListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val communityListNote = cache.getOrCreateAddressableNote(getCommunityListAddress())
|
||||
|
||||
fun getCommunityListAddress() = CommunityListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getCommunityListNote(): AddressableNote = cache.getOrCreateAddressableNote(getCommunityListAddress())
|
||||
fun getCommunityListFlow(): StateFlow<NoteState> = communityListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getCommunityListFlow(): StateFlow<NoteState> = getCommunityListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getCommunityList(): CommunityListEvent? = getCommunityListNote().event as? CommunityListEvent
|
||||
fun getCommunityList(): CommunityListEvent? = communityListNote.event as? CommunityListEvent
|
||||
|
||||
suspend fun communityListWithBackup(note: Note): Set<CommunityTag> {
|
||||
val event = note.event as? CommunityListEvent ?: settings.backupCommunityList
|
||||
@@ -70,14 +71,30 @@ class CommunityListState(
|
||||
.transformLatest { noteState ->
|
||||
emit(communityListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(communityListWithBackup(getCommunityListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
emit(communityListWithBackup(communityListNote))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flowNotes: StateFlow<List<AddressableNote>> =
|
||||
flow
|
||||
.map { hint ->
|
||||
hint.map {
|
||||
cache.getOrCreateAddressableNote(it.address)
|
||||
}
|
||||
}.onStart {
|
||||
emit(flow.value.map { cache.getOrCreateAddressableNote(it.address) })
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flowSet: StateFlow<Set<String>> =
|
||||
flow
|
||||
@@ -85,7 +102,7 @@ class CommunityListState(
|
||||
hint.mapTo(mutableSetOf()) { it.address.toValue() }
|
||||
}.onStart {
|
||||
emit(flow.value.mapTo(mutableSetOf()) { it.address.toValue() })
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -146,7 +163,7 @@ class CommunityListState(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Community List Collector Start")
|
||||
getCommunityListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Community List for ${signer.pubKey}")
|
||||
|
||||
@@ -20,15 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip78AppSpecific
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -47,17 +46,18 @@ class AppSpecificState(
|
||||
const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings"
|
||||
}
|
||||
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val amethystSettingsNote = cache.getOrCreateAddressableNote(getAppSpecificDataAddress())
|
||||
|
||||
fun getAppSpecificDataAddress() = AppSpecificDataEvent.createAddress(signer.pubKey, APP_SPECIFIC_DATA_D_TAG)
|
||||
|
||||
fun getAppSpecificDataNote() = cache.getOrCreateAddressableNote(getAppSpecificDataAddress())
|
||||
|
||||
fun getAppSpecificDataFlow(): StateFlow<NoteState> = getAppSpecificDataNote().flow().metadata.stateFlow
|
||||
fun getAppSpecificDataFlow(): StateFlow<NoteState> = amethystSettingsNote.flow().metadata.stateFlow
|
||||
|
||||
suspend fun saveNewAppSpecificData(): AppSpecificDataEvent {
|
||||
val toInternal = settings.syncedSettings.toInternal()
|
||||
return AppSpecificDataEvent.create(
|
||||
dTag = APP_SPECIFIC_DATA_D_TAG,
|
||||
description = signer.nip44Encrypt(JsonMapper.mapper.writeValueAsString(toInternal), signer.pubKey),
|
||||
description = signer.nip44Encrypt(JsonMapper.toJson(toInternal), signer.pubKey),
|
||||
otherTags = emptyArray(),
|
||||
signer = signer,
|
||||
)
|
||||
@@ -72,7 +72,7 @@ class AppSpecificState(
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
try {
|
||||
val decrypted = signer.decrypt(event.content, event.pubKey)
|
||||
val syncedSettings = JsonMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
val syncedSettings = JsonMapper.fromJson<AccountSyncedSettingsInternal>(decrypted)
|
||||
settings.syncedSettings.updateFrom(syncedSettings)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
@@ -81,7 +81,7 @@ class AppSpecificState(
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "AppSpecificData Collector Start")
|
||||
getAppSpecificDataFlow().collect {
|
||||
try {
|
||||
@@ -89,7 +89,7 @@ class AppSpecificState(
|
||||
(it.note.event as? AppSpecificDataEvent)?.let {
|
||||
val decrypted = signer.decrypt(it.content, it.pubKey)
|
||||
try {
|
||||
val syncedSettings = JsonMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
val syncedSettings = JsonMapper.fromJson<AccountSyncedSettingsInternal>(decrypted)
|
||||
settings.updateAppSpecificData(it, syncedSettings)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.model.nip96FileStorage
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
@@ -42,13 +41,14 @@ class FileStorageServerListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val fileStorageListNote = cache.getOrCreateAddressableNote(getFileServersAddress())
|
||||
|
||||
fun getFileServersAddress() = FileServersEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getFileServersAddress())
|
||||
fun getFileServersListFlow(): StateFlow<NoteState> = fileStorageListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getFileServersListFlow(): StateFlow<NoteState> = getFileServersNote().flow().metadata.stateFlow
|
||||
|
||||
fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent
|
||||
fun getFileServersList(): FileServersEvent? = fileStorageListNote.event as? FileServersEvent
|
||||
|
||||
fun normalizeServers(note: Note): List<String> {
|
||||
val event = note.event as? FileServersEvent
|
||||
@@ -58,8 +58,8 @@ class FileStorageServerListState(
|
||||
val flow =
|
||||
getFileServersListFlow()
|
||||
.map { normalizeServers(it.note) }
|
||||
.onStart { emit(normalizeServers(getFileServersNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeServers(fileStorageListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.model.nipB7Blossom
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
@@ -44,13 +43,14 @@ class BlossomServerListState(
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val blossomListNote = cache.getOrCreateAddressableNote(getBlossomServersAddress())
|
||||
|
||||
fun getBlossomServersAddress() = BlossomServersEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getBlossomServersNote(): AddressableNote = cache.getOrCreateAddressableNote(getBlossomServersAddress())
|
||||
fun getBlossomServersListFlow(): StateFlow<NoteState> = blossomListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getBlossomServersListFlow(): StateFlow<NoteState> = getBlossomServersNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBlossomServersList(): BlossomServersEvent? = getBlossomServersNote().event as? BlossomServersEvent
|
||||
fun getBlossomServersList(): BlossomServersEvent? = blossomListNote.event as? BlossomServersEvent
|
||||
|
||||
fun normalizeServers(note: Note): List<String> {
|
||||
val event = note.event as? BlossomServersEvent
|
||||
@@ -60,8 +60,8 @@ class BlossomServerListState(
|
||||
val flow =
|
||||
getBlossomServersListFlow()
|
||||
.map { normalizeServers(it.note) }
|
||||
.onStart { emit(normalizeServers(getBlossomServersNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onStart { emit(normalizeServers(blossomListNote)) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
@@ -74,7 +74,7 @@ class BlossomServerListState(
|
||||
return if (serverList != null && serverList.tags.isNotEmpty()) {
|
||||
BlossomServersEvent.updateRelayList(
|
||||
earlierVersion = serverList,
|
||||
relays = servers,
|
||||
servers = servers,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model.observables
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import java.io.File
|
||||
|
||||
class AccountPreferenceStores(
|
||||
@@ -75,8 +75,9 @@ class AccountPreferenceStores(
|
||||
)
|
||||
}
|
||||
|
||||
fun removeAccount(npub: String) {
|
||||
file(npub).delete()
|
||||
fun removeAccount(npub: String): Boolean {
|
||||
val deleted = file(npub).delete()
|
||||
storeCache.remove(npub)
|
||||
return deleted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.io.File
|
||||
|
||||
@@ -69,8 +69,9 @@ class AccountSecretsEncryptedStores(
|
||||
serializer = Nip47WalletConnect.Nip47URI::serializer,
|
||||
)
|
||||
|
||||
fun removeAccount(npub: String) {
|
||||
file(npub).delete()
|
||||
fun removeAccount(npub: String): Boolean {
|
||||
val deleted = file(npub).delete()
|
||||
storeCache.remove(npub)
|
||||
return deleted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,16 +29,16 @@ import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.IOException
|
||||
import java.util.Base64
|
||||
import kotlin.io.encoding.Base64
|
||||
|
||||
class EncryptedDataStore(
|
||||
private val store: DataStore<Preferences>,
|
||||
private val encryption: KeyStoreEncryption = KeyStoreEncryption(),
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
private fun decode(str: String): ByteArray = Base64.getDecoder().decode(str)
|
||||
private fun decode(str: String): ByteArray = Base64.decode(str)
|
||||
|
||||
private fun encode(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes)
|
||||
private fun encode(bytes: ByteArray): String = Base64.encode(bytes)
|
||||
|
||||
private fun encrypt(value: String): String = encode(encryption.encrypt(value.toByteArray()))
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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.preferences
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
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(
|
||||
val context: Context,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
companion object {
|
||||
// loads faster when individualized
|
||||
val TOR_TYPE_KEY = stringPreferencesKey("tor.torType")
|
||||
val EXTERNAL_SOCKS_PORT_KEY = intPreferencesKey("tor.externalSocksPort")
|
||||
val ONION_RELAYS_VIA_TOR_KEY = booleanPreferencesKey("tor.onionRelaysViaTor")
|
||||
val DM_RELAYS_VIA_TOR_KEY = booleanPreferencesKey("tor.dmRelaysViaTor")
|
||||
val NEW_RELAYS_VIA_TOR_KEY = booleanPreferencesKey("tor.newRelaysViaTor")
|
||||
val TRUSTED_RELAYS_VIA_TOR_KEY = booleanPreferencesKey("tor.trustedRelaysViaTor")
|
||||
val URL_PREVIEWS_VIA_TOR_KEY = booleanPreferencesKey("tor.urlPreviewsViaTor")
|
||||
val PROFILE_PICS_VIA_TOR_KEY = booleanPreferencesKey("tor.profilePicsViaTor")
|
||||
val IMAGES_VIA_TOR_KEY = booleanPreferencesKey("tor.imagesViaTor")
|
||||
val VIDEOS_VIA_TOR_KEY = booleanPreferencesKey("tor.videosViaTor")
|
||||
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,
|
||||
nip96UploadsViaTor = 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.nip96UploadsViaTor
|
||||
}
|
||||
} 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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.preferences
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.BooleanType
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.ProfileGalleryType
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.model.UiSettings
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
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(
|
||||
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 languageUpdate =
|
||||
value.preferredLanguage
|
||||
.onEach { language ->
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(language),
|
||||
)
|
||||
}.flowOn(Dispatchers.Main)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
value.toSettings(),
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val saving =
|
||||
value.propertyWatchFlow
|
||||
.debounce(1000)
|
||||
.distinctUntilChanged()
|
||||
.onEach(::save)
|
||||
.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}")
|
||||
|
||||
try {
|
||||
val oldVersion = LocalPreferences.loadSharedSettings()
|
||||
if (oldVersion != null) {
|
||||
save(oldVersion)
|
||||
}
|
||||
oldVersion
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
} 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class UpdatablePropertyFlow<T>(
|
||||
) {
|
||||
val stateFlow =
|
||||
flow
|
||||
.flowOn(Dispatchers.Default)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.privacyOptions
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
class EmptyRoleBasedHttpClientBuilder : IRoleBasedHttpClientBuilder {
|
||||
val rootOkHttpClient by lazy {
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun proxyPortForVideo(url: String) = (rootOkHttpClient.proxy?.address() as? InetSocketAddress)?.port
|
||||
|
||||
override fun okHttpClientForNip05(url: String) = rootOkHttpClient
|
||||
|
||||
override fun okHttpClientForUploads(url: String) = rootOkHttpClient
|
||||
|
||||
override fun okHttpClientForImage(url: String) = rootOkHttpClient
|
||||
|
||||
override fun okHttpClientForVideo(url: String) = rootOkHttpClient
|
||||
|
||||
override fun okHttpClientForMoney(url: String) = rootOkHttpClient
|
||||
|
||||
override fun okHttpClientForPreview(url: String) = rootOkHttpClient
|
||||
|
||||
override fun okHttpClientForPushRegistration(url: String) = rootOkHttpClient
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.privacyOptions
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
interface IRoleBasedHttpClientBuilder {
|
||||
fun proxyPortForVideo(url: String): Int?
|
||||
|
||||
fun okHttpClientForNip05(url: String): OkHttpClient
|
||||
|
||||
fun okHttpClientForUploads(url: String): OkHttpClient
|
||||
|
||||
fun okHttpClientForImage(url: String): OkHttpClient
|
||||
|
||||
fun okHttpClientForVideo(url: String): OkHttpClient
|
||||
|
||||
fun okHttpClientForMoney(url: String): OkHttpClient
|
||||
|
||||
fun okHttpClientForPreview(url: String): OkHttpClient
|
||||
|
||||
fun okHttpClientForPushRegistration(url: String): OkHttpClient
|
||||
}
|
||||
@@ -20,18 +20,21 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.privacyOptions
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class PrivacyState(
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
class RoleBasedHttpClientBuilder(
|
||||
val okHttpClient: DualHttpClientManager,
|
||||
val torSettings: TorSettingsFlow,
|
||||
) : IRoleBasedHttpClientBuilder {
|
||||
fun shouldUseTorForImageDownload(url: String) =
|
||||
shouldUseTorFor(
|
||||
url,
|
||||
settings.torSettings.torType.value,
|
||||
settings.torSettings.imagesViaTor.value,
|
||||
torSettings.torType.value,
|
||||
torSettings.imagesViaTor.value,
|
||||
)
|
||||
|
||||
fun shouldUseTorFor(
|
||||
@@ -57,37 +60,37 @@ class PrivacyState(
|
||||
}
|
||||
|
||||
fun shouldUseTorForVideoDownload() =
|
||||
when (settings.torSettings.torType.value) {
|
||||
when (torSettings.torType.value) {
|
||||
TorType.OFF -> false
|
||||
TorType.INTERNAL -> settings.torSettings.videosViaTor.value
|
||||
TorType.EXTERNAL -> settings.torSettings.videosViaTor.value
|
||||
TorType.INTERNAL -> torSettings.videosViaTor.value
|
||||
TorType.EXTERNAL -> torSettings.videosViaTor.value
|
||||
}
|
||||
|
||||
fun shouldUseTorForVideoDownload(url: String) =
|
||||
when (settings.torSettings.torType.value) {
|
||||
when (torSettings.torType.value) {
|
||||
TorType.OFF -> false
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.videosViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.videosViaTor.value)
|
||||
}
|
||||
|
||||
fun shouldUseTorForPreviewUrl(url: String) =
|
||||
when (settings.torSettings.torType.value) {
|
||||
when (torSettings.torType.value) {
|
||||
TorType.OFF -> false
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.urlPreviewsViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.urlPreviewsViaTor.value)
|
||||
}
|
||||
|
||||
fun shouldUseTorForTrustedRelays() =
|
||||
when (settings.torSettings.torType.value) {
|
||||
when (torSettings.torType.value) {
|
||||
TorType.OFF -> false
|
||||
TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value
|
||||
TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value
|
||||
TorType.INTERNAL -> torSettings.trustedRelaysViaTor.value
|
||||
TorType.EXTERNAL -> torSettings.trustedRelaysViaTor.value
|
||||
}
|
||||
|
||||
private fun checkLocalHostOnionAndThen(
|
||||
url: String,
|
||||
final: Boolean,
|
||||
): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final)
|
||||
): Boolean = checkLocalHostOnionAndThen(url, torSettings.onionRelaysViaTor.value, final)
|
||||
|
||||
private fun checkLocalHostOnionAndThen(
|
||||
normalizedUrl: String,
|
||||
@@ -103,23 +106,39 @@ class PrivacyState(
|
||||
}
|
||||
|
||||
fun shouldUseTorForMoneyOperations(url: String) =
|
||||
when (settings.torSettings.torType.value) {
|
||||
when (torSettings.torType.value) {
|
||||
TorType.OFF -> false
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.moneyOperationsViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.moneyOperationsViaTor.value)
|
||||
}
|
||||
|
||||
fun shouldUseTorForNIP05(url: String) =
|
||||
when (settings.torSettings.torType.value) {
|
||||
when (torSettings.torType.value) {
|
||||
TorType.OFF -> false
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip05VerificationsViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip05VerificationsViaTor.value)
|
||||
}
|
||||
|
||||
fun shouldUseTorForUploads(url: String) =
|
||||
when (settings.torSettings.torType.value) {
|
||||
when (torSettings.torType.value) {
|
||||
TorType.OFF -> false
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value)
|
||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip96UploadsViaTor.value)
|
||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip96UploadsViaTor.value)
|
||||
}
|
||||
|
||||
override fun proxyPortForVideo(url: String): Int? = okHttpClient.getCurrentProxyPort(shouldUseTorForVideoDownload(url))
|
||||
|
||||
override fun okHttpClientForNip05(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForNIP05(url))
|
||||
|
||||
override fun okHttpClientForUploads(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForUploads(url))
|
||||
|
||||
override fun okHttpClientForImage(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForImageDownload(url))
|
||||
|
||||
override fun okHttpClientForVideo(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForVideoDownload(url))
|
||||
|
||||
override fun okHttpClientForMoney(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForMoneyOperations(url))
|
||||
|
||||
override fun okHttpClientForPreview(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForPreviewUrl(url))
|
||||
|
||||
override fun okHttpClientForPushRegistration(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForTrustedRelays())
|
||||
}
|
||||
@@ -18,20 +18,27 @@
|
||||
* 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.quartz.nip03Timestamp.ots.okhttp
|
||||
package com.vitorpamplona.amethyst.model.privacyOptions
|
||||
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.CalendarBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendar
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendarAsyncSubmit
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
class OkHttpCalendarBuilder(
|
||||
val clientFn: (url: String) -> OkHttpClient,
|
||||
) : CalendarBuilder {
|
||||
override fun newSyncCalendar(url: String): ICalendar = OkHttpCalendar(url, clientFn(url))
|
||||
class SingleRoleBasedHttpClientBuilder(
|
||||
val okHttpClient: OkHttpClient,
|
||||
) : IRoleBasedHttpClientBuilder {
|
||||
override fun proxyPortForVideo(url: String) = (okHttpClient.proxy?.address() as? InetSocketAddress)?.port
|
||||
|
||||
override fun newAsyncCalendar(
|
||||
url: String,
|
||||
digest: ByteArray,
|
||||
): ICalendarAsyncSubmit = OkHttpCalendarAsyncSubmit(url, digest, clientFn(url))
|
||||
override fun okHttpClientForNip05(url: String) = okHttpClient
|
||||
|
||||
override fun okHttpClientForUploads(url: String) = okHttpClient
|
||||
|
||||
override fun okHttpClientForImage(url: String) = okHttpClient
|
||||
|
||||
override fun okHttpClientForVideo(url: String) = okHttpClient
|
||||
|
||||
override fun okHttpClientForMoney(url: String) = okHttpClient
|
||||
|
||||
override fun okHttpClientForPreview(url: String) = okHttpClient
|
||||
|
||||
override fun okHttpClientForPushRegistration(url: String) = okHttpClient
|
||||
}
|
||||
@@ -70,14 +70,14 @@ class Chatroom : NotesGatherer {
|
||||
}
|
||||
}
|
||||
|
||||
val createdAt = msg.createdAt() ?: 0
|
||||
if (createdAt > (newestMessage?.createdAt() ?: 0)) {
|
||||
val createdAt = msg.createdAt() ?: 0L
|
||||
if (createdAt > (newestMessage?.createdAt() ?: 0L)) {
|
||||
newestMessage = msg
|
||||
}
|
||||
|
||||
val newSubject = msg.event?.subject()
|
||||
|
||||
if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) {
|
||||
if (newSubject != null && (msg.createdAt() ?: 0L) > (subjectCreatedAt ?: 0)) {
|
||||
subject.tryEmit(newSubject)
|
||||
subjectCreatedAt = msg.createdAt()
|
||||
}
|
||||
@@ -96,7 +96,7 @@ class Chatroom : NotesGatherer {
|
||||
msg.removeGatherer(this)
|
||||
|
||||
if (msg == newestMessage) {
|
||||
newestMessage = messages.maxByOrNull { it.createdAt() ?: 0 }
|
||||
newestMessage = messages.maxByOrNull { it.createdAt() ?: 0L }
|
||||
}
|
||||
|
||||
if (msg.event?.subject() == subject.value) {
|
||||
@@ -127,7 +127,7 @@ class Chatroom : NotesGatherer {
|
||||
val sorted = messages.sortedWith(DefaultFeedOrder)
|
||||
|
||||
val toKeep =
|
||||
if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) {
|
||||
if ((sorted.firstOrNull()?.createdAt() ?: 0L) > TimeUtils.oneWeekAgo()) {
|
||||
// Recent messages, keep last 100
|
||||
sorted.take(100).toSet()
|
||||
} else {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user