mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
414 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c128e43fe5 | ||
|
|
f9ddf76686 | ||
|
|
b6cb839fe7 | ||
|
|
2b6a48f391 | ||
|
|
d1da863d3c | ||
|
|
15ab955a7d | ||
|
|
9176151971 | ||
|
|
eba5ecfd91 | ||
|
|
3e43a75acd | ||
|
|
60ba7786a0 | ||
|
|
1773ad5a88 | ||
|
|
26faa2bc69 | ||
|
|
75c7d40f4e | ||
|
|
aa4a3fe9af | ||
|
|
a32f907d6c | ||
|
|
d30e613b17 | ||
|
|
0477f390c2 | ||
|
|
15e5e2834a | ||
|
|
b1399d1c13 | ||
|
|
45446b4125 | ||
|
|
094d7e71e2 | ||
|
|
75423eaaab | ||
|
|
5e42fa5346 | ||
|
|
bc6d5060cb | ||
|
|
e578327c5b | ||
|
|
ce839ed6d9 | ||
|
|
197484dc1b | ||
|
|
60e8ae7b9a | ||
|
|
64ae32d005 | ||
|
|
87b8e18f1c | ||
|
|
47a9a6cc4e | ||
|
|
1a313a9f78 | ||
|
|
3554350ac1 | ||
|
|
e2e912c81f | ||
|
|
fa71161996 | ||
|
|
8de4d461d4 | ||
|
|
f1e3f6a592 | ||
|
|
234ef679c8 | ||
|
|
072000254f | ||
|
|
34321af07a | ||
|
|
7da559a0c9 | ||
|
|
0564a7e1f1 | ||
|
|
347b16ee8b | ||
|
|
edb834db38 | ||
|
|
97d0cf56a7 | ||
|
|
ad83d362da | ||
|
|
f15f6df2ae | ||
|
|
567a86c8a4 | ||
|
|
16c82ed69c | ||
|
|
a543ee54dc | ||
|
|
128831109a | ||
|
|
9641940bd9 | ||
|
|
ad449ba239 | ||
|
|
31e2fcdc21 | ||
|
|
e28b385038 | ||
|
|
8d9ecb2054 | ||
|
|
c57c190344 | ||
|
|
7dd319eb25 | ||
|
|
b4a059c742 | ||
|
|
43d140175c | ||
|
|
1a70a02b44 | ||
|
|
a476298c07 | ||
|
|
a0a7f28304 | ||
|
|
17109e7406 | ||
|
|
c50343cd04 | ||
|
|
8feac270f4 | ||
|
|
c7061066e9 | ||
|
|
217870244b | ||
|
|
6fd2282818 | ||
|
|
b360c3292f | ||
|
|
2a9e1bb08c | ||
|
|
aa4e6c051e | ||
|
|
f6b947430a | ||
|
|
4521317779 | ||
|
|
25ba353488 | ||
|
|
2074b34ec3 | ||
|
|
f63731c9b6 | ||
|
|
fc057f2b29 | ||
|
|
af4dfbf965 | ||
|
|
63c5098558 | ||
|
|
dcad12f836 | ||
|
|
08e84b056e | ||
|
|
f489deb808 | ||
|
|
511a7030b8 | ||
|
|
06a1862d17 | ||
|
|
b730b8c6f6 | ||
|
|
21a45844df | ||
|
|
2480b43778 | ||
|
|
ad6dcfc030 | ||
|
|
c81324843c | ||
|
|
a55da5f62e | ||
|
|
9b26bdac5c | ||
|
|
4bda6fa0cc | ||
|
|
1eaa22fe51 | ||
|
|
8e212bbb55 | ||
|
|
fdf0b69e5d | ||
|
|
4b5ab8eb35 | ||
|
|
349d6cfc22 | ||
|
|
33b8267de7 | ||
|
|
713bd59435 | ||
|
|
8c34394d86 | ||
|
|
6e1de6fd26 | ||
|
|
8d88efd27b | ||
|
|
6aeaeea5d1 | ||
|
|
3153296161 | ||
|
|
692f7c4270 | ||
|
|
cc8d73e119 | ||
|
|
27c2c71879 | ||
|
|
464beccd95 | ||
|
|
7519fc2f10 | ||
|
|
01816b0389 | ||
|
|
cc8e432336 | ||
|
|
d1ed73f78b | ||
|
|
7a76bf2ae5 | ||
|
|
6efb970794 | ||
|
|
a0a8010305 | ||
|
|
9ed1dbac72 | ||
|
|
c7280cf1be | ||
|
|
0ed87db3b3 | ||
|
|
20bc814211 | ||
|
|
0c92c57b79 | ||
|
|
e61404146b | ||
|
|
07dbcbf5c3 | ||
|
|
8421dbfa4c | ||
|
|
d2af7ba407 | ||
|
|
d0718fcda7 | ||
|
|
2372acbb2a | ||
|
|
9374798f92 | ||
|
|
e96015bcbc | ||
|
|
2bd05d80b0 | ||
|
|
3a7b503890 | ||
|
|
d20707f4df | ||
|
|
b01d308d65 | ||
|
|
e0f5c730a6 | ||
|
|
b9086de336 | ||
|
|
405d3d24a0 | ||
|
|
236de87574 | ||
|
|
b12d14d6f5 | ||
|
|
1d42440e26 | ||
|
|
ec065a6595 | ||
|
|
0d83511509 | ||
|
|
9fe08f6c45 | ||
|
|
557f2e1a83 | ||
|
|
efced4670b | ||
|
|
c89596809a | ||
|
|
7ccc93f17f | ||
|
|
f50e511da0 | ||
|
|
3d497ccf4d | ||
|
|
5c21e219fe | ||
|
|
58796ac31d | ||
|
|
66317f5593 | ||
|
|
2f276cb155 | ||
|
|
b1b915b405 | ||
|
|
052f9fa21f | ||
|
|
893b237d6a | ||
|
|
e367f51458 | ||
|
|
fa44b74f09 | ||
|
|
bb00616275 | ||
|
|
581d11702c | ||
|
|
16eb6097ce | ||
|
|
2051a6b2f3 | ||
|
|
02320548e3 | ||
|
|
44a0b59989 | ||
|
|
3288c35bcd | ||
|
|
bbe0b86690 | ||
|
|
afbabfd2d3 | ||
|
|
d404a09f8e | ||
|
|
30e9d4b3cd | ||
|
|
4238978373 | ||
|
|
7f3c7ea69c | ||
|
|
a4202c96d2 | ||
|
|
593f817b6d | ||
|
|
2230d1467c | ||
|
|
af3a52c75e | ||
|
|
f9b1cbc9ba | ||
|
|
0df37d6694 | ||
|
|
9aa511b398 | ||
|
|
5257b1e1cc | ||
|
|
4e9e123666 | ||
|
|
e7fec7caab | ||
|
|
9a4d829f73 | ||
|
|
2423ad6e87 | ||
|
|
dc40856bc2 | ||
|
|
5cbea6999e | ||
|
|
0d18a89177 | ||
|
|
b935f60fcb | ||
|
|
45df7713ce | ||
|
|
b26355097d | ||
|
|
feee845d31 | ||
|
|
660be862d6 | ||
|
|
b5047f213e | ||
|
|
1fafb6a503 | ||
|
|
7c6ec49312 | ||
|
|
6eb7b45990 | ||
|
|
7c7f704173 | ||
|
|
f331398316 | ||
|
|
6c88029fe3 | ||
|
|
aae7236106 | ||
|
|
963a42becf | ||
|
|
f0044afeab | ||
|
|
f9c3ff22e3 | ||
|
|
347b82ef7f | ||
|
|
a42762de53 | ||
|
|
27e5d135bc | ||
|
|
e073e58241 | ||
|
|
a57ba2550c | ||
|
|
a9cbbf66bd | ||
|
|
91caacd36d | ||
|
|
e181296a91 | ||
|
|
6a626c9865 | ||
|
|
7095c475c0 | ||
|
|
c18df3a157 | ||
|
|
aee63de507 | ||
|
|
2f9438d48d | ||
|
|
700974a5ae | ||
|
|
15b8a8027c | ||
|
|
6c0b22f129 | ||
|
|
4ee3694292 | ||
|
|
989099c791 | ||
|
|
a89440cba2 | ||
|
|
39c9b4d50f | ||
|
|
0a6d9a7764 | ||
|
|
372eaabb50 | ||
|
|
409b63b374 | ||
|
|
ca5c0d6ee4 | ||
|
|
faca3615fc | ||
|
|
7e31401b86 | ||
|
|
0fad7b313b | ||
|
|
930d7d0fed | ||
|
|
fb59499a0d | ||
|
|
6e256e465f | ||
|
|
e358387ba1 | ||
|
|
cd9ba9befe | ||
|
|
d6ab376f2d | ||
|
|
cc6551b11b | ||
|
|
5e417dd890 | ||
|
|
95694ac355 | ||
|
|
215fba83cf | ||
|
|
a8a2bda9af | ||
|
|
79ace7f18c | ||
|
|
c448e75953 | ||
|
|
683af698c6 | ||
|
|
fe19ddb259 | ||
|
|
4c3c7e7cba | ||
|
|
f5e08e47a8 | ||
|
|
782df3bcb5 | ||
|
|
1b2f7e21f6 | ||
|
|
4943e1c7ed | ||
|
|
0f066d0817 | ||
|
|
839cadea35 | ||
|
|
8cef7bcb75 | ||
|
|
62ac05ae81 | ||
|
|
4609d6e343 | ||
|
|
e70e8f8437 | ||
|
|
1b57c5636d | ||
|
|
9065f8eea2 | ||
|
|
8c3e87339a | ||
|
|
9ee2452430 | ||
|
|
266dabdd7a | ||
|
|
21898482de | ||
|
|
c281654e97 | ||
|
|
c0ee8e2d61 | ||
|
|
66e4021d7b | ||
|
|
b00781ec32 | ||
|
|
f08a41d7a5 | ||
|
|
6eb0f153fa | ||
|
|
a6bc40515b | ||
|
|
67ef25f538 | ||
|
|
c37adfc6a2 | ||
|
|
d6764e08fd | ||
|
|
ed80511cb6 | ||
|
|
08da97e36d | ||
|
|
a14ab59e78 | ||
|
|
f7c60b3745 | ||
|
|
f8a77d634c | ||
|
|
fdd1bb9810 | ||
|
|
8a1aefeda7 | ||
|
|
64813a4623 | ||
|
|
e86f0c54f3 | ||
|
|
d2f06f5310 | ||
|
|
7a22b3df71 | ||
|
|
97671c570c | ||
|
|
accad0c77a | ||
|
|
a19deb2076 | ||
|
|
bba7dba17c | ||
|
|
985274980a | ||
|
|
4470d01229 | ||
|
|
35a9c07636 | ||
|
|
3fa5877840 | ||
|
|
56bbfe60bb | ||
|
|
d2f604960e | ||
|
|
76a4236b70 | ||
|
|
fece7411c9 | ||
|
|
0e1e379c7d | ||
|
|
b39ac214a5 | ||
|
|
9e9e0667cd | ||
|
|
82f99faec8 | ||
|
|
d07a767fa5 | ||
|
|
ec19038e5c | ||
|
|
eaa09055fc | ||
|
|
0d956c2f39 | ||
|
|
3a4e2e9623 | ||
|
|
d33befe1ad | ||
|
|
b6fd067d48 | ||
|
|
d28f20519b | ||
|
|
239f390e7c | ||
|
|
857d16582a | ||
|
|
aae47aa2e1 | ||
|
|
f115111c49 | ||
|
|
83058500d6 | ||
|
|
2cb1e70170 | ||
|
|
73513713eb | ||
|
|
7bf5c03637 | ||
|
|
d39d1239f6 | ||
|
|
a0271a2982 | ||
|
|
2484927c22 | ||
|
|
bff662b85d | ||
|
|
f293f6c169 | ||
|
|
e9456af15c | ||
|
|
157fad6a84 | ||
|
|
6c137a5174 | ||
|
|
6550bdefd5 | ||
|
|
614e9ff661 | ||
|
|
5da9e430b0 | ||
|
|
fbd32bf426 | ||
|
|
f28d96d303 | ||
|
|
442322f708 | ||
|
|
a463e8889c | ||
|
|
224e54cc72 | ||
|
|
a23cbd1a38 | ||
|
|
548dd9e34f | ||
|
|
c27f4a62b6 | ||
|
|
e18945b378 | ||
|
|
08869ac350 | ||
|
|
5d5c3ae3e3 | ||
|
|
0fa6dce42c | ||
|
|
aad95595eb | ||
|
|
24fede3768 | ||
|
|
a8b6118e2d | ||
|
|
719be8469f | ||
|
|
1496052bba | ||
|
|
545df83177 | ||
|
|
80d506be37 | ||
|
|
ae0ae413e6 | ||
|
|
43339b8d36 | ||
|
|
6a9f9e3f81 | ||
|
|
010402ebd5 | ||
|
|
c07a0065a1 | ||
|
|
704af55636 | ||
|
|
475e1c6fe0 | ||
|
|
21c397b5e0 | ||
|
|
29a002e451 | ||
|
|
035affadb9 | ||
|
|
79e3142f2d | ||
|
|
bf880983a7 | ||
|
|
115746353e | ||
|
|
2fd4521e1d | ||
|
|
3a8a9e296e | ||
|
|
86ebb78823 | ||
|
|
bda0e37767 | ||
|
|
900cc03e4f | ||
|
|
013357ae6b | ||
|
|
a58a67d404 | ||
|
|
2ded31dd55 | ||
|
|
dad455f9a0 | ||
|
|
8b87f04268 | ||
|
|
627c8d1663 | ||
|
|
287a64be11 | ||
|
|
8f24eeaf4a | ||
|
|
39efc2ef9b | ||
|
|
52a24d5223 | ||
|
|
ca8d32b837 | ||
|
|
f508f45e16 | ||
|
|
d7a22f609a | ||
|
|
2503cbaeac | ||
|
|
6bc305dbe6 | ||
|
|
26cbc0754e | ||
|
|
ed5d7335a0 | ||
|
|
76e3b5abe7 | ||
|
|
45445c03e5 | ||
|
|
0d00f2f80a | ||
|
|
27f437020e | ||
|
|
2345c45ebf | ||
|
|
42408978c4 | ||
|
|
a716d13c69 | ||
|
|
77393b433e | ||
|
|
a7c06b46a7 | ||
|
|
1de55bb16f | ||
|
|
20520bbce4 | ||
|
|
42e11d239d | ||
|
|
957ad1e0db | ||
|
|
3123685f88 | ||
|
|
31e8782b1a | ||
|
|
cafad6a003 | ||
|
|
b785e0d505 | ||
|
|
226153a359 | ||
|
|
b70aa54aa3 | ||
|
|
fc96d89a44 | ||
|
|
dd71b931fc | ||
|
|
d6abdf16e0 | ||
|
|
66e18e17c5 | ||
|
|
7b2da8312d | ||
|
|
353a30efe4 | ||
|
|
c71c87e946 | ||
|
|
9ca6151313 | ||
|
|
b322efc286 | ||
|
|
b775c1bf74 | ||
|
|
a6b2671b1c | ||
|
|
abc5d0ea2f | ||
|
|
3c25d91b98 | ||
|
|
3d8aace55a | ||
|
|
bd6202f8e1 | ||
|
|
c1875e1389 | ||
|
|
3a63c1d1ab |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -18,11 +18,11 @@ jobs:
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
||||
@@ -46,11 +46,11 @@ jobs:
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Debug APK
|
||||
path: app/build/outputs/apk/debug/app-debug.apk
|
||||
path: amethyst/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Build Reports
|
||||
path: app/build/reports
|
||||
path: amethyst/build/reports
|
||||
|
||||
|
||||
36
.github/workflows/create-release.yml
vendored
36
.github/workflows/create-release.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Sign AAB (Google Play)
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/bundle/playRelease
|
||||
releaseDirectory: amethyst/build/outputs/bundle/playRelease
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
- name: Sign AAB (F-Droid)
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/bundle/fdroidRelease
|
||||
releaseDirectory: amethyst/build/outputs/bundle/fdroidRelease
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
- name: Sign APK (Google Play)
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/play/release
|
||||
releaseDirectory: amethyst/build/outputs/apk/play/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Sign APK (F-Droid)
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/fdroid/release
|
||||
releaseDirectory: amethyst/build/outputs/apk/fdroid/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
|
||||
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-x86-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
|
||||
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-x86_64-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
|
||||
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-arm64-v8a-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
|
||||
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-armeabi-v7a-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
|
||||
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
|
||||
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
|
||||
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
|
||||
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-arm64-v8a-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
|
||||
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-armeabi-v7a-release-unsigned-signed.apk
|
||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
|
||||
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/bundle/playRelease/app-play-release.aab
|
||||
asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
|
||||
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -225,6 +225,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab
|
||||
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
|
||||
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
|
||||
asset_content_type: application/zip
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,6 +12,7 @@
|
||||
/.idea/ktlint-plugin.xml
|
||||
/.idea/ktfmt.xml
|
||||
/.idea/studiobot.xml
|
||||
/.idea/other.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
@@ -41,6 +42,7 @@ out/
|
||||
.gradle
|
||||
.gradle/
|
||||
build/
|
||||
.kotlin/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.24" />
|
||||
<option name="version" value="2.0.0" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -226,7 +226,7 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
|
||||
- `KEY_PASSWORD` <- `<your password>`
|
||||
- `KEY_STORE_PASSWORD` <- `<your key store password>`
|
||||
- `SIGNING_KEY` <- the data from `<my-release-key.keystore>`
|
||||
3. Change the `versionCode` and `versionName` on `app/build.gradle`
|
||||
3. Change the `versionCode` and `versionName` on `amethyst/build.gradle`
|
||||
4. Commit and push.
|
||||
5. Tag the commit with `v{x.x.x}`
|
||||
6. Let the [Create Release GitHub Action](https://github.com/vitorpamplona/amethyst/actions/workflows/create-release.yml) build a new `aab` file.
|
||||
|
||||
0
app/.gitignore → amethyst/.gitignore
vendored
0
app/.gitignore → amethyst/.gitignore
vendored
@@ -1,20 +1,23 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
alias(libs.plugins.googleServices)
|
||||
alias(libs.plugins.jetbrainsComposeCompiler)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.vitorpamplona.amethyst'
|
||||
compileSdk 34
|
||||
compileSdk libs.versions.android.compileSdk.get().toInteger()
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 376
|
||||
versionName "0.87.7"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"863124b6592359494396c22b917f9d251ad12ac31b09ea29e0265e3c088ce9f8\""
|
||||
minSdk libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode 390
|
||||
versionName "0.89.4"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"1130badb252bdf62aaf93cb65bfa2bb09e7600c7d6764894f2954555b3b73018\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -132,19 +135,11 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
// Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin
|
||||
kotlinCompilerExtensionVersion "1.5.14"
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['/META-INF/{AL2.0,LGPL2.1}', '**/libscrypt.dylib']
|
||||
@@ -160,11 +155,22 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
composeCompiler {
|
||||
reportsDestination = layout.buildDirectory.dir("compose_compiler")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation platform(libs.androidx.compose.bom)
|
||||
|
||||
implementation project(path: ':quartz')
|
||||
implementation project(path: ':commons')
|
||||
implementation project(path: ':ammolite')
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.activity.compose
|
||||
|
||||
@@ -289,34 +295,3 @@ dependencies {
|
||||
debugImplementation libs.androidx.ui.test.manifest
|
||||
}
|
||||
|
||||
// https://gitlab.com/fdroid/wiki/-/wikis/HOWTO:-diff-&-fix-APKs-for-Reproducible-Builds#differing-assetsdexoptbaselineprofm-easy-to-fix
|
||||
// NB: Android Studio can't find the imports; this does not affect the
|
||||
// actual build since Gradle can find them just fine.
|
||||
import com.android.tools.profgen.ArtProfileKt
|
||||
import com.android.tools.profgen.ArtProfileSerializer
|
||||
import com.android.tools.profgen.DexFile
|
||||
|
||||
project.afterEvaluate {
|
||||
tasks.each { task ->
|
||||
if (task.name.startsWith("compile") && task.name.endsWith("ReleaseArtProfile")) {
|
||||
task.doLast {
|
||||
outputs.files.each { file ->
|
||||
if (file.name.endsWith(".profm")) {
|
||||
println("Sorting ${file} ...")
|
||||
def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
|
||||
def profile = ArtProfileKt.ArtProfile(file)
|
||||
def keys = new ArrayList(profile.profileData.keySet())
|
||||
def sortedData = new LinkedHashMap()
|
||||
Collections.sort keys, new DexFile.Companion()
|
||||
keys.each { key -> sortedData[key] = profile.profileData[key] }
|
||||
new FileOutputStream(file).with {
|
||||
write(version.magicBytes$profgen)
|
||||
write(version.versionBytes$profgen)
|
||||
version.write$profgen(it, sortedData, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,3 +63,5 @@
|
||||
-keep class com.vitorpamplona.quartz.events.** { *; }
|
||||
-keep class com.vitorpamplona.amethyst.model.** { *; }
|
||||
-keep class com.vitorpamplona.amethyst.service.** { *; }
|
||||
-keep class com.vitorpamplona.ammolite.service.** { *; }
|
||||
-keep class com.vitorpamplona.ammolite.relays.** { *; }
|
||||
@@ -75,9 +75,21 @@ class ImageUploadTesting {
|
||||
)
|
||||
|
||||
val url = result.tags!!.first { it[0] == "url" }.get(1)
|
||||
val size = result.tags!!.firstOrNull { it[0] == "size" }?.get(1)?.ifBlank { null }
|
||||
val dim = result.tags!!.firstOrNull { it[0] == "dim" }?.get(1)?.ifBlank { null }
|
||||
val hash = result.tags!!.firstOrNull { it[0] == "x" }?.get(1)?.ifBlank { null }
|
||||
val size =
|
||||
result.tags!!
|
||||
.firstOrNull { it[0] == "size" }
|
||||
?.get(1)
|
||||
?.ifBlank { null }
|
||||
val dim =
|
||||
result.tags!!
|
||||
.firstOrNull { it[0] == "dim" }
|
||||
?.get(1)
|
||||
?.ifBlank { null }
|
||||
val hash =
|
||||
result.tags!!
|
||||
.firstOrNull { it[0] == "x" }
|
||||
?.get(1)
|
||||
?.ifBlank { null }
|
||||
val contentType = result.tags!!.first { it[0] == "m" }.get(1)
|
||||
val ox = result.tags!!.first { it[0] == "ox" }.get(1)
|
||||
|
||||
@@ -79,8 +79,7 @@ class ThreadAssemblerTest {
|
||||
{"id":"512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","pubkey":"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194","created_at":1688194800,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","","reply"]],"content":"another one","sig":"b4bc4d8206a08de0a918043129b27c8863d17f60e4f58b4d2b535326625870d32640b04f247eb9eb5df1d62fe98c8e1b0341bdd119655553488300ec9d5b4036"},
|
||||
{"id":"ce6e32e3e17b6901d2cc70b60f3743e24f885bb6e9da6d88cff516079eac1883","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1688234921,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","","reply"]],"content":"yet another one, testing 0.0.3","sig":"13bd41c4029c6a7ee41cb03d12e831e9e9b6e14d43a61c78a72657070b45385ba2ce98e8121049fbcbdb7b2dd777c36c7867ba70b5e6a4798a9b866910ae5b62"}
|
||||
]
|
||||
"""
|
||||
.trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
val header =
|
||||
"""
|
||||
@@ -111,8 +110,7 @@ class ThreadAssemblerTest {
|
||||
["p","b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc"]
|
||||
]
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
@Test
|
||||
fun threadOrderTest() =
|
||||
@@ -55,15 +55,11 @@ object PushDistributorHandler : PushDistributorActions {
|
||||
endpointInternal = ""
|
||||
}
|
||||
|
||||
override fun getSavedDistributor(): String {
|
||||
return unifiedPush.getSavedDistributor(appContext) ?: ""
|
||||
}
|
||||
override fun getSavedDistributor(): String = unifiedPush.getSavedDistributor(appContext) ?: ""
|
||||
|
||||
fun savedDistributorExists(): Boolean = getSavedDistributor().isNotEmpty()
|
||||
|
||||
override fun getInstalledDistributors(): List<String> {
|
||||
return unifiedPush.getDistributors(appContext)
|
||||
}
|
||||
override fun getInstalledDistributors(): List<String> = unifiedPush.getDistributors(appContext)
|
||||
|
||||
fun formattedDistributorNames(): List<String> {
|
||||
val distributorsArray = getInstalledDistributors().toTypedArray()
|
||||
@@ -87,8 +83,7 @@ object PushDistributorHandler : PushDistributorActions {
|
||||
it
|
||||
}
|
||||
as String
|
||||
}
|
||||
.toTypedArray()
|
||||
}.toTypedArray()
|
||||
return distributorsNameArray.toList()
|
||||
}
|
||||
|
||||
@@ -126,8 +126,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
pushHandler.removeEndpoint()
|
||||
}
|
||||
|
||||
fun notificationManager(): NotificationManager {
|
||||
return ContextCompat.getSystemService(appContext, NotificationManager::class.java)
|
||||
fun notificationManager(): NotificationManager =
|
||||
ContextCompat.getSystemService(appContext, NotificationManager::class.java)
|
||||
as NotificationManager
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
@@ -60,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.CheckifItNeedsToRequestNoti
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@@ -79,7 +79,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
||||
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
|
||||
if (readableListWithExplainer.size > 1) {
|
||||
SpinnerSelectionDialog(
|
||||
title = stringResource(id = R.string.select_push_server),
|
||||
title = stringRes(id = R.string.select_push_server),
|
||||
options = readableListWithExplainer,
|
||||
onSelect = { index ->
|
||||
if (list[index] == "None") {
|
||||
@@ -101,9 +101,9 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
||||
} else {
|
||||
AlertDialog(
|
||||
onDismissRequest = { distributorPresent = true },
|
||||
title = { Text(stringResource(R.string.push_server_install_app)) },
|
||||
title = { Text(stringRes(R.string.push_server_install_app)) },
|
||||
text = {
|
||||
val content = stringResource(R.string.push_server_install_app_description)
|
||||
val content = stringRes(R.string.push_server_install_app_description)
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
@@ -128,7 +128,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.quick_action_dont_show_again_button))
|
||||
Text(stringRes(R.string.quick_action_dont_show_again_button))
|
||||
}
|
||||
Button(
|
||||
onClick = { distributorPresent = true },
|
||||
@@ -142,7 +142,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
||||
contentDescription = null,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.error_dialog_button_ok))
|
||||
Text(stringRes(R.string.error_dialog_button_ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,20 +168,19 @@ fun LoadDistributors(onInner: @Composable (String, ImmutableList<String>, Immuta
|
||||
}
|
||||
|
||||
val readableListWithExplainer =
|
||||
PushDistributorHandler.formattedDistributorNames()
|
||||
PushDistributorHandler
|
||||
.formattedDistributorNames()
|
||||
.mapIndexed { index, name ->
|
||||
TitleExplainer(
|
||||
name,
|
||||
stringResource(id = R.string.push_server_uses_app_explainer, list[index]),
|
||||
stringRes(id = R.string.push_server_uses_app_explainer, list[index]),
|
||||
)
|
||||
}
|
||||
.plus(
|
||||
}.plus(
|
||||
TitleExplainer(
|
||||
stringResource(id = R.string.push_server_none),
|
||||
stringResource(id = R.string.push_server_none_explainer),
|
||||
stringRes(id = R.string.push_server_none),
|
||||
stringRes(id = R.string.push_server_none_explainer),
|
||||
),
|
||||
)
|
||||
.toImmutableList()
|
||||
).toImmutableList()
|
||||
|
||||
onInner(
|
||||
currentDistributor,
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@@ -118,7 +118,5 @@ private class PullRefreshNestedScrollConnection(
|
||||
else -> Offset.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
return Velocity(0f, onRelease(available.y))
|
||||
}
|
||||
override suspend fun onPreFling(available: Velocity): Velocity = Velocity(0f, onRelease(available.y))
|
||||
}
|
||||
@@ -63,8 +63,7 @@ fun Modifier.pullRefreshIndicatorTransform(
|
||||
) {
|
||||
this@drawWithContent.drawContent()
|
||||
}
|
||||
}
|
||||
.graphicsLayer {
|
||||
}.graphicsLayer {
|
||||
translationY = state.position - size.height
|
||||
|
||||
if (scale && !state.refreshing) {
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Looper
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.os.StrictMode.VmPolicy
|
||||
@@ -61,7 +62,8 @@ class Amethyst : Application() {
|
||||
}
|
||||
|
||||
val coilCache: DiskCache by lazy {
|
||||
DiskCache.Builder()
|
||||
DiskCache
|
||||
.Builder()
|
||||
.directory(applicationContext.safeCacheDir.resolve("image_cache"))
|
||||
.maxSizePercent(0.2)
|
||||
.maximumMaxSizeBytes(500L * 1024 * 1024) // 250MB
|
||||
@@ -70,17 +72,28 @@ class Amethyst : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
instance = this
|
||||
|
||||
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") {
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy.Builder().detectAll().penaltyLog().build(),
|
||||
ThreadPolicy
|
||||
.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
VmPolicy.Builder().detectAll().penaltyLog().build(),
|
||||
VmPolicy
|
||||
.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
Looper.getMainLooper().setMessageLogging(LogMonitor())
|
||||
ChoreographerHelper.start()
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
@@ -93,13 +106,9 @@ class Amethyst : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
fun imageLoaderBuilder(): ImageLoader.Builder {
|
||||
return ImageLoader.Builder(applicationContext).diskCache { coilCache }
|
||||
}
|
||||
fun imageLoaderBuilder(): ImageLoader.Builder = ImageLoader.Builder(applicationContext).diskCache { coilCache }
|
||||
|
||||
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences {
|
||||
return EncryptedStorage.preferences(instance, npub)
|
||||
}
|
||||
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
|
||||
|
||||
companion object {
|
||||
lateinit var instance: Amethyst
|
||||
@@ -29,16 +29,15 @@ class EncryptedStorage {
|
||||
private const val PREFERENCES_NAME = "secret_keeper"
|
||||
|
||||
// returns the preferences for each account or a global file if null.
|
||||
fun prefsFileName(npub: String? = null): String {
|
||||
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
|
||||
}
|
||||
fun prefsFileName(npub: String? = null): String = if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
|
||||
|
||||
fun preferences(
|
||||
applicationContext: Context,
|
||||
npub: String? = null,
|
||||
): EncryptedSharedPreferences {
|
||||
val masterKey: MasterKey =
|
||||
MasterKey.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||
MasterKey
|
||||
.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
@@ -31,17 +31,19 @@ import com.vitorpamplona.amethyst.model.DefaultReactions
|
||||
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
@@ -90,6 +92,8 @@ private object PrefKeys {
|
||||
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
|
||||
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
|
||||
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
|
||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
||||
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
|
||||
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later
|
||||
@@ -99,14 +103,7 @@ private object PrefKeys {
|
||||
const val WARN_ABOUT_REPORTS = "warn_about_reports"
|
||||
const val FILTER_SPAM_FROM_STRANGERS = "filter_spam_from_strangers"
|
||||
const val LAST_READ_PER_ROUTE = "last_read_route_per_route"
|
||||
const val AUTOMATICALLY_SHOW_IMAGES = "automatically_show_images"
|
||||
const val AUTOMATICALLY_START_PLAYBACK = "automatically_start_playback"
|
||||
const val THEME = "theme"
|
||||
const val PREFERRED_LANGUAGE = "preferred_Language"
|
||||
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
|
||||
const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars"
|
||||
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
|
||||
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
|
||||
const val SIGNER_PACKAGE_NAME = "signer_package_name"
|
||||
const val HAS_DONATED_IN_VERSION = "has_donated_in_version"
|
||||
const val PENDING_ATTESTATIONS = "pending_attestations"
|
||||
@@ -148,7 +145,7 @@ object LocalPreferences {
|
||||
Event.mapper.readValue<List<AccountInfo>>(it)
|
||||
}
|
||||
|
||||
if (newSystemOfAccounts != null && newSystemOfAccounts.isNotEmpty()) {
|
||||
if (!newSystemOfAccounts.isNullOrEmpty()) {
|
||||
savedAccounts = newSystemOfAccounts
|
||||
} else {
|
||||
val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf()
|
||||
@@ -164,6 +161,8 @@ object LocalPreferences {
|
||||
}
|
||||
|
||||
savedAccounts = migrated
|
||||
|
||||
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts)) }.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,9 +264,7 @@ object LocalPreferences {
|
||||
saveToEncryptedStorage(account)
|
||||
}
|
||||
|
||||
fun allSavedAccounts(): List<AccountInfo> {
|
||||
return savedAccounts()
|
||||
}
|
||||
fun allSavedAccounts(): List<AccountInfo> = savedAccounts()
|
||||
|
||||
suspend fun saveToEncryptedStorage(account: Account) {
|
||||
Log.d("LocalPreferences", "Saving to encrypted storage")
|
||||
@@ -318,10 +315,33 @@ object LocalPreferences {
|
||||
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
|
||||
Event.mapper.writeValueAsString(account.zapPaymentRequest),
|
||||
)
|
||||
putString(
|
||||
PrefKeys.LATEST_CONTACT_LIST,
|
||||
Event.mapper.writeValueAsString(account.backupContactList),
|
||||
)
|
||||
if (account.backupContactList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_CONTACT_LIST,
|
||||
Event.mapper.writeValueAsString(account.backupContactList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_CONTACT_LIST)
|
||||
}
|
||||
|
||||
if (account.backupDMRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_DM_RELAY_LIST,
|
||||
Event.mapper.writeValueAsString(account.backupDMRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_DM_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (account.backupNIP65RelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_NIP65_RELAY_LIST,
|
||||
Event.mapper.writeValueAsString(account.backupNIP65RelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
||||
}
|
||||
|
||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
||||
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, account.hideNIP17WarningDialog)
|
||||
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
|
||||
@@ -335,24 +355,21 @@ object LocalPreferences {
|
||||
)
|
||||
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
|
||||
|
||||
if (account.showSensitiveContent == null) {
|
||||
if (account.showSensitiveContent.value == null) {
|
||||
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
|
||||
} else {
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent.value!!)
|
||||
}
|
||||
|
||||
putString(
|
||||
PrefKeys.PENDING_ATTESTATIONS,
|
||||
Event.mapper.writeValueAsString(account.pendingAttestations.value),
|
||||
)
|
||||
}
|
||||
.apply()
|
||||
}.apply()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadCurrentAccountFromEncryptedStorage(): Account? {
|
||||
return currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) }
|
||||
}
|
||||
suspend fun loadCurrentAccountFromEncryptedStorage(): Account? = currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) }
|
||||
|
||||
suspend fun saveSharedSettings(
|
||||
sharedSettings: Settings,
|
||||
@@ -481,8 +498,8 @@ object LocalPreferences {
|
||||
val latestContactList =
|
||||
try {
|
||||
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
|
||||
println("Decoding Contact List: " + it)
|
||||
if (it != null) {
|
||||
if (it != "null") {
|
||||
println("Decoding Contact List: $it")
|
||||
Event.fromJson(it) as ContactListEvent?
|
||||
} else {
|
||||
null
|
||||
@@ -498,6 +515,46 @@ object LocalPreferences {
|
||||
null
|
||||
}
|
||||
|
||||
val latestDmRelayList =
|
||||
try {
|
||||
getString(PrefKeys.LATEST_DM_RELAY_LIST, null)?.let {
|
||||
if (it != "null") {
|
||||
println("Decoding DM Relay List: $it")
|
||||
Event.fromJson(it) as ChatMessageRelayListEvent?
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding DM Relay List ${getString(PrefKeys.LATEST_DM_RELAY_LIST, null)}",
|
||||
e,
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
val latestNip65RelayList =
|
||||
try {
|
||||
getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)?.let {
|
||||
if (it != "null") {
|
||||
println("Decoding NIP65 Relay List: $it")
|
||||
Event.fromJson(it) as AdvertisedRelayListEvent?
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding NIP65 Relay List ${getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)}",
|
||||
e,
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
val pendingAttestations =
|
||||
try {
|
||||
getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let {
|
||||
@@ -512,7 +569,7 @@ object LocalPreferences {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}",
|
||||
"Error Decoding Contact List ${getString(PrefKeys.PENDING_ATTESTATIONS, null)}",
|
||||
e,
|
||||
)
|
||||
null
|
||||
@@ -605,9 +662,11 @@ object LocalPreferences {
|
||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
||||
backupContactList = latestContactList,
|
||||
backupNIP65RelayList = latestNip65RelayList,
|
||||
backupDMRelayList = latestDmRelayList,
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
showSensitiveContent = MutableStateFlow(showSensitiveContent),
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = lastReadPerRoute,
|
||||
198
amethyst/src/main/java/com/vitorpamplona/amethyst/LogMonitor.kt
Normal file
198
amethyst/src/main/java/com/vitorpamplona/amethyst/LogMonitor.kt
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.util.Printer
|
||||
import android.view.Choreographer
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class LogMonitor : Printer {
|
||||
private val mStackSampler: StackSampler
|
||||
private var mPrintingStarted = false
|
||||
private var mStartTimestamp: Long = 0
|
||||
|
||||
// threshold
|
||||
private val mBlockThresholdMillis: Long = 16
|
||||
|
||||
// Sampling frequency
|
||||
private val mSampleInterval: Long = 1000
|
||||
|
||||
private val mLogHandler: Handler
|
||||
|
||||
init {
|
||||
mStackSampler = StackSampler(mSampleInterval)
|
||||
val handlerThread = HandlerThread("block-canary-io")
|
||||
handlerThread.start()
|
||||
mLogHandler = Handler(handlerThread.getLooper())
|
||||
}
|
||||
|
||||
override fun println(x: String) {
|
||||
// From if to Else, execute DispatchMessage. If the execution takes more than the threshold, the output stuck information
|
||||
if (!mPrintingStarted) {
|
||||
// Record start time
|
||||
mStartTimestamp = System.currentTimeMillis()
|
||||
mPrintingStarted = true
|
||||
mStackSampler.startDump()
|
||||
} else {
|
||||
val endTime = System.currentTimeMillis()
|
||||
|
||||
if (x.indexOf("com.vitorpamplona.amethyst") > 0) {
|
||||
Log.d("block-canary", "Looper ${endTime - mStartTimestamp}ms for $x")
|
||||
}
|
||||
|
||||
mPrintingStarted = false
|
||||
//
|
||||
if (isBlock(endTime)) {
|
||||
notifyBlockEvent(endTime)
|
||||
}
|
||||
mStackSampler.stopDump()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyBlockEvent(endTime: Long) {
|
||||
mLogHandler.post {
|
||||
// Obtain the stack of the main thread stack
|
||||
val stacks: List<String> = mStackSampler.getStacks(mStartTimestamp, endTime)
|
||||
for (stack in stacks) {
|
||||
Log.e("block-canary", stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBlock(endTime: Long): Boolean = endTime - mStartTimestamp > mBlockThresholdMillis
|
||||
}
|
||||
|
||||
class StackSampler(
|
||||
private val mSampleInterval: Long,
|
||||
) {
|
||||
private val mHandler: Handler
|
||||
private val mStackMap: MutableMap<Long, String> = LinkedHashMap()
|
||||
private val mMaxCount = 100
|
||||
|
||||
// Whether to sample
|
||||
var mShouldSample: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
/**
|
||||
* Start sampling and execute stack
|
||||
*/
|
||||
fun startDump() {
|
||||
// Avoid repeating start
|
||||
if (mShouldSample.get()) {
|
||||
return
|
||||
}
|
||||
mShouldSample.set(true)
|
||||
mHandler.removeCallbacks(mRunnable)
|
||||
mHandler.postDelayed(mRunnable, mSampleInterval)
|
||||
}
|
||||
|
||||
fun stopDump() {
|
||||
if (!mShouldSample.get()) {
|
||||
return
|
||||
}
|
||||
mShouldSample.set(false)
|
||||
mHandler.removeCallbacks(mRunnable)
|
||||
}
|
||||
|
||||
fun getStacks(
|
||||
startTime: Long,
|
||||
endTime: Long,
|
||||
): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
synchronized(mStackMap) {
|
||||
for (entryTime in mStackMap.keys) {
|
||||
if (startTime < entryTime && entryTime < endTime) {
|
||||
result.add(
|
||||
TIME_FORMATTER.format(entryTime) +
|
||||
SEPARATOR +
|
||||
SEPARATOR +
|
||||
mStackMap[entryTime],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
init {
|
||||
val handlerThread = HandlerThread("block-canary-sampler")
|
||||
handlerThread.start()
|
||||
mHandler = Handler(handlerThread.looper)
|
||||
}
|
||||
|
||||
private val mRunnable: Runnable =
|
||||
object : Runnable {
|
||||
override fun run() {
|
||||
val sb = StringBuilder()
|
||||
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||
for (s in stackTrace) {
|
||||
sb.append(s.toString()).append("\n")
|
||||
}
|
||||
synchronized(mStackMap) {
|
||||
// Save up to 100 stack information
|
||||
if (mStackMap.size == mMaxCount) {
|
||||
mStackMap.remove(mStackMap.keys.iterator().next())
|
||||
}
|
||||
mStackMap.put(System.currentTimeMillis(), sb.toString())
|
||||
}
|
||||
if (mShouldSample.get()) {
|
||||
mHandler.postDelayed(this, mSampleInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SEPARATOR: String = "\r\n"
|
||||
val TIME_FORMATTER: SimpleDateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS")
|
||||
}
|
||||
}
|
||||
|
||||
object ChoreographerHelper {
|
||||
var lastFrameTimeNanos: Long = 0
|
||||
|
||||
fun start() {
|
||||
Choreographer.getInstance().postFrameCallback(
|
||||
object : Choreographer.FrameCallback {
|
||||
override fun doFrame(frameTimeNanos: Long) {
|
||||
// Last callback time
|
||||
if (lastFrameTimeNanos == 0L) {
|
||||
lastFrameTimeNanos = frameTimeNanos
|
||||
Choreographer.getInstance().postFrameCallback(this)
|
||||
return
|
||||
}
|
||||
val diff = (frameTimeNanos - lastFrameTimeNanos) / 1000000
|
||||
// only report after 30ms because videos play at 30fps
|
||||
if (diff > 35) {
|
||||
// Follow the frame number
|
||||
val droppedCount = (diff / 16.6).toInt()
|
||||
Log.w("block-canary", "Dropped $droppedCount frames. Skipped $diff ms")
|
||||
}
|
||||
lastFrameTimeNanos = frameTimeNanos
|
||||
Choreographer.getInstance().postFrameCallback(this)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import coil.decode.SvgDecoder
|
||||
import coil.size.Precision
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||
@@ -47,7 +46,8 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.ammolite.relays.Client
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
@@ -86,6 +86,7 @@ class ServiceManager {
|
||||
|
||||
// Resets Proxy Use
|
||||
HttpClientManager.setDefaultProxy(account?.proxy)
|
||||
HttpClientManager.setDefaultUserAgent("Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
|
||||
Coil.setImageLoader {
|
||||
Amethyst.instance
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,15 +25,18 @@ import android.util.LruCache
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayStats
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.RelayStats
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set<HexKey>)
|
||||
data class Spammer(
|
||||
val pubkeyHex: HexKey,
|
||||
var duplicatedMessages: Set<HexKey>,
|
||||
)
|
||||
|
||||
class AntiSpamFilter {
|
||||
val recentMessages = LruCache<Int, String>(1000)
|
||||
@@ -109,7 +112,9 @@ class AntiSpamFilter {
|
||||
}
|
||||
|
||||
@Stable
|
||||
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
||||
class AntiSpamLiveData(
|
||||
val cache: AntiSpamFilter,
|
||||
) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
@@ -126,4 +131,6 @@ class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(Anti
|
||||
}
|
||||
}
|
||||
|
||||
class AntiSpamState(val cache: AntiSpamFilter)
|
||||
class AntiSpamState(
|
||||
val cache: AntiSpamFilter,
|
||||
)
|
||||
@@ -25,9 +25,9 @@ import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.commons.data.LargeCache
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
@@ -37,7 +37,9 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@Stable
|
||||
class PublicChatChannel(idHex: String) : Channel(idHex) {
|
||||
class PublicChatChannel(
|
||||
idHex: String,
|
||||
) : Channel(idHex) {
|
||||
var info = ChannelCreateEvent.ChannelData(null, null, null)
|
||||
|
||||
fun updateChannelInfo(
|
||||
@@ -49,26 +51,22 @@ class PublicChatChannel(idHex: String) : Channel(idHex) {
|
||||
super.updateChannelInfo(creator, updatedAt)
|
||||
}
|
||||
|
||||
override fun toBestDisplayName(): String {
|
||||
return info.name ?: super.toBestDisplayName()
|
||||
}
|
||||
override fun toBestDisplayName(): String = info.name ?: super.toBestDisplayName()
|
||||
|
||||
override fun summary(): String? {
|
||||
return info.about
|
||||
}
|
||||
override fun summary(): String? = info.about
|
||||
|
||||
override fun profilePicture(): String? {
|
||||
if (info.picture.isNullOrBlank()) return super.profilePicture()
|
||||
return info.picture ?: super.profilePicture()
|
||||
}
|
||||
|
||||
override fun anyNameStartsWith(prefix: String): Boolean {
|
||||
return listOfNotNull(info.name, info.about).filter { it.contains(prefix, true) }.isNotEmpty()
|
||||
}
|
||||
override fun anyNameStartsWith(prefix: String): Boolean = listOfNotNull(info.name, info.about).filter { it.contains(prefix, true) }.isNotEmpty()
|
||||
}
|
||||
|
||||
@Stable
|
||||
class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) {
|
||||
class LiveActivitiesChannel(
|
||||
val address: ATag,
|
||||
) : Channel(address.toTag()) {
|
||||
var info: LiveActivitiesEvent? = null
|
||||
|
||||
override fun idNote() = address.toNAddr()
|
||||
@@ -86,27 +84,22 @@ class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) {
|
||||
super.updateChannelInfo(creator, updatedAt)
|
||||
}
|
||||
|
||||
override fun toBestDisplayName(): String {
|
||||
return info?.title() ?: super.toBestDisplayName()
|
||||
}
|
||||
override fun toBestDisplayName(): String = info?.title() ?: super.toBestDisplayName()
|
||||
|
||||
override fun summary(): String? {
|
||||
return info?.summary()
|
||||
}
|
||||
override fun summary(): String? = info?.summary()
|
||||
|
||||
override fun profilePicture(): String? {
|
||||
return info?.image()?.ifBlank { null }
|
||||
}
|
||||
override fun profilePicture(): String? = info?.image()?.ifBlank { null }
|
||||
|
||||
override fun anyNameStartsWith(prefix: String): Boolean {
|
||||
return listOfNotNull(info?.title(), info?.summary())
|
||||
override fun anyNameStartsWith(prefix: String): Boolean =
|
||||
listOfNotNull(info?.title(), info?.summary())
|
||||
.filter { it.contains(prefix, true) }
|
||||
.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
abstract class Channel(val idHex: String) {
|
||||
abstract class Channel(
|
||||
val idHex: String,
|
||||
) {
|
||||
var creator: User? = null
|
||||
var updatedMetadataAt: Long = 0
|
||||
val notes = LargeCache<HexKey, Note>()
|
||||
@@ -118,21 +111,13 @@ abstract class Channel(val idHex: String) {
|
||||
|
||||
open fun idDisplayNote() = idNote().toShortenHex()
|
||||
|
||||
open fun toBestDisplayName(): String {
|
||||
return idDisplayNote()
|
||||
}
|
||||
open fun toBestDisplayName(): String = idDisplayNote()
|
||||
|
||||
open fun summary(): String? {
|
||||
return null
|
||||
}
|
||||
open fun summary(): String? = null
|
||||
|
||||
open fun creatorName(): String? {
|
||||
return creator?.toBestDisplayName()
|
||||
}
|
||||
open fun creatorName(): String? = creator?.toBestDisplayName()
|
||||
|
||||
open fun profilePicture(): String? {
|
||||
return creator?.info?.banner
|
||||
}
|
||||
open fun profilePicture(): String? = creator?.info?.banner
|
||||
|
||||
open fun updateChannelInfo(
|
||||
creator: User,
|
||||
@@ -167,10 +152,10 @@ abstract class Channel(val idHex: String) {
|
||||
|
||||
fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
|
||||
val important =
|
||||
notes.filter { key, it ->
|
||||
it.author?.let { author -> account.isHidden(author) } == false
|
||||
}
|
||||
.sortedWith(DefaultFeedOrder)
|
||||
notes
|
||||
.filter { key, it ->
|
||||
it.author?.let { author -> account.isHidden(author) } == false
|
||||
}.sortedWith(DefaultFeedOrder)
|
||||
.take(500)
|
||||
.toSet()
|
||||
|
||||
@@ -182,7 +167,9 @@ abstract class Channel(val idHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelState(channel)) {
|
||||
class ChannelLiveData(
|
||||
val channel: Channel,
|
||||
) : LiveData<ChannelState>(ChannelState(channel)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
@@ -208,4 +195,6 @@ class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelStat
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelState(val channel: Channel)
|
||||
class ChannelState(
|
||||
val channel: Channel,
|
||||
)
|
||||
@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Stable
|
||||
class Chatroom() {
|
||||
class Chatroom {
|
||||
var authors: Set<User> = setOf()
|
||||
var roomMessages: Set<Note> = setOf()
|
||||
var subject: String? = null
|
||||
@@ -72,9 +72,7 @@ class Chatroom() {
|
||||
}
|
||||
}
|
||||
|
||||
fun senderIntersects(keySet: Set<HexKey>): Boolean {
|
||||
return authors.any { it.pubkeyHex in keySet }
|
||||
}
|
||||
fun senderIntersects(keySet: Set<HexKey>): Boolean = authors.any { it.pubkeyHex in keySet }
|
||||
|
||||
fun pruneMessagesToTheLatestOnly(): Set<Note> {
|
||||
val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||
@@ -68,8 +68,8 @@ fun RenderHashTagIcons() {
|
||||
}
|
||||
}
|
||||
|
||||
fun checkForHashtagWithIcon(tag: String): HashtagIcon? {
|
||||
return when (tag.lowercase()) {
|
||||
fun checkForHashtagWithIcon(tag: String): HashtagIcon? =
|
||||
when (tag.lowercase()) {
|
||||
"₿itcoin", "bitcoin", "btc", "timechain", "bitcoiner", "bitcoiners" -> bitcoin
|
||||
"nostr", "nostrich", "nostriches", "thenostr" -> nostr
|
||||
"lightning", "lightningnetwork" -> lightning
|
||||
@@ -86,7 +86,6 @@ fun checkForHashtagWithIcon(tag: String): HashtagIcon? {
|
||||
"weed", "weedstr", "420", "cannabis", "marijuana" -> weed
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val bitcoin = HashtagIcon(CustomHashTagIcons.Btc, "Bitcoin", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
|
||||
val nostr = HashtagIcon(CustomHashTagIcons.Nostr, "Nostr", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
|
||||
@@ -29,8 +29,8 @@ import com.vitorpamplona.amethyst.commons.data.LargeCache
|
||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor
|
||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||
import com.vitorpamplona.ammolite.relays.BundledInsert
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.HexValidator
|
||||
@@ -103,6 +103,7 @@ import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.RecommendRelayEvent
|
||||
import com.vitorpamplona.quartz.events.RelaySetEvent
|
||||
@@ -148,8 +149,8 @@ object LocalCache {
|
||||
|
||||
val deletionIndex = DeletionIndex()
|
||||
|
||||
private val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>>(10)
|
||||
private val observablesByKindAndAuthor = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindAndAuthor<Event>>>(10)
|
||||
val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>>(10)
|
||||
val observablesByKindAndAuthor = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindAndAuthor<Event>>>(10)
|
||||
|
||||
fun <T : Event> observeETag(
|
||||
kind: Int,
|
||||
@@ -209,13 +210,14 @@ object LocalCache {
|
||||
} as LatestByKindAndAuthor<T>
|
||||
}
|
||||
|
||||
fun updateObservables(event: Event) {
|
||||
val observablesOfKind = observablesByKindAndETag[event.kind()] ?: return
|
||||
event.forEachTaggedEvent {
|
||||
observablesOfKind[it]?.updateIfMatches(event)
|
||||
private fun updateObservables(event: Event) {
|
||||
observablesByKindAndETag[event.kind]?.let { observablesOfKind ->
|
||||
event.forEachTaggedEvent {
|
||||
observablesOfKind[it]?.updateIfMatches(event)
|
||||
}
|
||||
}
|
||||
|
||||
observablesByKindAndAuthor[event.kind()]?.get(event.pubKey)?.updateIfMatches(event)
|
||||
observablesByKindAndAuthor[event.kind]?.get(event.pubKey)?.updateIfMatches(event)
|
||||
}
|
||||
|
||||
fun checkGetOrCreateUser(key: String): User? {
|
||||
@@ -241,33 +243,25 @@ object LocalCache {
|
||||
return users.get(key)
|
||||
}
|
||||
|
||||
fun getAddressableNoteIfExists(key: String): AddressableNote? {
|
||||
return addressables.get(key)
|
||||
}
|
||||
fun getAddressableNoteIfExists(key: String): AddressableNote? = addressables.get(key)
|
||||
|
||||
fun getNoteIfExists(key: String): Note? {
|
||||
return addressables.get(key) ?: notes.get(key)
|
||||
}
|
||||
fun getNoteIfExists(key: String): Note? = addressables.get(key) ?: notes.get(key)
|
||||
|
||||
fun getChannelIfExists(key: String): Channel? {
|
||||
return channels.get(key)
|
||||
}
|
||||
fun getChannelIfExists(key: String): Channel? = channels.get(key)
|
||||
|
||||
fun getNoteIfExists(event: Event): Note? {
|
||||
return if (event is AddressableEvent) {
|
||||
fun getNoteIfExists(event: Event): Note? =
|
||||
if (event is AddressableEvent) {
|
||||
getAddressableNoteIfExists(event.addressTag())
|
||||
} else {
|
||||
getNoteIfExists(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreateNote(event: Event): Note {
|
||||
return if (event is AddressableEvent) {
|
||||
fun getOrCreateNote(event: Event): Note =
|
||||
if (event is AddressableEvent) {
|
||||
getOrCreateAddressableNote(event.address())
|
||||
} else {
|
||||
getOrCreateNote(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkGetOrCreateNote(key: String): Note? {
|
||||
checkNotInMainThread()
|
||||
@@ -348,8 +342,8 @@ object LocalCache {
|
||||
return HexValidator.isHex(key)
|
||||
}
|
||||
|
||||
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? {
|
||||
return try {
|
||||
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? =
|
||||
try {
|
||||
val addr = ATag.parse(key, null) // relay doesn't matter for the index.
|
||||
if (addr != null) {
|
||||
getOrCreateAddressableNote(addr)
|
||||
@@ -360,7 +354,6 @@ object LocalCache {
|
||||
Log.e("LocalCache", "Invalid Key to create channel: $key", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote {
|
||||
// checkNotInMainThread()
|
||||
@@ -395,6 +388,10 @@ object LocalCache {
|
||||
val newUserMetadata = event.contactMetaData()
|
||||
if (newUserMetadata != null) {
|
||||
oldUser.updateUserInfo(newUserMetadata, event)
|
||||
if (relay != null) {
|
||||
oldUser.addRelayBeingUsed(relay, event.createdAt)
|
||||
oldUser.latestMetadataRelay = relay.url
|
||||
}
|
||||
}
|
||||
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex()} ${oldUser.toBestDisplayName()} from ${relay?.url}")
|
||||
} else {
|
||||
@@ -428,11 +425,11 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun formattedDateTime(timestamp: Long): String {
|
||||
return Instant.ofEpochSecond(timestamp)
|
||||
fun formattedDateTime(timestamp: Long): String =
|
||||
Instant
|
||||
.ofEpochSecond(timestamp)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: TextNoteEvent,
|
||||
@@ -755,8 +752,8 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun computeReplyTo(event: Event): List<Note> {
|
||||
return when (event) {
|
||||
fun computeReplyTo(event: Event): List<Note> =
|
||||
when (event) {
|
||||
is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
@@ -800,12 +797,11 @@ object LocalCache {
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
|
||||
is DraftEvent -> {
|
||||
event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { checkGetOrCreateAddressableNote(it.toTag()) }
|
||||
event.mapTaggedEvent { checkGetOrCreateNote(it) } + event.mapTaggedAddress { checkGetOrCreateAddressableNote(it) }
|
||||
}
|
||||
|
||||
else -> emptyList<Note>()
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: PollNoteEvent,
|
||||
@@ -1218,9 +1214,23 @@ object LocalCache {
|
||||
if (deletionIndex.add(event)) {
|
||||
var deletedAtLeastOne = false
|
||||
|
||||
event.deleteEvents()
|
||||
event
|
||||
.deleteEvents()
|
||||
.mapNotNull { getNoteIfExists(it) }
|
||||
.forEach { deleteNote ->
|
||||
val deleteNoteEvent = deleteNote.event
|
||||
if (deleteNoteEvent is AddressableEvent) {
|
||||
val addressableNote = getAddressableNoteIfExists(deleteNoteEvent.addressTag())
|
||||
if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0) <= event.createdAt) {
|
||||
// Counts the replies
|
||||
deleteNote(addressableNote)
|
||||
|
||||
addressables.remove(addressableNote.idHex)
|
||||
|
||||
deletedAtLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
// must be the same author
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey) {
|
||||
// reverts the add
|
||||
@@ -1673,6 +1683,26 @@ object LocalCache {
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: ProfileGalleryEntryEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: FileStorageHeaderEvent,
|
||||
relay: Relay?,
|
||||
@@ -2030,16 +2060,16 @@ object LocalCache {
|
||||
suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
|
||||
checkNotInMainThread()
|
||||
|
||||
return addressables.filter { _, it ->
|
||||
val noteEvent = it.event
|
||||
(
|
||||
noteEvent is StatusEvent &&
|
||||
noteEvent.pubKey == user.pubkeyHex &&
|
||||
!noteEvent.isExpired() &&
|
||||
noteEvent.content.isNotBlank()
|
||||
)
|
||||
}
|
||||
.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
|
||||
return addressables
|
||||
.filter { _, it ->
|
||||
val noteEvent = it.event
|
||||
(
|
||||
noteEvent is StatusEvent &&
|
||||
noteEvent.pubKey == user.pubkeyHex &&
|
||||
!noteEvent.isExpired() &&
|
||||
noteEvent.content.isNotBlank()
|
||||
)
|
||||
}.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
.toImmutableList()
|
||||
}
|
||||
@@ -2066,9 +2096,7 @@ object LocalCache {
|
||||
|
||||
val modificationCache = LruCache<HexKey, List<Note>>(20)
|
||||
|
||||
fun cachedModificationEventsForNote(note: Note): List<Note>? {
|
||||
return modificationCache[note.idHex]
|
||||
}
|
||||
fun cachedModificationEventsForNote(note: Note): List<Note>? = modificationCache[note.idHex]
|
||||
|
||||
suspend fun findLatestModificationForNote(note: Note): List<Note> {
|
||||
checkNotInMainThread()
|
||||
@@ -2082,11 +2110,12 @@ object LocalCache {
|
||||
val time = TimeUtils.now()
|
||||
|
||||
val newNotes =
|
||||
notes.filter { _, item ->
|
||||
val noteEvent = item.event
|
||||
notes
|
||||
.filter { _, item ->
|
||||
val noteEvent = item.event
|
||||
|
||||
noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
|
||||
}.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
|
||||
}.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
|
||||
modificationCache.put(note.idHex, newNotes)
|
||||
|
||||
@@ -2210,9 +2239,11 @@ object LocalCache {
|
||||
note.event is GenericRepostEvent
|
||||
) &&
|
||||
note.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
|
||||
note.liveSet?.isInUse() != true && // don't delete if observing.
|
||||
note.liveSet?.isInUse() != true &&
|
||||
// don't delete if observing.
|
||||
note.author?.pubkeyHex !in
|
||||
accounts && // don't delete if it is the logged in account
|
||||
accounts &&
|
||||
// don't delete if it is the logged in account
|
||||
note.event?.isTaggedUsers(accounts) !=
|
||||
true // don't delete if it's a notification to the logged in user
|
||||
}
|
||||
@@ -2308,8 +2339,7 @@ object LocalCache {
|
||||
?.hiddenUsers
|
||||
?.map { userHex ->
|
||||
(notes.filter { _, it -> it.event?.pubKey() == userHex } + addressables.filter { _, it -> it.event?.pubKey() == userHex }).toSet()
|
||||
}
|
||||
?.flatten()
|
||||
}?.flatten()
|
||||
?: emptyList()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
@@ -2341,7 +2371,7 @@ object LocalCache {
|
||||
}
|
||||
|
||||
// Observers line up here.
|
||||
val live: LocalCacheLiveData = LocalCacheLiveData()
|
||||
val live: LocalCacheFlow = LocalCacheFlow()
|
||||
|
||||
private fun refreshObservers(newNote: Note) {
|
||||
updateObservables(newNote.event as Event)
|
||||
@@ -2489,7 +2519,28 @@ object LocalCache {
|
||||
event: Event,
|
||||
relay: Relay?,
|
||||
) {
|
||||
if (deletionIndex.hasBeenDeleted(event)) return
|
||||
if (deletionIndex.hasBeenDeleted(event)) {
|
||||
// update relay with deletion event from another.
|
||||
if (relay != null) {
|
||||
deletionIndex.hasBeenDeletedBy(event)?.let {
|
||||
Log.d("LocalCache", "Updating ${relay.url} with a Deletion Event ${it.toJson()} because of ${event.toJson()}")
|
||||
relay.send(it)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event is AddressableEvent && relay != null) {
|
||||
// updates relay with a new event.
|
||||
getAddressableNoteIfExists(event.addressTag())?.let {
|
||||
it.event?.let { existingEvent ->
|
||||
if (existingEvent.createdAt() > event.createdAt) {
|
||||
Log.d("LocalCache", "Updating ${relay.url} with a new version of ${event.toJson()} to ${existingEvent.toJson()}")
|
||||
relay.send(existingEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -2534,6 +2585,7 @@ object LocalCache {
|
||||
}
|
||||
is FhirResourceEvent -> consume(event, relay)
|
||||
is FileHeaderEvent -> consume(event, relay)
|
||||
is ProfileGalleryEntryEvent -> consume(event, relay)
|
||||
is FileServersEvent -> consume(event, relay)
|
||||
is FileStorageEvent -> consume(event, relay)
|
||||
is FileStorageHeaderEvent -> consume(event, relay)
|
||||
@@ -2596,8 +2648,8 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun hasConsumed(notificationEvent: Event): Boolean {
|
||||
return if (notificationEvent is AddressableEvent) {
|
||||
fun hasConsumed(notificationEvent: Event): Boolean =
|
||||
if (notificationEvent is AddressableEvent) {
|
||||
val note = addressables.get(notificationEvent.addressTag())
|
||||
val noteEvent = note?.event
|
||||
noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt()
|
||||
@@ -2605,11 +2657,10 @@ object LocalCache {
|
||||
val note = notes.get(notificationEvent.id)
|
||||
note?.event != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class LocalCacheLiveData {
|
||||
class LocalCacheFlow {
|
||||
private val _newEventBundles = MutableSharedFlow<Set<Note>>(0, 10, BufferOverflow.DROP_OLDEST)
|
||||
val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view
|
||||
|
||||
@@ -2617,8 +2668,7 @@ class LocalCacheLiveData {
|
||||
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
|
||||
|
||||
fun invalidateData(newNote: Note) {
|
||||
bundler.invalidateList(newNote) {
|
||||
bundledNewNotes ->
|
||||
bundler.invalidateList(newNote) { bundledNewNotes ->
|
||||
_newEventBundles.emit(bundledNewNotes)
|
||||
}
|
||||
}
|
||||
@@ -66,9 +66,7 @@ class HumanName(
|
||||
var family: String? = null,
|
||||
var given: ArrayList<String> = arrayListOf(),
|
||||
) {
|
||||
fun assembleName(): String {
|
||||
return given.joinToString(" ") + " " + family
|
||||
}
|
||||
fun assembleName(): String = given.joinToString(" ") + " " + family
|
||||
}
|
||||
|
||||
class Bundle(
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
@@ -29,18 +28,18 @@ import androidx.lifecycle.distinctUntilChanged
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.actions.relays.updated
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.combineWith
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.encoders.toNote
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
@@ -75,7 +74,9 @@ import java.math.BigDecimal
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Stable
|
||||
class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
||||
class AddressableNote(
|
||||
val address: ATag,
|
||||
) : Note(address.toTag()) {
|
||||
override fun idNote() = address.toNAddr()
|
||||
|
||||
override fun toNEvent() = address.toNAddr()
|
||||
@@ -93,9 +94,7 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
||||
return minOf(publishedAt, lastCreatedAt)
|
||||
}
|
||||
|
||||
fun dTag(): String? {
|
||||
return (event as? AddressableEvent)?.dTag()
|
||||
}
|
||||
fun dTag(): String? = (event as? AddressableEvent)?.dTag()
|
||||
|
||||
override fun wasOrShouldBeDeletedBy(
|
||||
deletionEvents: Set<HexKey>,
|
||||
@@ -107,7 +106,9 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
||||
}
|
||||
|
||||
@Stable
|
||||
open class Note(val idHex: String) {
|
||||
open class Note(
|
||||
val idHex: String,
|
||||
) {
|
||||
// These fields are only available after the Text Note event is received.
|
||||
// They are immutable after that.
|
||||
var event: EventInterface? = null
|
||||
@@ -138,6 +139,8 @@ open class Note(val idHex: String) {
|
||||
var relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
||||
private set
|
||||
|
||||
var associatedNote: Note? = null
|
||||
|
||||
var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()
|
||||
|
||||
fun id() = Hex.decode(idHex)
|
||||
@@ -163,14 +166,12 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun toNostrUri(): String {
|
||||
return "nostr:${toNEvent()}"
|
||||
}
|
||||
fun toNostrUri(): String = "nostr:${toNEvent()}"
|
||||
|
||||
open fun idDisplayNote() = idNote().toShortenHex()
|
||||
|
||||
fun channelHex(): HexKey? {
|
||||
return if (
|
||||
fun channelHex(): HexKey? =
|
||||
if (
|
||||
event is ChannelMessageEvent ||
|
||||
event is ChannelMetadataEvent ||
|
||||
event is ChannelCreateEvent ||
|
||||
@@ -185,7 +186,6 @@ open class Note(val idHex: String) {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
open fun address(): ATag? = null
|
||||
|
||||
@@ -259,7 +259,9 @@ open class Note(val idHex: String) {
|
||||
if (repliesChanged) liveSet?.innerReplies?.invalidateData()
|
||||
if (reactionsChanged) liveSet?.innerReactions?.invalidateData()
|
||||
if (boostsChanged) liveSet?.innerBoosts?.invalidateData()
|
||||
if (reportsChanged) liveSet?.innerReports?.invalidateData()
|
||||
if (reportsChanged) {
|
||||
flowSet?.reports?.invalidateData()
|
||||
}
|
||||
if (zapsChanged) liveSet?.innerZaps?.invalidateData()
|
||||
|
||||
return toBeRemoved
|
||||
@@ -291,7 +293,7 @@ open class Note(val idHex: String) {
|
||||
if (reports[author]?.contains(deleteNote) == true) {
|
||||
reports[author]?.let {
|
||||
reports = reports + Pair(author, it.minus(deleteNote))
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
flowSet?.reports?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,10 +402,10 @@ open class Note(val idHex: String) {
|
||||
|
||||
if (reportsByAuthor == null) {
|
||||
reports = reports + Pair(author, listOf(note))
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
flowSet?.reports?.invalidateData()
|
||||
} else if (!reportsByAuthor.contains(note)) {
|
||||
reports = reports + Pair(author, reportsByAuthor + note)
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
flowSet?.reports?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,7 +419,7 @@ open class Note(val idHex: String) {
|
||||
fun addRelay(relay: Relay) {
|
||||
if (relay.brief !in relays) {
|
||||
addRelaySync(relay.brief)
|
||||
liveSet?.innerRelays?.invalidateData()
|
||||
flowSet?.relays?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,39 +525,30 @@ open class Note(val idHex: String) {
|
||||
isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor)
|
||||
}
|
||||
|
||||
fun getReactionBy(user: User): String? {
|
||||
return reactions.firstNotNullOfOrNull {
|
||||
fun getReactionBy(user: User): String? =
|
||||
reactions.firstNotNullOfOrNull {
|
||||
if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) {
|
||||
it.key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isBoostedBy(user: User): Boolean {
|
||||
return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||
}
|
||||
fun isBoostedBy(user: User): Boolean = boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||
|
||||
fun hasReportsBy(user: User): Boolean {
|
||||
return reports[user]?.isNotEmpty() ?: false
|
||||
}
|
||||
fun hasReportsBy(user: User): Boolean = reports[user]?.isNotEmpty() ?: false
|
||||
|
||||
fun countReportAuthorsBy(users: Set<HexKey>): Int {
|
||||
return reports.count { it.key.pubkeyHex in users }
|
||||
}
|
||||
fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
|
||||
|
||||
fun reportsBy(users: Set<HexKey>): List<Note> {
|
||||
return reports
|
||||
fun reportsBy(users: Set<HexKey>): List<Note> =
|
||||
reports
|
||||
.mapNotNull {
|
||||
if (it.key.pubkeyHex in users) {
|
||||
it.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
}
|
||||
}.flatten()
|
||||
|
||||
private fun updateZapTotal() {
|
||||
var sumOfAmounts = BigDecimal.ZERO
|
||||
@@ -635,8 +628,8 @@ open class Note(val idHex: String) {
|
||||
)
|
||||
}
|
||||
|
||||
fun hasPledgeBy(user: User): Boolean {
|
||||
return replies
|
||||
fun hasPledgeBy(user: User): Boolean =
|
||||
replies
|
||||
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
|
||||
.any {
|
||||
val pledgeValue =
|
||||
@@ -650,10 +643,9 @@ open class Note(val idHex: String) {
|
||||
|
||||
pledgeValue != null && it.author == user
|
||||
}
|
||||
}
|
||||
|
||||
fun pledgedAmountByOthers(): BigDecimal {
|
||||
return replies
|
||||
fun pledgedAmountByOthers(): BigDecimal =
|
||||
replies
|
||||
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
|
||||
.mapNotNull {
|
||||
try {
|
||||
@@ -663,9 +655,7 @@ open class Note(val idHex: String) {
|
||||
null
|
||||
// do nothing if it can't convert to bigdecimal
|
||||
}
|
||||
}
|
||||
.sumOf { it }
|
||||
}
|
||||
}.sumOf { it }
|
||||
|
||||
fun hasAnyReports(): Boolean {
|
||||
val dayAgo = TimeUtils.oneDayAgo()
|
||||
@@ -676,8 +666,8 @@ open class Note(val idHex: String) {
|
||||
)
|
||||
}
|
||||
|
||||
fun isNewThread(): Boolean {
|
||||
return (
|
||||
fun isNewThread(): Boolean =
|
||||
(
|
||||
event is RepostEvent ||
|
||||
event is GenericRepostEvent ||
|
||||
replyTo == null ||
|
||||
@@ -685,29 +675,20 @@ open class Note(val idHex: String) {
|
||||
) &&
|
||||
event !is ChannelMessageEvent &&
|
||||
event !is LiveActivitiesChatMessageEvent
|
||||
}
|
||||
|
||||
fun hasZapped(loggedIn: User): Boolean {
|
||||
return zaps.any { it.key.author == loggedIn }
|
||||
}
|
||||
fun hasZapped(loggedIn: User): Boolean = zaps.any { it.key.author == loggedIn }
|
||||
|
||||
fun hasReacted(
|
||||
loggedIn: User,
|
||||
content: String,
|
||||
): Boolean {
|
||||
return reactedBy(loggedIn, content).isNotEmpty()
|
||||
}
|
||||
): Boolean = reactedBy(loggedIn, content).isNotEmpty()
|
||||
|
||||
fun reactedBy(
|
||||
loggedIn: User,
|
||||
content: String,
|
||||
): List<Note> {
|
||||
return reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
|
||||
}
|
||||
): List<Note> = reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
|
||||
|
||||
fun reactedBy(loggedIn: User): List<String> {
|
||||
return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
|
||||
}
|
||||
fun reactedBy(loggedIn: User): List<String> = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
|
||||
|
||||
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
||||
return boosts.firstOrNull {
|
||||
@@ -715,9 +696,7 @@ open class Note(val idHex: String) {
|
||||
} != null // 5 minute protection
|
||||
}
|
||||
|
||||
fun boostedBy(loggedIn: User): List<Note> {
|
||||
return boosts.filter { it.author == loggedIn }
|
||||
}
|
||||
fun boostedBy(loggedIn: User): List<Note> = boosts.filter { it.author == loggedIn }
|
||||
|
||||
fun moveAllReferencesTo(note: AddressableNote) {
|
||||
// migrates these comments to a new version
|
||||
@@ -762,33 +741,38 @@ open class Note(val idHex: String) {
|
||||
|
||||
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
||||
val thisEvent = event ?: return false
|
||||
val hash = thisEvent.pubKey().hashCode()
|
||||
|
||||
val isBoostedNoteHidden =
|
||||
if (
|
||||
thisEvent is GenericRepostEvent ||
|
||||
thisEvent is RepostEvent ||
|
||||
thisEvent is CommunityPostApprovalEvent
|
||||
) {
|
||||
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
|
||||
} else {
|
||||
false
|
||||
// if the author is hidden by spam or blocked
|
||||
if (accountChoices.hiddenUsersHashCodes.contains(hash) ||
|
||||
accountChoices.spammersHashCodes.contains(hash)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if the post is sensitive and the user doesn't want to see sensitive content
|
||||
if (accountChoices.showSensitiveContent == false && thisEvent.isSensitive()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if this is a repost, consider the inner event.
|
||||
if (
|
||||
thisEvent is GenericRepostEvent ||
|
||||
thisEvent is RepostEvent ||
|
||||
thisEvent is CommunityPostApprovalEvent
|
||||
) {
|
||||
if (replyTo?.lastOrNull()?.isHiddenFor(accountChoices) == true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val isHiddenByWord =
|
||||
if (thisEvent is BaseTextNoteEvent) {
|
||||
accountChoices.hiddenWords.any {
|
||||
thisEvent.content.containsAny(accountChoices.hiddenWordsCase)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
if (thisEvent is BaseTextNoteEvent) {
|
||||
if (accountChoices.hiddenWordsCase.isNotEmpty() && thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val isSensitive = thisEvent.isSensitive()
|
||||
return isBoostedNoteHidden ||
|
||||
isHiddenByWord ||
|
||||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
|
||||
accountChoices.spammers.contains(author?.pubkeyHex) ||
|
||||
(isSensitive && accountChoices.showSensitiveContent == false)
|
||||
return false
|
||||
}
|
||||
|
||||
var liveSet: NoteLiveSet? = null
|
||||
@@ -858,28 +842,35 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteFlowSet(u: Note) {
|
||||
class NoteFlowSet(
|
||||
u: Note,
|
||||
) {
|
||||
// Observers line up here.
|
||||
val metadata = NoteBundledRefresherFlow(u)
|
||||
val reports = NoteBundledRefresherFlow(u)
|
||||
val relays = NoteBundledRefresherFlow(u)
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return metadata.stateFlow.subscriptionCount.value > 0
|
||||
}
|
||||
fun isInUse(): Boolean =
|
||||
metadata.stateFlow.subscriptionCount.value > 0 ||
|
||||
reports.stateFlow.subscriptionCount.value > 0 ||
|
||||
relays.stateFlow.subscriptionCount.value > 0
|
||||
|
||||
fun destroy() {
|
||||
metadata.destroy()
|
||||
reports.destroy()
|
||||
relays.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteLiveSet(u: Note) {
|
||||
class NoteLiveSet(
|
||||
u: Note,
|
||||
) {
|
||||
// Observers line up here.
|
||||
val innerMetadata = NoteBundledRefresherLiveData(u)
|
||||
val innerReactions = NoteBundledRefresherLiveData(u)
|
||||
val innerBoosts = NoteBundledRefresherLiveData(u)
|
||||
val innerReplies = NoteBundledRefresherLiveData(u)
|
||||
val innerReports = NoteBundledRefresherLiveData(u)
|
||||
val innerRelays = NoteBundledRefresherLiveData(u)
|
||||
val innerZaps = NoteBundledRefresherLiveData(u)
|
||||
val innerOts = NoteBundledRefresherLiveData(u)
|
||||
val innerModifications = NoteBundledRefresherLiveData(u)
|
||||
@@ -888,8 +879,6 @@ class NoteLiveSet(u: Note) {
|
||||
val reactions = innerReactions.map { it }
|
||||
val boosts = innerBoosts.map { it }
|
||||
val replies = innerReplies.map { it }
|
||||
val reports = innerReports.map { it }
|
||||
val relays = innerRelays.map { it }
|
||||
val zaps = innerZaps.map { it }
|
||||
|
||||
val hasEvent = innerMetadata.map { it.note.event != null }.distinctUntilChanged()
|
||||
@@ -901,8 +890,7 @@ class NoteLiveSet(u: Note) {
|
||||
?: false ||
|
||||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
|
||||
reactionState?.note?.reactions?.isNotEmpty() ?: false
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged()
|
||||
|
||||
@@ -912,22 +900,17 @@ class NoteLiveSet(u: Note) {
|
||||
var total = 0
|
||||
it.note.reactions.forEach { total += it.value.size }
|
||||
total
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged()
|
||||
|
||||
val relayInfo = innerRelays.map { it.note.relays }
|
||||
|
||||
val content = innerMetadata.map { it.note.event?.content() ?: "" }
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return metadata.hasObservers() ||
|
||||
fun isInUse(): Boolean =
|
||||
metadata.hasObservers() ||
|
||||
reactions.hasObservers() ||
|
||||
boosts.hasObservers() ||
|
||||
replies.hasObservers() ||
|
||||
reports.hasObservers() ||
|
||||
relays.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
hasEvent.hasObservers() ||
|
||||
hasReactions.hasObservers() ||
|
||||
@@ -936,15 +919,12 @@ class NoteLiveSet(u: Note) {
|
||||
boostCount.hasObservers() ||
|
||||
innerOts.hasObservers() ||
|
||||
innerModifications.hasObservers()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
innerMetadata.destroy()
|
||||
innerReactions.destroy()
|
||||
innerBoosts.destroy()
|
||||
innerReplies.destroy()
|
||||
innerReports.destroy()
|
||||
innerRelays.destroy()
|
||||
innerZaps.destroy()
|
||||
innerOts.destroy()
|
||||
innerModifications.destroy()
|
||||
@@ -952,8 +932,11 @@ class NoteLiveSet(u: Note) {
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteBundledRefresherFlow(val note: Note) {
|
||||
class NoteBundledRefresherFlow(
|
||||
val note: Note,
|
||||
) {
|
||||
// Refreshes observers in batches.
|
||||
// TODO: Replace the bundler for .sample
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
val stateFlow = MutableStateFlow(NoteState(note))
|
||||
|
||||
@@ -973,7 +956,9 @@ class NoteBundledRefresherFlow(val note: Note) {
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||
class NoteBundledRefresherLiveData(
|
||||
val note: Note,
|
||||
) : LiveData<NoteState>(NoteState(note)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
|
||||
@@ -1000,7 +985,10 @@ class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteSta
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
|
||||
class NoteLoadingLiveData<Y>(
|
||||
val note: Note,
|
||||
initialValue: Y?,
|
||||
) : MediatorLiveData<Y>(initialValue) {
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
if (note is AddressableNote) {
|
||||
@@ -1020,24 +1008,6 @@ class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveDat
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable class NoteState(val note: Note)
|
||||
|
||||
object RelayBriefInfoCache {
|
||||
val cache = LruCache<String, RelayBriefInfo?>(50)
|
||||
|
||||
@Immutable
|
||||
data class RelayBriefInfo(
|
||||
val url: String,
|
||||
val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern(),
|
||||
val favIcon: String = "https://$displayUrl/favicon.ico".intern(),
|
||||
)
|
||||
|
||||
fun get(url: String): RelayBriefInfo {
|
||||
val info = cache[url]
|
||||
if (info != null) return info
|
||||
|
||||
val newInfo = RelayBriefInfo(url)
|
||||
cache.put(url, newInfo)
|
||||
return newInfo
|
||||
}
|
||||
}
|
||||
@Immutable class NoteState(
|
||||
val note: Note,
|
||||
)
|
||||
@@ -34,17 +34,20 @@ data class Settings(
|
||||
val automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS,
|
||||
val dontShowPushNotificationSelector: Boolean = false,
|
||||
val dontAskForNotificationPermissions: Boolean = false,
|
||||
val featureSet: FeatureSetType = FeatureSetType.COMPLETE,
|
||||
val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
|
||||
)
|
||||
|
||||
enum class ThemeType(val screenCode: Int, val resourceId: Int) {
|
||||
enum class ThemeType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
SYSTEM(0, R.string.system),
|
||||
LIGHT(1, R.string.light),
|
||||
DARK(2, R.string.dark),
|
||||
}
|
||||
|
||||
fun parseThemeType(code: Int?): ThemeType {
|
||||
return when (code) {
|
||||
fun parseThemeType(code: Int?): ThemeType =
|
||||
when (code) {
|
||||
ThemeType.SYSTEM.screenCode -> ThemeType.SYSTEM
|
||||
ThemeType.LIGHT.screenCode -> ThemeType.LIGHT
|
||||
ThemeType.DARK.screenCode -> ThemeType.DARK
|
||||
@@ -52,21 +55,28 @@ fun parseThemeType(code: Int?): ThemeType {
|
||||
ThemeType.SYSTEM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val resourceId: Int) {
|
||||
enum class ConnectivityType(
|
||||
val prefCode: Boolean?,
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
ALWAYS(null, 0, R.string.connectivity_type_always),
|
||||
WIFI_ONLY(true, 1, R.string.connectivity_type_wifi_only),
|
||||
WIFI_ONLY(true, 1, R.string.connectivity_type_unmetered_wifi_only),
|
||||
NEVER(false, 2, R.string.connectivity_type_never),
|
||||
}
|
||||
|
||||
enum class FeatureSetType(val screenCode: Int, val resourceId: Int) {
|
||||
enum class FeatureSetType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
COMPLETE(0, R.string.ui_feature_set_type_complete),
|
||||
SIMPLIFIED(1, R.string.ui_feature_set_type_simplified),
|
||||
PERFORMANCE(2, R.string.ui_feature_set_type_performance),
|
||||
}
|
||||
|
||||
fun parseConnectivityType(code: Boolean?): ConnectivityType {
|
||||
return when (code) {
|
||||
fun parseConnectivityType(code: Boolean?): ConnectivityType =
|
||||
when (code) {
|
||||
ConnectivityType.ALWAYS.prefCode -> ConnectivityType.ALWAYS
|
||||
ConnectivityType.WIFI_ONLY.prefCode -> ConnectivityType.WIFI_ONLY
|
||||
ConnectivityType.NEVER.prefCode -> ConnectivityType.NEVER
|
||||
@@ -74,10 +84,9 @@ fun parseConnectivityType(code: Boolean?): ConnectivityType {
|
||||
ConnectivityType.ALWAYS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseConnectivityType(screenCode: Int): ConnectivityType {
|
||||
return when (screenCode) {
|
||||
fun parseConnectivityType(screenCode: Int): ConnectivityType =
|
||||
when (screenCode) {
|
||||
ConnectivityType.ALWAYS.screenCode -> ConnectivityType.ALWAYS
|
||||
ConnectivityType.WIFI_ONLY.screenCode -> ConnectivityType.WIFI_ONLY
|
||||
ConnectivityType.NEVER.screenCode -> ConnectivityType.NEVER
|
||||
@@ -85,39 +94,40 @@ fun parseConnectivityType(screenCode: Int): ConnectivityType {
|
||||
ConnectivityType.ALWAYS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseFeatureSetType(screenCode: Int): FeatureSetType {
|
||||
return when (screenCode) {
|
||||
fun parseFeatureSetType(screenCode: Int): FeatureSetType =
|
||||
when (screenCode) {
|
||||
FeatureSetType.COMPLETE.screenCode -> FeatureSetType.COMPLETE
|
||||
FeatureSetType.SIMPLIFIED.screenCode -> FeatureSetType.SIMPLIFIED
|
||||
FeatureSetType.PERFORMANCE.screenCode -> FeatureSetType.PERFORMANCE
|
||||
else -> {
|
||||
FeatureSetType.COMPLETE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class BooleanType(val prefCode: Boolean?, val screenCode: Int, val reourceId: Int) {
|
||||
enum class BooleanType(
|
||||
val prefCode: Boolean?,
|
||||
val screenCode: Int,
|
||||
val reourceId: Int,
|
||||
) {
|
||||
ALWAYS(null, 0, R.string.connectivity_type_always),
|
||||
NEVER(false, 1, R.string.connectivity_type_never),
|
||||
}
|
||||
|
||||
fun parseBooleanType(code: Boolean?): BooleanType {
|
||||
return when (code) {
|
||||
fun parseBooleanType(code: Boolean?): BooleanType =
|
||||
when (code) {
|
||||
BooleanType.ALWAYS.prefCode -> BooleanType.ALWAYS
|
||||
BooleanType.NEVER.prefCode -> BooleanType.NEVER
|
||||
else -> {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseBooleanType(screenCode: Int): BooleanType {
|
||||
return when (screenCode) {
|
||||
fun parseBooleanType(screenCode: Int): BooleanType =
|
||||
when (screenCode) {
|
||||
BooleanType.ALWAYS.screenCode -> BooleanType.ALWAYS
|
||||
BooleanType.NEVER.screenCode -> BooleanType.NEVER
|
||||
else -> {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,8 +62,7 @@ class ThreadAssembler {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.filterNotNull()
|
||||
}?.filterNotNull()
|
||||
|
||||
if (roots != null && roots.isNotEmpty()) {
|
||||
return roots[0]
|
||||
@@ -148,9 +147,7 @@ class OnlyLatestVersionSet : MutableSet<Note> {
|
||||
}
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<Note>): Boolean {
|
||||
return elements.map { add(it) }.any()
|
||||
}
|
||||
override fun addAll(elements: Collection<Note>): Boolean = elements.map { add(it) }.any()
|
||||
|
||||
override val size: Int
|
||||
get() = set.size
|
||||
@@ -160,29 +157,17 @@ class OnlyLatestVersionSet : MutableSet<Note> {
|
||||
map.clear()
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return set.isEmpty()
|
||||
}
|
||||
override fun isEmpty(): Boolean = set.isEmpty()
|
||||
|
||||
override fun containsAll(elements: Collection<Note>): Boolean {
|
||||
return set.containsAll(elements)
|
||||
}
|
||||
override fun containsAll(elements: Collection<Note>): Boolean = set.containsAll(elements)
|
||||
|
||||
override fun contains(element: Note): Boolean {
|
||||
return set.contains(element)
|
||||
}
|
||||
override fun contains(element: Note): Boolean = set.contains(element)
|
||||
|
||||
override fun iterator(): MutableIterator<Note> {
|
||||
return set.iterator()
|
||||
}
|
||||
override fun iterator(): MutableIterator<Note> = set.iterator()
|
||||
|
||||
override fun retainAll(elements: Collection<Note>): Boolean {
|
||||
return set.retainAll(elements)
|
||||
}
|
||||
override fun retainAll(elements: Collection<Note>): Boolean = set.retainAll(elements)
|
||||
|
||||
override fun removeAll(elements: Collection<Note>): Boolean {
|
||||
return elements.map { remove(it) }.any()
|
||||
}
|
||||
override fun removeAll(elements: Collection<Note>): Boolean = elements.map { remove(it) }.any()
|
||||
|
||||
override fun remove(element: Note): Boolean {
|
||||
element.address()?.let {
|
||||
@@ -27,16 +27,20 @@ import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
data class LevelSignature(val signature: String, val createdAt: Long?, val author: User?)
|
||||
data class LevelSignature(
|
||||
val signature: String,
|
||||
val createdAt: Long?,
|
||||
val author: User?,
|
||||
)
|
||||
|
||||
object ThreadLevelCalculator {
|
||||
val levelFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss")
|
||||
|
||||
private fun formattedDateTime(timestamp: Long): String {
|
||||
return Instant.ofEpochSecond(timestamp)
|
||||
private fun formattedDateTime(timestamp: Long): String =
|
||||
Instant
|
||||
.ofEpochSecond(timestamp)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.format(levelFormatter)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method caches signatures during each execution to avoid recalculation in longer threads
|
||||
@@ -76,8 +80,7 @@ object ThreadLevelCalculator {
|
||||
accountFollowingSet,
|
||||
now,
|
||||
).apply { cachedSignatures.put(it, this) }
|
||||
}
|
||||
.maxByOrNull { it.signature.length }
|
||||
}.maxByOrNull { it.signature.length }
|
||||
)
|
||||
|
||||
val parentSignature = parent?.signature?.removeSuffix(";") ?: ""
|
||||
@@ -74,7 +74,6 @@ object UrlCachedPreviewer {
|
||||
onReady(state)
|
||||
}
|
||||
},
|
||||
)
|
||||
.fetchUrlPreview()
|
||||
).fetchUrlPreview()
|
||||
}
|
||||
}
|
||||
@@ -27,14 +27,16 @@ import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Lud06
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
@@ -49,10 +51,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
class User(val pubkeyHex: String) {
|
||||
class User(
|
||||
val pubkeyHex: String,
|
||||
) {
|
||||
var info: UserMetadata? = null
|
||||
|
||||
var latestMetadata: MetadataEvent? = null
|
||||
var latestMetadataRelay: String? = null
|
||||
var latestContactList: ContactListEvent? = null
|
||||
var latestBookmarkList: BookmarkListEvent? = null
|
||||
|
||||
@@ -76,7 +81,16 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
|
||||
|
||||
fun toNostrUri() = "nostr:${pubkeyNpub()}"
|
||||
fun toNProfile(): String {
|
||||
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)?.writeRelays()
|
||||
|
||||
return Nip19Bech32.createNProfile(
|
||||
pubkeyHex,
|
||||
relayList?.take(3) ?: listOfNotNull(latestMetadataRelay),
|
||||
)
|
||||
}
|
||||
|
||||
fun toNostrUri() = "nostr:${toNProfile()}"
|
||||
|
||||
override fun toString(): String = pubkeyHex
|
||||
|
||||
@@ -96,17 +110,11 @@ class User(val pubkeyHex: String) {
|
||||
return firstName
|
||||
}
|
||||
|
||||
fun toBestDisplayName(): String {
|
||||
return info?.bestName() ?: pubkeyDisplayHex()
|
||||
}
|
||||
fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex()
|
||||
|
||||
fun nip05(): String? {
|
||||
return info?.nip05
|
||||
}
|
||||
fun nip05(): String? = info?.nip05
|
||||
|
||||
fun profilePicture(): String? {
|
||||
return info?.picture
|
||||
}
|
||||
fun profilePicture(): String? = info?.picture
|
||||
|
||||
fun updateBookmark(event: BookmarkListEvent) {
|
||||
if (event.id == latestBookmarkList?.id) return
|
||||
@@ -132,10 +140,18 @@ class User(val pubkeyHex: String) {
|
||||
// Update Followers of the past user list
|
||||
// Update Followers of the new contact list
|
||||
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
|
||||
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
|
||||
LocalCache
|
||||
.getUserIfExists(it)
|
||||
?.liveSet
|
||||
?.innerFollowers
|
||||
?.invalidateData()
|
||||
}
|
||||
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
|
||||
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
|
||||
LocalCache
|
||||
.getUserIfExists(it)
|
||||
?.liveSet
|
||||
?.innerFollowers
|
||||
?.invalidateData()
|
||||
}
|
||||
|
||||
liveSet?.innerRelays?.invalidateData()
|
||||
@@ -198,25 +214,19 @@ class User(val pubkeyHex: String) {
|
||||
return amount
|
||||
}
|
||||
|
||||
fun reportsBy(user: User): Set<Note> {
|
||||
return reports[user] ?: emptySet()
|
||||
}
|
||||
fun reportsBy(user: User): Set<Note> = reports[user] ?: emptySet()
|
||||
|
||||
fun countReportAuthorsBy(users: Set<HexKey>): Int {
|
||||
return reports.count { it.key.pubkeyHex in users }
|
||||
}
|
||||
fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
|
||||
|
||||
fun reportsBy(users: Set<HexKey>): List<Note> {
|
||||
return reports
|
||||
fun reportsBy(users: Set<HexKey>): List<Note> =
|
||||
reports
|
||||
.mapNotNull {
|
||||
if (it.key.pubkeyHex in users) {
|
||||
it.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
}
|
||||
}.flatten()
|
||||
|
||||
@Synchronized
|
||||
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom {
|
||||
@@ -235,9 +245,7 @@ class User(val pubkeyHex: String) {
|
||||
return getOrCreatePrivateChatroom(key)
|
||||
}
|
||||
|
||||
private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom {
|
||||
return privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key)
|
||||
}
|
||||
private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key)
|
||||
|
||||
fun addMessage(
|
||||
room: ChatroomKey,
|
||||
@@ -326,13 +334,9 @@ class User(val pubkeyHex: String) {
|
||||
liveSet?.innerMetadata?.invalidateData()
|
||||
}
|
||||
|
||||
fun isFollowing(user: User): Boolean {
|
||||
return latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
|
||||
}
|
||||
fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
|
||||
|
||||
fun isFollowingHashtag(tag: String): Boolean {
|
||||
return latestContactList?.isTaggedHash(tag) ?: false
|
||||
}
|
||||
fun isFollowingHashtag(tag: String): Boolean = latestContactList?.isTaggedHash(tag) ?: false
|
||||
|
||||
fun isFollowingHashtagCached(tag: String): Boolean {
|
||||
return latestContactList?.verifiedFollowTagSet?.let {
|
||||
@@ -362,37 +366,21 @@ class User(val pubkeyHex: String) {
|
||||
?: false
|
||||
}
|
||||
|
||||
fun transientFollowCount(): Int? {
|
||||
return latestContactList?.unverifiedFollowKeySet()?.size
|
||||
}
|
||||
fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size
|
||||
|
||||
suspend fun transientFollowerCount(): Int {
|
||||
return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
}
|
||||
suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
|
||||
fun cachedFollowingKeySet(): Set<HexKey> {
|
||||
return latestContactList?.verifiedFollowKeySet ?: emptySet()
|
||||
}
|
||||
fun cachedFollowingKeySet(): Set<HexKey> = latestContactList?.verifiedFollowKeySet ?: emptySet()
|
||||
|
||||
fun cachedFollowingTagSet(): Set<String> {
|
||||
return latestContactList?.verifiedFollowTagSet ?: emptySet()
|
||||
}
|
||||
fun cachedFollowingTagSet(): Set<String> = latestContactList?.verifiedFollowTagSet ?: emptySet()
|
||||
|
||||
fun cachedFollowingGeohashSet(): Set<HexKey> {
|
||||
return latestContactList?.verifiedFollowGeohashSet ?: emptySet()
|
||||
}
|
||||
fun cachedFollowingGeohashSet(): Set<HexKey> = latestContactList?.verifiedFollowGeohashSet ?: emptySet()
|
||||
|
||||
fun cachedFollowingCommunitiesSet(): Set<HexKey> {
|
||||
return latestContactList?.verifiedFollowCommunitySet ?: emptySet()
|
||||
}
|
||||
fun cachedFollowingCommunitiesSet(): Set<HexKey> = latestContactList?.verifiedFollowCommunitySet ?: emptySet()
|
||||
|
||||
fun cachedFollowCount(): Int? {
|
||||
return latestContactList?.verifiedFollowKeySet?.size
|
||||
}
|
||||
fun cachedFollowCount(): Int? = latestContactList?.verifiedFollowKeySet?.size
|
||||
|
||||
suspend fun cachedFollowerCount(): Int {
|
||||
return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
}
|
||||
suspend fun cachedFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
|
||||
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
|
||||
val messagesToUser = privateChatrooms[key] ?: return false
|
||||
@@ -403,16 +391,13 @@ class User(val pubkeyHex: String) {
|
||||
fun hasReport(
|
||||
loggedIn: User,
|
||||
type: ReportEvent.ReportType,
|
||||
): Boolean {
|
||||
return reports[loggedIn]?.firstOrNull {
|
||||
): Boolean =
|
||||
reports[loggedIn]?.firstOrNull {
|
||||
it.event is ReportEvent &&
|
||||
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
|
||||
} != null
|
||||
}
|
||||
|
||||
fun anyNameStartsWith(username: String): Boolean {
|
||||
return info?.anyNameStartsWith(username) ?: false
|
||||
}
|
||||
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
|
||||
|
||||
var liveSet: UserLiveSet? = null
|
||||
var flowSet: UserFlowSet? = null
|
||||
@@ -473,14 +458,14 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserFlowSet(u: User) {
|
||||
class UserFlowSet(
|
||||
u: User,
|
||||
) {
|
||||
// Observers line up here.
|
||||
val follows = UserBundledRefresherFlow(u)
|
||||
val relays = UserBundledRefresherFlow(u)
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
|
||||
}
|
||||
fun isInUse(): Boolean = relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
|
||||
|
||||
fun destroy() {
|
||||
relays.destroy()
|
||||
@@ -489,7 +474,9 @@ class UserFlowSet(u: User) {
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserLiveSet(u: User) {
|
||||
class UserLiveSet(
|
||||
u: User,
|
||||
) {
|
||||
val innerMetadata = UserBundledRefresherLiveData(u)
|
||||
|
||||
// UI Observers line up here.
|
||||
@@ -501,6 +488,7 @@ class UserLiveSet(u: User) {
|
||||
val innerRelayInfo = UserBundledRefresherLiveData(u)
|
||||
val innerZaps = UserBundledRefresherLiveData(u)
|
||||
val innerBookmarks = UserBundledRefresherLiveData(u)
|
||||
val innerGallery = UserBundledRefresherLiveData(u)
|
||||
val innerStatuses = UserBundledRefresherLiveData(u)
|
||||
|
||||
// UI Observers line up here.
|
||||
@@ -513,6 +501,7 @@ class UserLiveSet(u: User) {
|
||||
val relayInfo = innerRelayInfo.map { it }
|
||||
val zaps = innerZaps.map { it }
|
||||
val bookmarks = innerBookmarks.map { it }
|
||||
val gallery = innerGallery.map { it }
|
||||
val statuses = innerStatuses.map { it }
|
||||
|
||||
val profilePictureChanges = innerMetadata.map { it.user.profilePicture() }.distinctUntilChanged()
|
||||
@@ -521,8 +510,8 @@ class UserLiveSet(u: User) {
|
||||
|
||||
val userMetadataInfo = innerMetadata.map { it.user.info }.distinctUntilChanged()
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return metadata.hasObservers() ||
|
||||
fun isInUse(): Boolean =
|
||||
metadata.hasObservers() ||
|
||||
follows.hasObservers() ||
|
||||
followers.hasObservers() ||
|
||||
reports.hasObservers() ||
|
||||
@@ -531,11 +520,11 @@ class UserLiveSet(u: User) {
|
||||
relayInfo.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
bookmarks.hasObservers() ||
|
||||
gallery.hasObservers() ||
|
||||
statuses.hasObservers() ||
|
||||
profilePictureChanges.hasObservers() ||
|
||||
nip05Changes.hasObservers() ||
|
||||
userMetadataInfo.hasObservers()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
innerMetadata.destroy()
|
||||
@@ -547,6 +536,7 @@ class UserLiveSet(u: User) {
|
||||
innerRelayInfo.destroy()
|
||||
innerZaps.destroy()
|
||||
innerBookmarks.destroy()
|
||||
innerGallery.destroy()
|
||||
innerStatuses.destroy()
|
||||
}
|
||||
}
|
||||
@@ -558,7 +548,9 @@ data class RelayInfo(
|
||||
var counter: Long,
|
||||
)
|
||||
|
||||
class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||
class UserBundledRefresherLiveData(
|
||||
val user: User,
|
||||
) : LiveData<UserState>(UserState(user)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
|
||||
@@ -585,7 +577,9 @@ class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserSta
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserBundledRefresherFlow(val user: User) {
|
||||
class UserBundledRefresherFlow(
|
||||
val user: User,
|
||||
) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
val stateFlow = MutableStateFlow(UserState(user))
|
||||
@@ -605,7 +599,10 @@ class UserBundledRefresherFlow(val user: User) {
|
||||
}
|
||||
}
|
||||
|
||||
class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
|
||||
class UserLoadingLiveData<Y>(
|
||||
val user: User,
|
||||
initialValue: Y?,
|
||||
) : MediatorLiveData<Y>(initialValue) {
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
NostrSingleUserDataSource.add(user)
|
||||
@@ -617,4 +614,6 @@ class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveDat
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable class UserState(val user: User)
|
||||
@Immutable class UserState(
|
||||
val user: User,
|
||||
)
|
||||
@@ -27,7 +27,10 @@ import com.vitorpamplona.quartz.events.Event
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class LatestByKindAndAuthor<T : Event>(private val kind: Int, private val pubkey: String) {
|
||||
class LatestByKindAndAuthor<T : Event>(
|
||||
private val kind: Int,
|
||||
private val pubkey: String,
|
||||
) {
|
||||
private val _latest = MutableStateFlow<T?>(null)
|
||||
val latest = _latest.asStateFlow()
|
||||
|
||||
@@ -41,32 +44,38 @@ class LatestByKindAndAuthor<T : Event>(private val kind: Int, private val pubkey
|
||||
}
|
||||
}
|
||||
|
||||
fun canDelete(): Boolean {
|
||||
return _latest.subscriptionCount.value == 0
|
||||
fun canDelete(): Boolean = _latest.subscriptionCount.value == 0
|
||||
|
||||
fun restart() {
|
||||
val latestNote =
|
||||
if ((kind in 10000..19999) || (kind in 30000..39999)) {
|
||||
LocalCache.addressables
|
||||
.maxOrNullOf(
|
||||
filter = { idHex: String, note: AddressableNote ->
|
||||
note.event?.let {
|
||||
it.kind() == kind && it.pubKey() == pubkey
|
||||
} == true
|
||||
},
|
||||
comparator = CreatedAtComparatorAddresses,
|
||||
)?.event as? T
|
||||
} else {
|
||||
LocalCache.notes
|
||||
.maxOrNullOf(
|
||||
filter = { idHex: String, note: Note ->
|
||||
note.event?.let {
|
||||
it.kind() == kind && it.pubKey() == pubkey
|
||||
} == true
|
||||
},
|
||||
comparator = CreatedAtComparator,
|
||||
)?.event as? T
|
||||
}
|
||||
|
||||
if (_latest.value != latestNote) {
|
||||
_latest.tryEmit(latestNote)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun init() {
|
||||
val latestNote =
|
||||
if ((kind in 10000..19999) || (kind in 30000..39999)) {
|
||||
LocalCache.addressables.maxOrNullOf(
|
||||
filter = { idHex: String, note: AddressableNote ->
|
||||
note.event?.let {
|
||||
it.kind() == kind && it.pubKey() == pubkey
|
||||
} == true
|
||||
},
|
||||
comparator = CreatedAtComparatorAddresses,
|
||||
)?.event as? T
|
||||
} else {
|
||||
LocalCache.notes.maxOrNullOf(
|
||||
filter = { idHex: String, note: Note ->
|
||||
note.event?.let {
|
||||
it.kind() == kind && it.pubKey() == pubkey
|
||||
} == true
|
||||
},
|
||||
comparator = CreatedAtComparator,
|
||||
)?.event as? T
|
||||
}
|
||||
|
||||
_latest.tryEmit(latestNote)
|
||||
restart()
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,10 @@ import com.vitorpamplona.quartz.events.Event
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class LatestByKindWithETag<T : Event>(private val kind: Int, private val eTag: String) {
|
||||
class LatestByKindWithETag<T : Event>(
|
||||
private val kind: Int,
|
||||
private val eTag: String,
|
||||
) {
|
||||
private val _latest = MutableStateFlow<T?>(null)
|
||||
val latest = _latest.asStateFlow()
|
||||
|
||||
@@ -40,21 +43,26 @@ class LatestByKindWithETag<T : Event>(private val kind: Int, private val eTag: S
|
||||
}
|
||||
}
|
||||
|
||||
fun canDelete(): Boolean {
|
||||
return _latest.subscriptionCount.value == 0
|
||||
fun canDelete(): Boolean = _latest.subscriptionCount.value == 0
|
||||
|
||||
fun restart() {
|
||||
val latestNote =
|
||||
LocalCache.notes
|
||||
.maxOrNullOf(
|
||||
filter = { idHex: String, note: Note ->
|
||||
note.event?.let {
|
||||
it.kind() == kind && it.isTaggedEvent(eTag)
|
||||
} == true
|
||||
},
|
||||
comparator = CreatedAtComparator,
|
||||
)?.event as? T
|
||||
|
||||
if (_latest.value != latestNote) {
|
||||
_latest.tryEmit(latestNote)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun init() {
|
||||
val latestNote =
|
||||
LocalCache.notes.maxOrNullOf(
|
||||
filter = { idHex: String, note: Note ->
|
||||
note.event?.let {
|
||||
it.kind() == kind && it.isTaggedEvent(eTag)
|
||||
} == true
|
||||
},
|
||||
comparator = CreatedAtComparator,
|
||||
)?.event as? T
|
||||
|
||||
_latest.tryEmit(latestNote)
|
||||
restart()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.ammolite.relays.NostrDataSource
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
abstract class AmethystNostrDataSource(
|
||||
debugName: String,
|
||||
) : NostrDataSource(debugName) {
|
||||
override fun consume(
|
||||
event: Event,
|
||||
relay: Relay,
|
||||
) {
|
||||
LocalCache.verifyAndConsume(event, relay)
|
||||
}
|
||||
|
||||
override fun markAsSeenOnRelay(
|
||||
eventId: String,
|
||||
relay: Relay,
|
||||
) {
|
||||
val note = LocalCache.getNoteIfExists(eventId)
|
||||
val noteEvent = note?.event
|
||||
if (noteEvent is AddressableEvent) {
|
||||
LocalCache.getAddressableNoteIfExists(noteEvent.address().toTag())?.addRelay(relay)
|
||||
} else {
|
||||
note?.addRelay(relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,9 +59,7 @@ class BlurHashFetcher(
|
||||
data: Uri,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader,
|
||||
): Fetcher {
|
||||
return BlurHashFetcher(options, data)
|
||||
}
|
||||
): Fetcher = BlurHashFetcher(options, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +70,8 @@ object BlurHashRequester {
|
||||
): ImageRequest {
|
||||
val encodedMessage = URLEncoder.encode(message, "utf-8")
|
||||
|
||||
return ImageRequest.Builder(context)
|
||||
return ImageRequest
|
||||
.Builder(context)
|
||||
.data("bluehash:$encodedMessage")
|
||||
.fetcherFactory(BlurHashFetcher.Factory)
|
||||
.build()
|
||||
@@ -28,13 +28,16 @@ import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
object CachedRichTextParser {
|
||||
val richTextCache = LruCache<String, RichTextViewerState>(50)
|
||||
|
||||
fun getCached(content: String): RichTextViewerState? = richTextCache[content]
|
||||
|
||||
fun parseText(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
callbackUri: String? = null,
|
||||
): RichTextViewerState {
|
||||
return if (richTextCache[content] != null) {
|
||||
richTextCache[content]
|
||||
val cached = richTextCache[content]
|
||||
return if (cached != null) {
|
||||
cached
|
||||
} else {
|
||||
val newUrls = RichTextParser().parseText(content, tags, callbackUri)
|
||||
richTextCache.put(content, newUrls)
|
||||
@@ -28,6 +28,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
@@ -46,9 +48,7 @@ data class CashuToken(
|
||||
object CachedCashuProcessor {
|
||||
val cashuCache = LruCache<String, GenericLoadable<CashuToken>>(20)
|
||||
|
||||
fun cached(token: String): GenericLoadable<CashuToken> {
|
||||
return cashuCache[token] ?: GenericLoadable.Loading()
|
||||
}
|
||||
fun cached(token: String): GenericLoadable<CashuToken> = cashuCache[token] ?: GenericLoadable.Loading()
|
||||
|
||||
fun parse(token: String): GenericLoadable<CashuToken> {
|
||||
if (cashuCache[token] !is GenericLoadable.Loaded) {
|
||||
@@ -150,7 +150,12 @@ class CashuProcessor {
|
||||
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val requestBody = jsonObject.toString().toRequestBody(mediaType)
|
||||
val request = Request.Builder().url(url).post(requestBody).build()
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use {
|
||||
val body = it.body.string()
|
||||
@@ -163,13 +168,19 @@ class CashuProcessor {
|
||||
feeCost,
|
||||
)
|
||||
} else {
|
||||
val msg = tree?.get("detail")?.asText()?.split('.')?.getOrNull(0)?.ifBlank { null }
|
||||
val msg =
|
||||
tree
|
||||
?.get("detail")
|
||||
?.asText()
|
||||
?.split('.')
|
||||
?.getOrNull(0)
|
||||
?.ifBlank { null }
|
||||
onError(
|
||||
context.getString(R.string.cashu_failed_redemption),
|
||||
stringRes(context, R.string.cashu_failed_redemption),
|
||||
if (msg != null) {
|
||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, msg)
|
||||
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg)
|
||||
} else {
|
||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg)
|
||||
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -177,8 +188,8 @@ class CashuProcessor {
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(
|
||||
context.getString(R.string.cashu_successful_redemption),
|
||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||
stringRes(context, R.string.cashu_successful_redemption),
|
||||
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -203,7 +214,12 @@ class CashuProcessor {
|
||||
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val requestBody = jsonObject.toString().toRequestBody(mediaType)
|
||||
val request = Request.Builder().url(url).post(requestBody).build()
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use {
|
||||
val body = it.body.string()
|
||||
@@ -213,21 +229,28 @@ class CashuProcessor {
|
||||
|
||||
if (successful) {
|
||||
onSuccess(
|
||||
context.getString(R.string.cashu_successful_redemption),
|
||||
context.getString(
|
||||
stringRes(context, R.string.cashu_successful_redemption),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.cashu_successful_redemption_explainer,
|
||||
token.totalAmount.toString(),
|
||||
fees.toString(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
val msg = tree?.get("detail")?.asText()?.split('.')?.getOrNull(0)?.ifBlank { null }
|
||||
val msg =
|
||||
tree
|
||||
?.get("detail")
|
||||
?.asText()
|
||||
?.split('.')
|
||||
?.getOrNull(0)
|
||||
?.ifBlank { null }
|
||||
onError(
|
||||
context.getString(R.string.cashu_failed_redemption),
|
||||
stringRes(context, R.string.cashu_failed_redemption),
|
||||
if (msg != null) {
|
||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, msg)
|
||||
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg)
|
||||
} else {
|
||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg)
|
||||
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -235,8 +258,8 @@ class CashuProcessor {
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(
|
||||
context.getString(R.string.cashu_successful_redemption),
|
||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||
stringRes(context, R.string.cashu_successful_redemption),
|
||||
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,7 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
|
||||
fun String.isUTF16Char(pos: Int): Boolean {
|
||||
return Character.charCount(this.codePointAt(pos)) == 2
|
||||
}
|
||||
fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2
|
||||
|
||||
fun String.firstFullCharOld(): String {
|
||||
return when (this.length) {
|
||||
@@ -236,10 +236,10 @@ fun MediaMetadataRetriever.prepareVideoHeight(): Int? {
|
||||
}
|
||||
}
|
||||
|
||||
class ByteArrayMediaDataSource(var imageData: ByteArray) : MediaDataSource() {
|
||||
override fun getSize(): Long {
|
||||
return imageData.size.toLong()
|
||||
}
|
||||
class ByteArrayMediaDataSource(
|
||||
var imageData: ByteArray,
|
||||
) : MediaDataSource() {
|
||||
override fun getSize(): Long = imageData.size.toLong()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun readAt(
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service
|
||||
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
class HttpStatusMessages {
|
||||
companion object {
|
||||
fun resourceIdFor(statusCode: Int = 0): Int? =
|
||||
when (statusCode) {
|
||||
400 -> R.string.http_status_400
|
||||
401 -> R.string.http_status_401
|
||||
402 -> R.string.http_status_402
|
||||
403 -> R.string.http_status_403
|
||||
404 -> R.string.http_status_404
|
||||
405 -> R.string.http_status_405
|
||||
406 -> R.string.http_status_406
|
||||
407 -> R.string.http_status_407
|
||||
408 -> R.string.http_status_408
|
||||
409 -> R.string.http_status_409
|
||||
410 -> R.string.http_status_410
|
||||
411 -> R.string.http_status_411
|
||||
412 -> R.string.http_status_412
|
||||
413 -> R.string.http_status_413
|
||||
414 -> R.string.http_status_414
|
||||
415 -> R.string.http_status_415
|
||||
416 -> R.string.http_status_416
|
||||
417 -> R.string.http_status_417
|
||||
426 -> R.string.http_status_426
|
||||
|
||||
500 -> R.string.http_status_500
|
||||
501 -> R.string.http_status_501
|
||||
502 -> R.string.http_status_502
|
||||
503 -> R.string.http_status_503
|
||||
504 -> R.string.http_status_504
|
||||
505 -> R.string.http_status_505
|
||||
506 -> R.string.http_status_506
|
||||
507 -> R.string.http_status_507
|
||||
508 -> R.string.http_status_508
|
||||
511 -> R.string.http_status_511
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,9 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class LocationUtil(context: Context) {
|
||||
class LocationUtil(
|
||||
context: Context,
|
||||
) {
|
||||
companion object {
|
||||
const val MIN_TIME: Long = 1000L
|
||||
const val MIN_DISTANCE: Float = 0.0f
|
||||
@@ -96,9 +98,7 @@ class LocationUtil(context: Context) {
|
||||
object CachedGeoLocations {
|
||||
val locationNames = LruCache<String, String>(20)
|
||||
|
||||
fun cached(geoHashStr: String): String? {
|
||||
return locationNames[geoHashStr]
|
||||
}
|
||||
fun cached(geoHashStr: String): String? = locationNames[geoHashStr]
|
||||
|
||||
suspend fun geoLocate(
|
||||
geoHashStr: String,
|
||||
@@ -31,4 +31,6 @@ fun checkNotInMainThread() {
|
||||
|
||||
fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()
|
||||
|
||||
class OnMainThreadException(str: String) : RuntimeException(str)
|
||||
class OnMainThreadException(
|
||||
str: String,
|
||||
) : RuntimeException(str)
|
||||
@@ -22,15 +22,13 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class Nip05NostrAddressVerifier() {
|
||||
class Nip05NostrAddressVerifier {
|
||||
fun assembleUrl(nip05address: String): String? {
|
||||
val parts = nip05address.trim().split("@")
|
||||
|
||||
@@ -46,7 +44,7 @@ class Nip05NostrAddressVerifier() {
|
||||
|
||||
suspend fun fetchNip05Json(
|
||||
nip05: String,
|
||||
onSuccess: (String) -> Unit,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
checkNotInMainThread()
|
||||
@@ -60,52 +58,39 @@ class Nip05NostrAddressVerifier() {
|
||||
|
||||
try {
|
||||
val request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
HttpClientManager.getHttpClient()
|
||||
// Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint.
|
||||
HttpClientManager
|
||||
.getHttpClient()
|
||||
.newBuilder()
|
||||
.followRedirects(false)
|
||||
.build()
|
||||
.newCall(request)
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
.execute()
|
||||
.use {
|
||||
checkNotInMainThread()
|
||||
|
||||
response.use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError(
|
||||
"Could not resolve $nip05. Error: ${it.code}. Check if the server is up and if the address $nip05 is correct",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: java.io.IOException,
|
||||
) {
|
||||
onError(
|
||||
"Could not resolve $url. Check if the server is up and if the address $nip05 is correct",
|
||||
)
|
||||
e.printStackTrace()
|
||||
}
|
||||
},
|
||||
)
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError(
|
||||
"Could not resolve $nip05. Error: ${it.code}. Check if the server is up and if the address $nip05 is correct",
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError("Could not resolve '$url': ${e.message}")
|
||||
onError("Could not resolve NIP-05 $nip05 as URL $url: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun verifyNip05(
|
||||
nip05: String,
|
||||
onSuccess: (String) -> Unit,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
) {
|
||||
// check fails on tests
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -33,14 +34,23 @@ import okhttp3.Response
|
||||
import java.io.IOException
|
||||
|
||||
object Nip11CachedRetriever {
|
||||
open class RetrieveResult(val time: Long)
|
||||
open class RetrieveResult(
|
||||
val time: Long,
|
||||
)
|
||||
|
||||
class RetrieveResultError(val error: Nip11Retriever.ErrorCode, val msg: String? = null) : RetrieveResult(TimeUtils.now())
|
||||
class RetrieveResultError(
|
||||
val error: Nip11Retriever.ErrorCode,
|
||||
val msg: String? = null,
|
||||
) : RetrieveResult(TimeUtils.now())
|
||||
|
||||
class RetrieveResultSuccess(val data: Nip11RelayInformation) : RetrieveResult(TimeUtils.now())
|
||||
class RetrieveResultSuccess(
|
||||
val data: Nip11RelayInformation,
|
||||
) : RetrieveResult(TimeUtils.now())
|
||||
|
||||
val relayInformationDocumentCache = LruCache<String, RetrieveResult?>(100)
|
||||
val retriever = Nip11Retriever()
|
||||
class RetrieveResultLoading : RetrieveResult(TimeUtils.now())
|
||||
|
||||
private val relayInformationDocumentCache = LruCache<String, RetrieveResult?>(100)
|
||||
private val retriever = Nip11Retriever()
|
||||
|
||||
fun getFromCache(dirtyUrl: String): Nip11RelayInformation? {
|
||||
val result = relayInformationDocumentCache.get(RelayUrlFormatter.getHttpsUrl(dirtyUrl)) ?: return null
|
||||
@@ -53,45 +63,53 @@ object Nip11CachedRetriever {
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
val url = RelayUrlFormatter.getHttpsUrl(dirtyUrl)
|
||||
val doc = relayInformationDocumentCache.get(url)
|
||||
|
||||
if (doc != null) {
|
||||
if (doc is RetrieveResultSuccess) {
|
||||
onInfo(doc.data)
|
||||
} else if (doc is RetrieveResultLoading) {
|
||||
if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) {
|
||||
// just wait.
|
||||
} else {
|
||||
retrieve(url, dirtyUrl, onInfo, onError)
|
||||
}
|
||||
} else if (doc is RetrieveResultError) {
|
||||
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
|
||||
onError(dirtyUrl, doc.error, null)
|
||||
} else {
|
||||
retriever.loadRelayInfo(
|
||||
url = url,
|
||||
dirtyUrl = dirtyUrl,
|
||||
onInfo = {
|
||||
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
|
||||
onInfo(it)
|
||||
},
|
||||
onError = { dirtyUrl, code, errorMsg ->
|
||||
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
|
||||
onError(url, code, errorMsg)
|
||||
},
|
||||
)
|
||||
retrieve(url, dirtyUrl, onInfo, onError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
retriever.loadRelayInfo(
|
||||
url = url,
|
||||
dirtyUrl = dirtyUrl,
|
||||
onInfo = {
|
||||
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
|
||||
onInfo(it)
|
||||
},
|
||||
onError = { dirtyUrl, code, errorMsg ->
|
||||
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
|
||||
onError(url, code, errorMsg)
|
||||
},
|
||||
)
|
||||
retrieve(url, dirtyUrl, onInfo, onError)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun retrieve(
|
||||
url: String,
|
||||
dirtyUrl: String,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
relayInformationDocumentCache.put(url, RetrieveResultLoading())
|
||||
retriever.loadRelayInfo(
|
||||
url = url,
|
||||
dirtyUrl = dirtyUrl,
|
||||
onInfo = {
|
||||
checkNotInMainThread()
|
||||
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
|
||||
onInfo(it)
|
||||
},
|
||||
onError = { dirtyUrl, code, errorMsg ->
|
||||
checkNotInMainThread()
|
||||
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
|
||||
onError(url, code, errorMsg)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Nip11Retriever {
|
||||
@@ -111,9 +129,14 @@ class Nip11Retriever {
|
||||
checkNotInMainThread()
|
||||
try {
|
||||
val request: Request =
|
||||
Request.Builder().header("Accept", "application/nostr+json").url(url).build()
|
||||
Request
|
||||
.Builder()
|
||||
.header("Accept", "application/nostr+json")
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
HttpClientManager.getHttpClientForUrl(dirtyUrl)
|
||||
HttpClientManager
|
||||
.getHttpClientForUrl(dirtyUrl)
|
||||
.newCall(request)
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
@@ -24,6 +24,7 @@ import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.Request
|
||||
|
||||
@@ -37,7 +38,10 @@ object Nip96MediaServers {
|
||||
ServerName("Void.cat", "https://void.cat"),
|
||||
)
|
||||
|
||||
data class ServerName(val name: String, val baseUrl: String)
|
||||
data class ServerName(
|
||||
val name: String,
|
||||
val baseUrl: String,
|
||||
)
|
||||
|
||||
val cache: MutableMap<String, Nip96Retriever.ServerInfo> = mutableMapOf()
|
||||
|
||||
@@ -82,7 +86,8 @@ class Nip96Retriever {
|
||||
checkNotInMainThread()
|
||||
|
||||
val request: Request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("Accept", "application/nostr+json")
|
||||
.url(baseUrl.removeSuffix("/") + "/.well-known/nostr/nip96.json")
|
||||
.build()
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
@@ -29,7 +30,10 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
@@ -47,7 +51,9 @@ val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
|
||||
|
||||
fun randomChars() = List(16) { charPool.random() }.joinToString("")
|
||||
|
||||
class Nip96Uploader(val account: Account?) {
|
||||
class Nip96Uploader(
|
||||
val account: Account?,
|
||||
) {
|
||||
suspend fun uploadImage(
|
||||
uri: Uri,
|
||||
contentType: String?,
|
||||
@@ -57,6 +63,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
server: Nip96MediaServers.ServerName,
|
||||
contentResolver: ContentResolver,
|
||||
onProgress: (percentage: Float) -> Unit,
|
||||
context: Context,
|
||||
): PartialEvent {
|
||||
val serverInfo =
|
||||
Nip96Retriever()
|
||||
@@ -73,6 +80,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
serverInfo,
|
||||
contentResolver,
|
||||
onProgress,
|
||||
context,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,6 +93,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
server: Nip96Retriever.ServerInfo,
|
||||
contentResolver: ContentResolver,
|
||||
onProgress: (percentage: Float) -> Unit,
|
||||
context: Context,
|
||||
): PartialEvent {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -110,6 +119,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
sensitiveContent,
|
||||
server,
|
||||
onProgress,
|
||||
context,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -121,6 +131,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
sensitiveContent: String?,
|
||||
server: Nip96Retriever.ServerInfo,
|
||||
onProgress: (percentage: Float) -> Unit,
|
||||
context: Context,
|
||||
): PartialEvent {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -129,11 +140,11 @@ class Nip96Uploader(val account: Account?) {
|
||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
val requestBody: RequestBody
|
||||
val requestBuilder = Request.Builder()
|
||||
|
||||
requestBody =
|
||||
MultipartBody.Builder()
|
||||
val requestBody: RequestBody =
|
||||
MultipartBody
|
||||
.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("expiration", "")
|
||||
.addFormDataPart("size", length.toString())
|
||||
@@ -141,8 +152,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
alt?.let { body.addFormDataPart("alt", it) }
|
||||
sensitiveContent?.let { body.addFormDataPart("content-warning", it) }
|
||||
contentType?.let { body.addFormDataPart("content_type", it) }
|
||||
}
|
||||
.addFormDataPart(
|
||||
}.addFormDataPart(
|
||||
"file",
|
||||
"$fileName.$extension",
|
||||
object : RequestBody() {
|
||||
@@ -154,8 +164,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
inputStream.source().use(sink::writeAll)
|
||||
}
|
||||
},
|
||||
)
|
||||
.build()
|
||||
).build()
|
||||
|
||||
nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) }
|
||||
|
||||
@@ -177,11 +186,16 @@ class Nip96Uploader(val account: Account?) {
|
||||
} else if (result.status == "success" && result.nip94Event != null) {
|
||||
return result.nip94Event
|
||||
} else {
|
||||
throw RuntimeException("Failed to upload with message: ${result.message}")
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, result.message))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw RuntimeException("Error Uploading image: ${response.code}")
|
||||
val explanation = HttpStatusMessages.resourceIdFor(response.code)
|
||||
if (explanation != null) {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, stringRes(context, explanation)))
|
||||
} else {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, response.code))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,6 +204,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
hash: String,
|
||||
contentType: String?,
|
||||
server: Nip96Retriever.ServerInfo,
|
||||
context: Context,
|
||||
): Boolean {
|
||||
val extension =
|
||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||
@@ -215,7 +230,12 @@ class Nip96Uploader(val account: Account?) {
|
||||
return result.status == "success"
|
||||
}
|
||||
} else {
|
||||
throw RuntimeException("Error Uploading image: ${response.code}")
|
||||
val explanation = HttpStatusMessages.resourceIdFor(response.code)
|
||||
if (explanation != null) {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation)))
|
||||
} else {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,7 +252,8 @@ class Nip96Uploader(val account: Account?) {
|
||||
onProgress((currentResult.percentage ?: 100) / 100f)
|
||||
|
||||
val request: Request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(result.processingUrl)
|
||||
.build()
|
||||
@@ -256,13 +277,12 @@ class Nip96Uploader(val account: Account?) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun nip98Header(url: String): String? {
|
||||
return withTimeoutOrNull(5000) {
|
||||
suspend fun nip98Header(url: String): String? =
|
||||
withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun nip98Header(
|
||||
url: String,
|
||||
@@ -23,13 +23,14 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Client
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeAwardEvent
|
||||
@@ -69,90 +70,94 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
// TODO: Migrate this to a property of AccountVi
|
||||
object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
lateinit var account: Account
|
||||
var otherAccounts = listOf<HexKey>()
|
||||
|
||||
val latestEOSEs = EOSEAccount()
|
||||
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
|
||||
|
||||
fun createAccountContactListFilter(): TypedFilter {
|
||||
return TypedFilter(
|
||||
fun createAccountContactListFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createAccountMetadataFilter(): TypedFilter {
|
||||
return TypedFilter(
|
||||
fun createAccountMetadataFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createAccountRelayListFilter(): TypedFilter {
|
||||
return TypedFilter(
|
||||
fun createAccountRelayListFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds = listOf(StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND, SearchRelayListEvent.KIND, PrivateOutboxRelayListEvent.KIND),
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
StatusEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND,
|
||||
SearchRelayListEvent.KIND,
|
||||
FileServersEvent.KIND,
|
||||
PrivateOutboxRelayListEvent.KIND,
|
||||
),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 10,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createOtherAccountsBaseFilter(): TypedFilter? {
|
||||
if (otherAccounts.isEmpty()) return null
|
||||
val otherAuthors = otherAccounts.filter { it != account.userProfile().pubkeyHex }
|
||||
if (otherAuthors.isEmpty()) return null
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
ContactListEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND,
|
||||
PrivateOutboxRelayListEvent.KIND,
|
||||
SearchRelayListEvent.KIND,
|
||||
FileServersEvent.KIND,
|
||||
MuteListEvent.KIND,
|
||||
PeopleListEvent.KIND,
|
||||
),
|
||||
authors = otherAccounts.filter { it != account.userProfile().pubkeyHex },
|
||||
authors = otherAuthors,
|
||||
limit = 100,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createAccountSettingsFilter(): TypedFilter {
|
||||
return TypedFilter(
|
||||
fun createAccountSettingsFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND, BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 100,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createAccountReportsFilter(): TypedFilter {
|
||||
return TypedFilter(
|
||||
fun createAccountReportsFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(DraftEvent.KIND, ReportEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
since =
|
||||
@@ -162,18 +167,16 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createAccountLastPostsListFilter(): TypedFilter {
|
||||
return TypedFilter(
|
||||
fun createAccountLastPostsListFilter(): TypedFilter =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 400,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createNotificationFilter(): TypedFilter {
|
||||
val since =
|
||||
@@ -186,7 +189,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -219,7 +222,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
GitReplyEvent.KIND,
|
||||
@@ -241,7 +244,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(GiftWrapEvent.KIND),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
since =
|
||||
@@ -321,15 +324,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
val noteEvent = note?.event as? GiftWrapEvent
|
||||
if (noteEvent != null) {
|
||||
if (relay.brief !in note.relays) {
|
||||
LocalCache.justConsume(noteEvent, relay)
|
||||
noteEvent.cachedGift(account.signer) {
|
||||
LocalCache.justConsume(noteEvent, relay)
|
||||
this.consume(it, relay)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// new event
|
||||
event.cachedGift(account.signer) { this.consume(it, relay) }
|
||||
LocalCache.justConsume(event, relay)
|
||||
event.cachedGift(account.signer) {
|
||||
LocalCache.justConsume(event, relay)
|
||||
this.consume(it, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,15 +345,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
if (noteEvent != null) {
|
||||
if (relay.brief !in note.relays) {
|
||||
// adds the relay to seal and inner chat
|
||||
LocalCache.consume(noteEvent, relay)
|
||||
noteEvent.cachedGossip(account.signer) {
|
||||
LocalCache.consume(noteEvent, relay)
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// new event
|
||||
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
|
||||
LocalCache.justConsume(event, relay)
|
||||
event.cachedGossip(account.signer) {
|
||||
LocalCache.justConsume(event, relay)
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,30 +389,27 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
super.markAsSeenOnRelay(eventId, relay)
|
||||
|
||||
val note = LocalCache.getNoteIfExists(eventId) ?: return
|
||||
val privKey = account.keyPair.privKey ?: return
|
||||
|
||||
val noteEvent = note.event ?: return
|
||||
markInnerAsSeenOnRelay(noteEvent, privKey, relay)
|
||||
markInnerAsSeenOnRelay(noteEvent, relay)
|
||||
}
|
||||
|
||||
private fun markInnerAsSeenOnRelay(
|
||||
noteEvent: EventInterface,
|
||||
privKey: ByteArray,
|
||||
relay: Relay,
|
||||
) {
|
||||
LocalCache.getNoteIfExists(noteEvent.id())?.addRelay(relay)
|
||||
|
||||
if (noteEvent is GiftWrapEvent) {
|
||||
noteEvent.cachedGift(account.signer) { gift -> markInnerAsSeenOnRelay(gift, privKey, relay) }
|
||||
noteEvent.cachedGift(account.signer) { gift -> markInnerAsSeenOnRelay(gift, relay) }
|
||||
} else if (noteEvent is SealedGossipEvent) {
|
||||
noteEvent.cachedGossip(account.signer) { rumor ->
|
||||
markInnerAsSeenOnRelay(rumor, privKey, relay)
|
||||
markInnerAsSeenOnRelay(rumor, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
return if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
override fun updateChannelFilters() =
|
||||
if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
// gets everything about the user logged in
|
||||
accountChannel.typedFilters =
|
||||
listOfNotNull(
|
||||
@@ -419,8 +423,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
createAccountSettingsFilter(),
|
||||
createAccountLastPostsListFilter(),
|
||||
createOtherAccountsBaseFilter(),
|
||||
)
|
||||
.ifEmpty { null }
|
||||
).ifEmpty { null }
|
||||
} else {
|
||||
// just the basics.
|
||||
accountChannel.typedFilters =
|
||||
@@ -429,10 +432,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
createAccountContactListFilter(),
|
||||
createAccountRelayListFilter(),
|
||||
createAccountSettingsFilter(),
|
||||
)
|
||||
.ifEmpty { null }
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
|
||||
override fun auth(
|
||||
relay: Relay,
|
||||
@@ -24,13 +24,13 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
|
||||
object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
||||
object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
var account: Account? = null
|
||||
var channel: Channel? = null
|
||||
|
||||
@@ -58,7 +58,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
authors = listOf(myAccount.userProfile().pubkeyHex),
|
||||
limit = 50,
|
||||
@@ -71,7 +71,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
|
||||
authors = listOf(myAccount.userProfile().pubkeyHex),
|
||||
limit = 50,
|
||||
@@ -86,7 +86,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
tags = mapOf("e" to listOfNotNull(channel?.idHex)),
|
||||
limit = 200,
|
||||
@@ -96,7 +96,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
|
||||
tags = mapOf("a" to listOfNotNull(channel?.idHex)),
|
||||
limit = 200,
|
||||
@@ -113,7 +113,6 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
||||
listOfNotNull(
|
||||
createMessagesToChannelFilter(),
|
||||
createMessagesByMeToChannelFilter(),
|
||||
)
|
||||
.ifEmpty { null }
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
|
||||
object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
|
||||
object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") {
|
||||
lateinit var account: Account
|
||||
private var withRoom: ChatroomKey? = null
|
||||
|
||||
@@ -50,9 +50,9 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
authors = myPeer.users.map { it },
|
||||
authors = myPeer.users.toList(),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
@@ -73,7 +73,7 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
tags = mapOf("p" to myPeer.users.map { it }),
|
||||
@@ -103,7 +103,6 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
|
||||
listOfNotNull(
|
||||
createMessagesToMeFilter(),
|
||||
createMessagesFromMeFilter(),
|
||||
)
|
||||
.ifEmpty { null }
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -22,16 +22,16 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
|
||||
object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val latestEOSEs = EOSEAccount()
|
||||
@@ -41,11 +41,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(chatRoomList)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -53,11 +56,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PRIVATE_DMS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(PrivateDmEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(chatRoomList)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -65,11 +71,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(chatRoomList)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -82,11 +91,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
// Metadata comes from any relay
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ChannelCreateEvent.KIND),
|
||||
ids = followingEvents.toList(),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(chatRoomList)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -101,7 +113,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
// Metadata comes from any relay
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ChannelMetadataEvent.KIND),
|
||||
tags = mapOf("e" to listOf(it)),
|
||||
limit = 1,
|
||||
@@ -119,11 +131,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
tags = mapOf("e" to listOf(it)),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(chatRoomList)
|
||||
?.relayList,
|
||||
// Remember to consider spam that is being removed from the UI
|
||||
limit = 50,
|
||||
),
|
||||
@@ -149,8 +164,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
list,
|
||||
createLastChannelInfoFilter(),
|
||||
createLastMessageOfEachChannelFilter(),
|
||||
)
|
||||
.flatten()
|
||||
).flatten()
|
||||
.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,13 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
|
||||
object NostrCommunityDataSource : NostrDataSource("SingleCommunityFeed") {
|
||||
object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed") {
|
||||
private var communityToWatch: AddressableNote? = null
|
||||
|
||||
private fun createLoadCommunityFilter(): TypedFilter? {
|
||||
@@ -38,7 +38,7 @@ object NostrCommunityDataSource : NostrDataSource("SingleCommunityFeed") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
authors =
|
||||
community
|
||||
.moderators()
|
||||
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
@@ -39,7 +39,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val scope = Amethyst.instance.applicationIOScope
|
||||
@@ -66,15 +66,24 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createMarketplaceFilter(): List<TypedFilter> {
|
||||
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
|
||||
val geohashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
|
||||
val follows =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.toList()
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.hashtags
|
||||
?.toList()
|
||||
val geohashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.geotags
|
||||
?.toList()
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
authors = follows,
|
||||
kinds = listOf(ClassifiedsEvent.KIND),
|
||||
limit = 300,
|
||||
@@ -89,7 +98,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ClassifiedsEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -111,7 +120,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ClassifiedsEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -132,12 +141,12 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
)
|
||||
}
|
||||
|
||||
fun createNIP89Filter(kTags: List<String>): List<TypedFilter> {
|
||||
return listOfNotNull(
|
||||
fun createNIP89Filter(kTags: List<String>): List<TypedFilter> =
|
||||
listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(AppDefinitionEvent.KIND),
|
||||
limit = 300,
|
||||
tags = mapOf("k" to kTags),
|
||||
@@ -149,16 +158,18 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createLiveStreamFilter(): List<TypedFilter> {
|
||||
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
|
||||
val follows =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.toList()
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
authors = follows,
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
|
||||
limit = 300,
|
||||
@@ -173,7 +184,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
tags = mapOf("p" to it),
|
||||
kinds = listOf(LiveActivitiesEvent.KIND),
|
||||
limit = 100,
|
||||
@@ -189,14 +200,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createPublicChatFilter(): List<TypedFilter> {
|
||||
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
|
||||
val follows =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.toList()
|
||||
val followChats = account.selectedChatsFollowList().toList()
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
authors = follows,
|
||||
kinds = listOf(ChannelMessageEvent.KIND),
|
||||
limit = 500,
|
||||
@@ -211,7 +225,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
ids = followChats,
|
||||
kinds = listOf(ChannelCreateEvent.KIND, ChannelMessageEvent.KIND),
|
||||
limit = 300,
|
||||
@@ -229,12 +243,15 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createCommunitiesFilter(): TypedFilter {
|
||||
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
|
||||
val follows =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.users
|
||||
?.toList()
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
authors = follows,
|
||||
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
|
||||
limit = 300,
|
||||
@@ -248,14 +265,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createLiveStreamTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.hashtags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -275,14 +295,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createLiveStreamGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.geotags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -302,14 +325,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createPublicChatsTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.hashtags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
|
||||
tags =
|
||||
@@ -330,14 +356,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createPublicChatsGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.geotags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
|
||||
tags =
|
||||
@@ -358,14 +387,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createCommunitiesTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.hashtags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -385,14 +417,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createCommunitiesGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
|
||||
val hashToLoad =
|
||||
account.liveDiscoveryFollowLists.value
|
||||
?.geotags
|
||||
?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -437,8 +472,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
createPublicChatsTagsFilter(),
|
||||
createPublicChatsGeohashesFilter(),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
).toList()
|
||||
.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
@@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
|
||||
object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
|
||||
object NostrGeohashDataSource : AmethystNostrDataSource("SingleGeoHashFeed") {
|
||||
private var geohashToWatch: String? = null
|
||||
|
||||
fun createLoadHashtagFilter(): TypedFilter? {
|
||||
@@ -43,7 +43,7 @@ object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
tags =
|
||||
mapOf(
|
||||
"g" to
|
||||
@@ -20,9 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
@@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
|
||||
object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
|
||||
object NostrHashtagDataSource : AmethystNostrDataSource("SingleHashtagFeed") {
|
||||
private var hashtagToWatch: String? = null
|
||||
|
||||
fun createLoadHashtagFilter(): TypedFilter? {
|
||||
@@ -43,7 +43,7 @@ object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
tags =
|
||||
mapOf(
|
||||
"t" to
|
||||
@@ -23,9 +23,10 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
@@ -35,6 +36,7 @@ import com.vitorpamplona.quartz.events.HighlightEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
@@ -45,7 +47,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val scope = Amethyst.instance.applicationIOScope
|
||||
@@ -80,7 +82,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -108,6 +110,33 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
)
|
||||
}
|
||||
|
||||
fun createFollowMetadataAndReleaseFilter(): TypedFilter? {
|
||||
val follows = account.liveHomeFollowLists.value?.users
|
||||
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.shuffled()?.ifEmpty { null }
|
||||
|
||||
return if (followSet != null) {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
MetadataEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
),
|
||||
authors = followSet.take(500),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
?.get(account.defaultHomeFollowList.value)
|
||||
?.relayList,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun createFollowTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.liveHomeFollowLists.value?.hashtags ?: return null
|
||||
|
||||
@@ -116,7 +145,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -155,7 +184,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -194,7 +223,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -235,10 +264,10 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
followAccountChannel.typedFilters =
|
||||
listOfNotNull(
|
||||
createFollowAccountsFilter(),
|
||||
createFollowMetadataAndReleaseFilter(),
|
||||
createFollowCommunitiesFilter(),
|
||||
createFollowTagsFilter(),
|
||||
createFollowGeohashesFilter(),
|
||||
)
|
||||
.ifEmpty { null }
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
class NostrLnZapPaymentResponseDataSource(
|
||||
@@ -34,7 +31,7 @@ class NostrLnZapPaymentResponseDataSource(
|
||||
private val toUserHex: String,
|
||||
private val replyingToHex: String,
|
||||
private val authSigner: NostrSigner,
|
||||
) : NostrDataSource("LnZapPaymentResponseFeed") {
|
||||
) : AmethystNostrDataSource("LnZapPaymentResponseFeed") {
|
||||
val feedTypes = setOf(FeedType.WALLET_CONNECT)
|
||||
|
||||
private fun createWalletConnectServiceWatcher(): TypedFilter {
|
||||
@@ -42,7 +39,7 @@ class NostrLnZapPaymentResponseDataSource(
|
||||
return TypedFilter(
|
||||
types = feedTypes,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(LnZapPaymentResponseEvent.KIND),
|
||||
authors = listOf(fromServiceHex),
|
||||
tags =
|
||||
@@ -62,18 +59,4 @@ class NostrLnZapPaymentResponseDataSource(
|
||||
|
||||
channel.typedFilters = listOfNotNull(wc).ifEmpty { null }
|
||||
}
|
||||
|
||||
override fun auth(
|
||||
relay: Relay,
|
||||
challenge: String,
|
||||
) {
|
||||
super.auth(relay, challenge)
|
||||
|
||||
RelayAuthEvent.create(relay.url, challenge, authSigner) {
|
||||
Client.send(
|
||||
it,
|
||||
relay.url,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
@@ -52,7 +52,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFeed") {
|
||||
private var searchString: String? = null
|
||||
|
||||
private fun createAnythingWithIDFilter(): List<TypedFilter>? {
|
||||
@@ -95,7 +95,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND, aTag.kind),
|
||||
authors = listOfNotNull(aTag.pubKeyHex),
|
||||
// just to be sure
|
||||
@@ -110,7 +110,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
ids = listOfNotNull(hexToWatch),
|
||||
),
|
||||
),
|
||||
@@ -118,7 +118,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOfNotNull(hexToWatch),
|
||||
// just to be sure
|
||||
@@ -135,7 +135,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
search = mySearchString,
|
||||
limit = 1000,
|
||||
@@ -144,7 +144,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -165,7 +165,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.SEARCH),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
ChannelMetadataEvent.KIND,
|
||||
@@ -23,14 +23,14 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
|
||||
object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
||||
object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed") {
|
||||
private var channelsToWatch = setOf<Channel>()
|
||||
|
||||
private fun createMetadataChangeFilter(): TypedFilter? {
|
||||
@@ -44,7 +44,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ChannelMetadataEvent.KIND),
|
||||
tags = mapOf("e" to reactionsToWatch),
|
||||
),
|
||||
@@ -65,7 +65,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
||||
return TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ChannelCreateEvent.KIND),
|
||||
ids = interestedEvents.toList(),
|
||||
),
|
||||
@@ -88,7 +88,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(aTag.kind),
|
||||
tags = mapOf("d" to listOf(aTag.dTag)),
|
||||
authors = listOf(aTag.pubKeyHex),
|
||||
@@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
@@ -43,78 +43,81 @@ import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||
|
||||
object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
private var eventsToWatch = setOf<Note>()
|
||||
private var addressesToWatch = setOf<Note>()
|
||||
object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
private var nextEventsToWatch = setOf<Note>()
|
||||
private var nextAddressesToWatch = setOf<Note>()
|
||||
|
||||
private var eventsToWatchInProd = setOf<Note>()
|
||||
private var addressesToWatchInProd = setOf<Note>()
|
||||
|
||||
private fun createReactionsToWatchInAddressFilter(): List<TypedFilter>? {
|
||||
val addressesToWatch =
|
||||
val myAddressesToWatch =
|
||||
(
|
||||
eventsToWatch.filter { it.address() != null } +
|
||||
addressesToWatch.filter { it.address() != null }
|
||||
)
|
||||
.toSet()
|
||||
eventsToWatchInProd.filter { it.address() != null } +
|
||||
addressesToWatchInProd.filter { it.address() != null }
|
||||
).toSet()
|
||||
|
||||
if (addressesToWatch.isEmpty()) {
|
||||
if (myAddressesToWatch.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return groupByEOSEPresence(addressesToWatch).map {
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
CommunityPostApprovalEvent.KIND,
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
),
|
||||
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 1000,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
DeletionEvent.KIND,
|
||||
),
|
||||
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 10,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.flatten()
|
||||
return groupByEOSEPresence(myAddressesToWatch)
|
||||
.map {
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
CommunityPostApprovalEvent.KIND,
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
),
|
||||
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 1000,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
DeletionEvent.KIND,
|
||||
),
|
||||
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 10,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
private fun createAddressFilter(): List<TypedFilter>? {
|
||||
val addressesToWatch = addressesToWatch.filter { it.event == null }
|
||||
val myAddressesToWatch = addressesToWatchInProd.filter { it.event == null }
|
||||
|
||||
if (addressesToWatch.isEmpty()) {
|
||||
if (myAddressesToWatch.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return addressesToWatch.mapNotNull {
|
||||
return myAddressesToWatch.mapNotNull {
|
||||
it.address()?.let { aTag ->
|
||||
if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(aTag.kind),
|
||||
authors = listOf(aTag.pubKeyHex),
|
||||
limit = 5,
|
||||
@@ -124,7 +127,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(aTag.kind),
|
||||
tags = mapOf("d" to listOf(aTag.dTag)),
|
||||
authors = listOf(aTag.pubKeyHex),
|
||||
@@ -137,81 +140,83 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
}
|
||||
|
||||
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
|
||||
if (eventsToWatch.isEmpty()) {
|
||||
if (eventsToWatchInProd.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return groupByEOSEPresence(eventsToWatch).map {
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
OtsEvent.KIND,
|
||||
TextNoteModificationEvent.KIND,
|
||||
GitReplyEvent.KIND,
|
||||
),
|
||||
tags = mapOf("e" to it.map { it.idHex }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 1000,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
DeletionEvent.KIND,
|
||||
NIP90ContentDiscoveryResponseEvent.KIND,
|
||||
NIP90StatusEvent.KIND,
|
||||
),
|
||||
tags = mapOf("e" to it.map { it.idHex }),
|
||||
since = findMinimumEOSEs(it),
|
||||
limit = 100,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.flatten()
|
||||
return groupByEOSEPresence(eventsToWatchInProd)
|
||||
.map {
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
OtsEvent.KIND,
|
||||
TextNoteModificationEvent.KIND,
|
||||
GitReplyEvent.KIND,
|
||||
),
|
||||
tags = mapOf("e" to it.map { it.idHex }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 10000,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
DeletionEvent.KIND,
|
||||
NIP90ContentDiscoveryResponseEvent.KIND,
|
||||
NIP90StatusEvent.KIND,
|
||||
),
|
||||
tags = mapOf("e" to it.map { it.idHex }),
|
||||
since = findMinimumEOSEs(it),
|
||||
limit = 100,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
private fun createQuotesFilter(): List<TypedFilter>? {
|
||||
if (eventsToWatch.isEmpty()) {
|
||||
if (eventsToWatchInProd.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return groupByEOSEPresence(eventsToWatch).map {
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds = listOf(TextNoteEvent.KIND),
|
||||
tags = mapOf("q" to it.map { it.idHex }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 1000,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.flatten()
|
||||
return groupByEOSEPresence(eventsToWatchInProd)
|
||||
.map {
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(TextNoteEvent.KIND),
|
||||
tags = mapOf("q" to it.map { it.idHex }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 1000,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
|
||||
val directEventsToLoad = eventsToWatch.filter { it.event == null }
|
||||
val directEventsToLoad = eventsToWatchInProd.filter { it.event == null }
|
||||
|
||||
val threadingEventsToLoad =
|
||||
eventsToWatch
|
||||
eventsToWatchInProd
|
||||
.mapNotNull { it.replyTo }
|
||||
.flatten()
|
||||
.filter { it !is AddressableNote && it.event == null }
|
||||
@@ -227,7 +232,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
ids = interestedEvents.toList(),
|
||||
),
|
||||
),
|
||||
@@ -241,7 +246,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
|
||||
checkNotInMainThread()
|
||||
|
||||
eventsToWatch.forEach {
|
||||
eventsToWatchInProd.forEach {
|
||||
val eose = it.lastReactionsDownloadTime[relayUrl]
|
||||
if (eose == null) {
|
||||
it.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time))
|
||||
@@ -250,7 +255,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
}
|
||||
}
|
||||
|
||||
addressesToWatch.forEach {
|
||||
addressesToWatchInProd.forEach {
|
||||
val eose = it.lastReactionsDownloadTime[relayUrl]
|
||||
if (eose == null) {
|
||||
it.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time))
|
||||
@@ -265,6 +270,9 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
addressesToWatchInProd = nextAddressesToWatch
|
||||
eventsToWatchInProd = nextEventsToWatch
|
||||
|
||||
val reactions = createRepliesAndReactionsFilter()
|
||||
val missing = createLoadEventsIfNotLoadedFilter()
|
||||
val addresses = createAddressFilter()
|
||||
@@ -276,47 +284,55 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
}
|
||||
|
||||
fun add(eventId: Note) {
|
||||
if (!eventsToWatch.contains(eventId)) {
|
||||
eventsToWatch = eventsToWatch.plus(eventId)
|
||||
if (!nextEventsToWatch.contains(eventId)) {
|
||||
nextEventsToWatch = nextEventsToWatch.plus(eventId)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(eventId: Note) {
|
||||
if (eventsToWatch.contains(eventId)) {
|
||||
eventsToWatch = eventsToWatch.minus(eventId)
|
||||
if (nextEventsToWatch.contains(eventId)) {
|
||||
nextEventsToWatch = nextEventsToWatch.minus(eventId)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun addAddress(addressableNote: Note) {
|
||||
if (!addressesToWatch.contains(addressableNote)) {
|
||||
addressesToWatch = addressesToWatch.plus(addressableNote)
|
||||
if (!nextAddressesToWatch.contains(addressableNote)) {
|
||||
nextAddressesToWatch = nextAddressesToWatch.plus(addressableNote)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAddress(addressableNote: Note) {
|
||||
if (addressesToWatch.contains(addressableNote)) {
|
||||
addressesToWatch = addressesToWatch.minus(addressableNote)
|
||||
if (nextAddressesToWatch.contains(addressableNote)) {
|
||||
nextAddressesToWatch = nextAddressesToWatch.minus(addressableNote)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun groupByEOSEPresence(notes: Set<Note>): Collection<List<Note>> {
|
||||
return notes.groupBy { it.lastReactionsDownloadTime.keys.sorted().joinToString(",") }.values
|
||||
}
|
||||
fun groupByEOSEPresence(notes: Set<Note>): Collection<List<Note>> =
|
||||
notes
|
||||
.groupBy {
|
||||
it.lastReactionsDownloadTime.keys
|
||||
.sorted()
|
||||
.joinToString(",")
|
||||
}.values
|
||||
|
||||
fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> {
|
||||
return users.groupBy { it.latestEOSEs.keys.sorted().joinToString(",") }.values
|
||||
}
|
||||
fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> =
|
||||
users
|
||||
.groupBy {
|
||||
it.latestEOSEs.keys
|
||||
.sorted()
|
||||
.joinToString(",")
|
||||
}.values
|
||||
|
||||
fun findMinimumEOSEs(notes: List<Note>): Map<String, EOSETime> {
|
||||
val minLatestEOSEs = mutableMapOf<String, EOSETime>()
|
||||
|
||||
notes.forEach {
|
||||
it.lastReactionsDownloadTime.forEach {
|
||||
notes.forEach { note ->
|
||||
note.lastReactionsDownloadTime.forEach {
|
||||
val minEose = minLatestEOSEs[it.key]
|
||||
if (minEose == null) {
|
||||
minLatestEOSEs.put(it.key, EOSETime(it.value.time))
|
||||
@@ -21,17 +21,18 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.EOSETime
|
||||
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.StatusEvent
|
||||
|
||||
object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
var usersToWatch = setOf<User>()
|
||||
object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
private var usersToWatch = setOf<User>()
|
||||
|
||||
fun createUserMetadataFilter(): List<TypedFilter>? {
|
||||
if (usersToWatch.isEmpty()) return null
|
||||
@@ -44,7 +45,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND),
|
||||
authors = firstTimers,
|
||||
),
|
||||
@@ -67,8 +68,8 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND),
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND),
|
||||
authors = groupIds,
|
||||
since = minEOSEs,
|
||||
),
|
||||
@@ -76,15 +77,14 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ReportEvent.KIND),
|
||||
tags = mapOf("p" to groupIds),
|
||||
since = minEOSEs,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
.flatten()
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
val userChannel =
|
||||
@@ -110,8 +110,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
listOfNotNull(
|
||||
createUserMetadataFilter(),
|
||||
createUserMetadataStatusReportFilter(),
|
||||
)
|
||||
.flatten()
|
||||
).flatten()
|
||||
.ifEmpty { null }
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
|
||||
object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") {
|
||||
object NostrThreadDataSource : AmethystNostrDataSource("SingleThreadFeed") {
|
||||
private var eventToWatch: String? = null
|
||||
|
||||
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
|
||||
@@ -45,7 +45,7 @@ object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
ids = eventsToLoad.toList(),
|
||||
),
|
||||
)
|
||||
@@ -21,9 +21,9 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.AppRecommendationEvent
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
@@ -39,11 +39,12 @@ import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
|
||||
object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
var user: User? = null
|
||||
|
||||
fun loadUserProfile(user: User?) {
|
||||
@@ -55,7 +56,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 1,
|
||||
@@ -68,7 +69,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
@@ -93,9 +94,10 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(LnZapEvent.KIND),
|
||||
tags = mapOf("p" to listOf(it.pubkeyHex)),
|
||||
limit = 200,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -105,7 +107,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 1,
|
||||
@@ -118,7 +120,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags = mapOf("p" to listOf(it.pubkeyHex)),
|
||||
),
|
||||
@@ -130,7 +132,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(BadgeProfilesEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 1,
|
||||
@@ -143,7 +145,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
@@ -152,12 +154,26 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
)
|
||||
}
|
||||
|
||||
fun createProfileGalleryFilter() =
|
||||
user?.let {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(ProfileGalleryEntryEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 1000,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createReceivedAwardsFilter() =
|
||||
user?.let {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(BadgeAwardEvent.KIND),
|
||||
tags = mapOf("p" to listOf(it.pubkeyHex)),
|
||||
limit = 20,
|
||||
@@ -172,13 +188,13 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
listOfNotNull(
|
||||
createUserInfoFilter(),
|
||||
createUserPostsFilter(),
|
||||
createProfileGalleryFilter(),
|
||||
createFollowFilter(),
|
||||
createFollowersFilter(),
|
||||
createUserReceivedZapsFilter(),
|
||||
createAcceptedAwardsFilter(),
|
||||
createReceivedAwardsFilter(),
|
||||
createBookmarksFilter(),
|
||||
)
|
||||
.ifEmpty { null }
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.Filter
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
||||
@@ -34,10 +34,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav")
|
||||
val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif")
|
||||
val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet()
|
||||
|
||||
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val scope = Amethyst.instance.applicationIOScope
|
||||
@@ -64,12 +64,15 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
}
|
||||
|
||||
fun createContextualFilter(): TypedFilter {
|
||||
val follows = account.liveStoriesFollowLists.value?.users?.toList()
|
||||
val follows =
|
||||
account.liveStoriesFollowLists.value
|
||||
?.users
|
||||
?.toList()
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
authors = follows,
|
||||
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
|
||||
limit = 200,
|
||||
@@ -84,14 +87,17 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
}
|
||||
|
||||
fun createFollowTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.liveStoriesFollowLists.value?.hashtags?.toList() ?: return null
|
||||
val hashToLoad =
|
||||
account.liveStoriesFollowLists.value
|
||||
?.hashtags
|
||||
?.toList() ?: return null
|
||||
|
||||
if (hashToLoad.isEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -112,14 +118,17 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
}
|
||||
|
||||
fun createFollowGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad = account.liveStoriesFollowLists.value?.geotags?.toList() ?: return null
|
||||
val hashToLoad =
|
||||
account.liveStoriesFollowLists.value
|
||||
?.geotags
|
||||
?.toList() ?: return null
|
||||
|
||||
if (hashToLoad.isEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter =
|
||||
JsonFilter(
|
||||
Filter(
|
||||
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
@@ -155,7 +164,6 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
createContextualFilter(),
|
||||
createFollowTagsFilter(),
|
||||
createFollowGeohashesFilter(),
|
||||
)
|
||||
.ifEmpty { null }
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import okhttp3.EventListener
|
||||
import okhttp3.Protocol
|
||||
@@ -31,7 +32,10 @@ import okhttp3.Request
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Immutable data class OnlineCheckResult(val timeInMs: Long, val online: Boolean)
|
||||
@Immutable data class OnlineCheckResult(
|
||||
val timeInMs: Long,
|
||||
val online: Boolean,
|
||||
)
|
||||
|
||||
object OnlineChecker {
|
||||
val checkOnlineCache = LruCache<String, OnlineCheckResult>(100)
|
||||
@@ -57,7 +61,8 @@ object OnlineChecker {
|
||||
val result =
|
||||
if (url.startsWith("wss")) {
|
||||
val request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url.replace("wss+livekit://", "wss://"))
|
||||
.header("Upgrade", "websocket")
|
||||
@@ -68,7 +73,9 @@ object OnlineChecker {
|
||||
.build()
|
||||
|
||||
val client =
|
||||
HttpClientManager.getHttpClient().newBuilder()
|
||||
HttpClientManager
|
||||
.getHttpClient()
|
||||
.newBuilder()
|
||||
.eventListener(EventListener.NONE)
|
||||
.protocols(listOf(Protocol.HTTP_1_1))
|
||||
.build()
|
||||
@@ -79,7 +86,8 @@ object OnlineChecker {
|
||||
}
|
||||
} else {
|
||||
val request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.get()
|
||||
@@ -28,15 +28,12 @@ object PackageUtils {
|
||||
private fun isPackageInstalled(
|
||||
context: Context,
|
||||
target: String,
|
||||
): Boolean {
|
||||
return context.packageManager.getInstalledApplications(0).find { info ->
|
||||
): Boolean =
|
||||
context.packageManager.getInstalledApplications(0).find { info ->
|
||||
info.packageName == target
|
||||
} != null
|
||||
}
|
||||
|
||||
fun isOrbotInstalled(context: Context): Boolean {
|
||||
return isPackageInstalled(context, "org.torproject.android")
|
||||
}
|
||||
fun isOrbotInstalled(context: Context): Boolean = isPackageInstalled(context, "org.torproject.android")
|
||||
|
||||
fun isExternalSignerInstalled(context: Context): Boolean {
|
||||
val intent =
|
||||
@@ -29,6 +29,8 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.collectSuccessfulSigningOperations
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
@@ -40,7 +42,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.round
|
||||
|
||||
class ZapPaymentHandler(val account: Account) {
|
||||
class ZapPaymentHandler(
|
||||
val account: Account,
|
||||
) {
|
||||
@Immutable
|
||||
data class Payable(
|
||||
val info: ZapSplitSetup,
|
||||
@@ -55,6 +59,7 @@ class ZapPaymentHandler(val account: Account) {
|
||||
pollOption: Int?,
|
||||
message: String,
|
||||
context: Context,
|
||||
showErrorIfNoLnAddress: Boolean,
|
||||
onError: (String, String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
|
||||
@@ -77,8 +82,9 @@ class ZapPaymentHandler(val account: Account) {
|
||||
|
||||
if (lud16.isNullOrBlank()) {
|
||||
onError(
|
||||
context.getString(R.string.missing_lud16),
|
||||
context.getString(
|
||||
stringRes(context, R.string.missing_lud16),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
),
|
||||
)
|
||||
@@ -92,8 +98,9 @@ class ZapPaymentHandler(val account: Account) {
|
||||
|
||||
if (lud16.isNullOrBlank()) {
|
||||
onError(
|
||||
context.getString(R.string.missing_lud16),
|
||||
context.getString(
|
||||
stringRes(context, R.string.missing_lud16),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
),
|
||||
)
|
||||
@@ -112,7 +119,7 @@ class ZapPaymentHandler(val account: Account) {
|
||||
onProgress(0.05f)
|
||||
}
|
||||
|
||||
assembleAllInvoices(splitZapRequestPairs.toList(), amountMilliSats, message, onError, onProgress = {
|
||||
assembleAllInvoices(splitZapRequestPairs.toList(), amountMilliSats, message, showErrorIfNoLnAddress, onError, onProgress = {
|
||||
onProgress(it * 0.7f + 0.05f) // keeps within range.
|
||||
}, context) {
|
||||
if (it.isEmpty()) {
|
||||
@@ -130,14 +137,15 @@ class ZapPaymentHandler(val account: Account) {
|
||||
}
|
||||
} else {
|
||||
onPayViaIntent(
|
||||
it.map {
|
||||
Payable(
|
||||
info = it.key.first,
|
||||
user = it.key.second.user,
|
||||
amountMilliSats = it.value.zapValue,
|
||||
invoice = it.value.invoice,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
it
|
||||
.map {
|
||||
Payable(
|
||||
info = it.key.first,
|
||||
user = it.key.second.user,
|
||||
amountMilliSats = it.value.zapValue,
|
||||
invoice = it.value.invoice,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
onProgress(0f)
|
||||
@@ -169,6 +177,18 @@ class ZapPaymentHandler(val account: Account) {
|
||||
zapsToSend: List<ZapSplitSetup>,
|
||||
onAllDone: suspend (MutableMap<ZapSplitSetup, SignAllZapRequestsReturn>) -> Unit,
|
||||
) {
|
||||
val authorRelayList =
|
||||
note.author
|
||||
?.pubkeyHex
|
||||
?.let {
|
||||
(
|
||||
LocalCache
|
||||
.getAddressableNoteIfExists(
|
||||
AdvertisedRelayListEvent.createAddressTag(it),
|
||||
)?.event as? AdvertisedRelayListEvent?
|
||||
)?.readRelays()
|
||||
}?.toSet()
|
||||
|
||||
collectSuccessfulSigningOperations<ZapSplitSetup, SignAllZapRequestsReturn>(
|
||||
operationsInput = zapsToSend,
|
||||
runRequestFor = { next: ZapSplitSetup, onReady ->
|
||||
@@ -180,7 +200,17 @@ class ZapPaymentHandler(val account: Account) {
|
||||
}
|
||||
} else {
|
||||
val user = LocalCache.getUserIfExists(next.lnAddressOrPubKeyHex)
|
||||
prepareZapRequestIfNeeded(note, pollOption, message, zapType, user) { zapRequestJson ->
|
||||
val userRelayList =
|
||||
(
|
||||
(
|
||||
LocalCache
|
||||
.getAddressableNoteIfExists(
|
||||
AdvertisedRelayListEvent.createAddressTag(next.lnAddressOrPubKeyHex),
|
||||
)?.event as? AdvertisedRelayListEvent?
|
||||
)?.readRelays()?.toSet() ?: emptySet()
|
||||
) + (authorRelayList ?: emptySet())
|
||||
|
||||
prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList) { zapRequestJson ->
|
||||
if (zapRequestJson != null) {
|
||||
onReady(SignAllZapRequestsReturn(zapRequestJson, user))
|
||||
}
|
||||
@@ -195,6 +225,7 @@ class ZapPaymentHandler(val account: Account) {
|
||||
invoices: List<Pair<ZapSplitSetup, SignAllZapRequestsReturn>>,
|
||||
totalAmountMilliSats: Long,
|
||||
message: String,
|
||||
showErrorIfNoLnAddress: Boolean,
|
||||
onError: (String, String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
context: Context,
|
||||
@@ -211,6 +242,7 @@ class ZapPaymentHandler(val account: Account) {
|
||||
nostrZapRequest = splitZapRequestPair.second.zapRequestJson,
|
||||
zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.first.weight, totalWeight),
|
||||
message = message,
|
||||
showErrorIfNoLnAddress = showErrorIfNoLnAddress,
|
||||
onError = onError,
|
||||
onProgressStep = { percentStepForThisPayment ->
|
||||
progressAllPayments += percentStepForThisPayment / invoices.size
|
||||
@@ -250,8 +282,9 @@ class ZapPaymentHandler(val account: Account) {
|
||||
progressAllPayments += 0.5f / invoices.size
|
||||
onProgress(progressAllPayments)
|
||||
onError(
|
||||
context.getString(R.string.error_dialog_pay_invoice_error),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_dialog_pay_invoice_error),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.wallet_connect_pay_invoice_error_error,
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString() ?: "Error parsing error message",
|
||||
@@ -278,6 +311,7 @@ class ZapPaymentHandler(val account: Account) {
|
||||
nostrZapRequest: String,
|
||||
zapValue: Long,
|
||||
message: String,
|
||||
showErrorIfNoLnAddress: Boolean = true,
|
||||
onError: (String, String) -> Unit,
|
||||
onProgressStep: (percent: Float) -> Unit,
|
||||
context: Context,
|
||||
@@ -314,15 +348,19 @@ class ZapPaymentHandler(val account: Account) {
|
||||
},
|
||||
)
|
||||
} else {
|
||||
onError(
|
||||
context.getString(
|
||||
R.string.missing_lud16,
|
||||
),
|
||||
context.getString(
|
||||
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
user?.toBestDisplayName() ?: splitSetup.lnAddressOrPubKeyHex,
|
||||
),
|
||||
)
|
||||
if (showErrorIfNoLnAddress) {
|
||||
onError(
|
||||
stringRes(
|
||||
context,
|
||||
R.string.missing_lud16,
|
||||
),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
user?.toBestDisplayName() ?: splitSetup.lnAddressOrPubKeyHex,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,10 +370,12 @@ class ZapPaymentHandler(val account: Account) {
|
||||
message: String,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
overrideUser: User? = null,
|
||||
additionalRelays: Set<String>? = null,
|
||||
onReady: (String?) -> Unit,
|
||||
) {
|
||||
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
||||
account.createZapRequestFor(note, pollOption, message, zapType, overrideUser) { zapRequest ->
|
||||
account.createZapRequestFor(note, pollOption, message, zapType, overrideUser, additionalRelays) { zapRequest ->
|
||||
println("Zap Request " + zapRequest.toJson())
|
||||
onReady(zapRequest.toJson())
|
||||
}
|
||||
} else {
|
||||
@@ -27,14 +27,15 @@ import kotlinx.coroutines.CancellationException
|
||||
import java.text.NumberFormat
|
||||
|
||||
@Stable
|
||||
data class InvoiceAmount(val invoice: String, val amount: String?)
|
||||
data class InvoiceAmount(
|
||||
val invoice: String,
|
||||
val amount: String?,
|
||||
)
|
||||
|
||||
object CachedLnInvoiceParser {
|
||||
val lnInvoicesCache = LruCache<String, InvoiceAmount>(20)
|
||||
|
||||
fun cached(lnurl: String): InvoiceAmount? {
|
||||
return lnInvoicesCache[lnurl]
|
||||
}
|
||||
fun cached(lnurl: String): InvoiceAmount? = lnInvoicesCache[lnurl]
|
||||
|
||||
fun parse(lnbcWord: String): InvoiceAmount? {
|
||||
val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord)
|
||||
@@ -24,8 +24,9 @@ import android.content.Context
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.encoders.Lud06
|
||||
import okhttp3.Request
|
||||
@@ -34,7 +35,7 @@ import java.math.RoundingMode
|
||||
import java.net.URLEncoder
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class LightningAddressResolver() {
|
||||
class LightningAddressResolver {
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
|
||||
fun assembleUrl(lnaddress: String): String? {
|
||||
@@ -63,8 +64,9 @@ class LightningAddressResolver() {
|
||||
|
||||
if (url == null) {
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup,
|
||||
lnaddress,
|
||||
),
|
||||
@@ -74,7 +76,8 @@ class LightningAddressResolver() {
|
||||
|
||||
try {
|
||||
val request: Request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.build()
|
||||
@@ -84,8 +87,9 @@ class LightningAddressResolver() {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
.the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct,
|
||||
url,
|
||||
@@ -99,8 +103,9 @@ class LightningAddressResolver() {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception,
|
||||
url,
|
||||
@@ -133,7 +138,8 @@ class LightningAddressResolver() {
|
||||
}
|
||||
|
||||
val request: Request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.build()
|
||||
@@ -143,8 +149,8 @@ class LightningAddressResolver() {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.could_not_fetch_invoice_from, lnCallback),
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(context, R.string.could_not_fetch_invoice_from, lnCallback),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -173,8 +179,9 @@ class LightningAddressResolver() {
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user,
|
||||
lnaddress,
|
||||
),
|
||||
@@ -186,8 +193,9 @@ class LightningAddressResolver() {
|
||||
|
||||
if (callback == null) {
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user,
|
||||
lnaddress,
|
||||
),
|
||||
@@ -211,8 +219,9 @@ class LightningAddressResolver() {
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user,
|
||||
lnaddress,
|
||||
@@ -236,8 +245,9 @@ class LightningAddressResolver() {
|
||||
} else {
|
||||
onProgress(0.0f)
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.incorrect_invoice_amount_sats_from_it_should_have_been,
|
||||
invoiceAmount.toLong().toString(),
|
||||
lnaddress,
|
||||
@@ -253,8 +263,9 @@ class LightningAddressResolver() {
|
||||
?.let { reason ->
|
||||
onProgress(0.0f)
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user,
|
||||
lnaddress,
|
||||
@@ -265,8 +276,9 @@ class LightningAddressResolver() {
|
||||
?: run {
|
||||
onProgress(0.0f)
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
.unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user,
|
||||
lnaddress,
|
||||
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
@@ -44,7 +45,9 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
class EventNotificationConsumer(
|
||||
private val applicationContext: Context,
|
||||
) {
|
||||
suspend fun consume(event: GiftWrapEvent) {
|
||||
Log.d("EventNotificationConsumer", "New Notification Arrived")
|
||||
if (!LocalCache.justVerify(event)) return
|
||||
@@ -52,10 +55,17 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
|
||||
// PushNotification Wraps don't include a receiver.
|
||||
// Test with all logged in accounts
|
||||
var matchAccount = false
|
||||
LocalPreferences.allSavedAccounts().forEach {
|
||||
if (it.hasPrivKey || it.loggedInWithExternalSigner) {
|
||||
if (!matchAccount && it.hasPrivKey || it.loggedInWithExternalSigner) {
|
||||
LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc ->
|
||||
consumeIfMatchesAccount(event, acc)
|
||||
Log.d("EventNotificationConsumer", "New Notification Testing if for ${it.npub}")
|
||||
try {
|
||||
consumeIfMatchesAccount(event, acc)
|
||||
matchAccount = true
|
||||
} catch (e: Exception) {
|
||||
// Not for this account.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +78,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
pushWrappedEvent.cachedGift(account.signer) { notificationEvent ->
|
||||
val consumed = LocalCache.hasConsumed(notificationEvent)
|
||||
val verified = LocalCache.justVerify(notificationEvent)
|
||||
Log.d("EventNotificationConsumer", "New Notification Arrived for ${account.userProfile().toBestDisplayName()} consumed= $consumed && verified= $verified")
|
||||
Log.d("EventNotificationConsumer", "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${account.userProfile().toBestDisplayName()} consumed= $consumed && verified= $verified")
|
||||
if (!consumed && verified) {
|
||||
Log.d("EventNotificationConsumer", "New Notification was verified")
|
||||
unwrapAndConsume(notificationEvent, account) { innerEvent ->
|
||||
@@ -124,7 +134,8 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
acc: Account,
|
||||
) {
|
||||
if (
|
||||
event.createdAt > TimeUtils.fifteenMinutesAgo() && // old event being re-broadcasted
|
||||
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
|
||||
// old event being re-broadcasted
|
||||
event.pubKey != acc.userProfile().pubkeyHex
|
||||
) { // from the user
|
||||
|
||||
@@ -137,7 +148,8 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
(
|
||||
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
|
||||
acc.userProfile().hasSentMessagesTo(chatRoom)
|
||||
) && !acc.isAllHidden(chatRoom.users)
|
||||
) &&
|
||||
!acc.isAllHidden(chatRoom.users)
|
||||
|
||||
if (isKnownRoom) {
|
||||
val content = chatNote.event?.content() ?: ""
|
||||
@@ -149,6 +161,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
event.id,
|
||||
content,
|
||||
user,
|
||||
event.createdAt,
|
||||
userPicture,
|
||||
noteUri,
|
||||
applicationContext,
|
||||
@@ -178,9 +191,9 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
(
|
||||
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
|
||||
acc.userProfile().hasSentMessagesTo(it)
|
||||
) && !acc.isAllHidden(it.users)
|
||||
}
|
||||
.toSet()
|
||||
) &&
|
||||
!acc.isAllHidden(it.users)
|
||||
}.toSet()
|
||||
|
||||
note.author?.let {
|
||||
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
|
||||
@@ -189,7 +202,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
val userPicture = note.author?.profilePicture()
|
||||
val noteUri = note.toNEvent()
|
||||
notificationManager()
|
||||
.sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
|
||||
.sendDMNotification(event.id, content, user, event.createdAt, userPicture, noteUri, applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,51 +213,105 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
event: LnZapEvent,
|
||||
acc: Account,
|
||||
) {
|
||||
Log.d("EventNotificationConsumer", "Notify Start ${event.toNostrUri()}")
|
||||
val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify Not Notified Yet")
|
||||
|
||||
// old event being re-broadcast
|
||||
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify Not an old event")
|
||||
|
||||
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
val noteZapped =
|
||||
event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify ZapRequest $noteZapRequest zapped $noteZapped")
|
||||
|
||||
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify Amount Bigger than 10")
|
||||
|
||||
if (event.isTaggedUser(acc.userProfile().pubkeyHex)) {
|
||||
val amount = showAmount(event.amount)
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify Amount $amount")
|
||||
|
||||
(noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
|
||||
acc.decryptZapContentAuthor(noteZapRequest) {
|
||||
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Zap ${event.id}")
|
||||
|
||||
val author = LocalCache.getOrCreateUser(it.pubKey)
|
||||
val senderInfo = Pair(author, it.content.ifBlank { null })
|
||||
|
||||
acc.decryptContent(noteZapped) {
|
||||
val zappedContent = it.split("\n").get(0)
|
||||
if (noteZapped.event?.content() != null) {
|
||||
acc.decryptContent(noteZapped) {
|
||||
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Note")
|
||||
|
||||
val zappedContent = it.split("\n").get(0)
|
||||
|
||||
val user = senderInfo.first.toBestDisplayName()
|
||||
var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
|
||||
senderInfo.second?.ifBlank { null }?.let { title += " ($it)" }
|
||||
|
||||
var content =
|
||||
stringRes(
|
||||
applicationContext,
|
||||
R.string.app_notification_zaps_channel_message_from,
|
||||
user,
|
||||
)
|
||||
zappedContent.let {
|
||||
content +=
|
||||
" " +
|
||||
stringRes(
|
||||
applicationContext,
|
||||
R.string.app_notification_zaps_channel_message_for,
|
||||
zappedContent,
|
||||
)
|
||||
}
|
||||
val userPicture = senderInfo?.first?.profilePicture()
|
||||
val noteUri = "nostr:Notifications"
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify ${event.id} $content $title $noteUri")
|
||||
|
||||
notificationManager()
|
||||
.sendZapNotification(
|
||||
event.id,
|
||||
content,
|
||||
title,
|
||||
event.createdAt,
|
||||
userPicture,
|
||||
noteUri,
|
||||
applicationContext,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// doesn't have a base note to refer to.
|
||||
Log.d("EventNotificationConsumer", "Notify Zapped note not available")
|
||||
|
||||
val user = senderInfo.first.toBestDisplayName()
|
||||
var title =
|
||||
applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
|
||||
var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
|
||||
senderInfo.second?.ifBlank { null }?.let { title += " ($it)" }
|
||||
var content =
|
||||
applicationContext.getString(
|
||||
|
||||
val content =
|
||||
stringRes(
|
||||
applicationContext,
|
||||
R.string.app_notification_zaps_channel_message_from,
|
||||
user,
|
||||
)
|
||||
zappedContent?.let {
|
||||
content +=
|
||||
" " +
|
||||
applicationContext.getString(
|
||||
R.string.app_notification_zaps_channel_message_for,
|
||||
zappedContent,
|
||||
)
|
||||
}
|
||||
val userPicture = senderInfo?.first?.profilePicture()
|
||||
|
||||
val userPicture = senderInfo.first.profilePicture()
|
||||
val noteUri = "nostr:Notifications"
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify ${event.id} $content $title $noteUri")
|
||||
|
||||
notificationManager()
|
||||
.sendZapNotification(
|
||||
event.id,
|
||||
content,
|
||||
title,
|
||||
event.createdAt,
|
||||
userPicture,
|
||||
noteUri,
|
||||
applicationContext,
|
||||
@@ -255,8 +322,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun notificationManager(): NotificationManager {
|
||||
return ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
|
||||
fun notificationManager(): NotificationManager =
|
||||
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
|
||||
as NotificationManager
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import coil.executeBlocking
|
||||
import coil.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
object NotificationUtils {
|
||||
private var dmChannel: NotificationChannel? = null
|
||||
@@ -46,14 +47,13 @@ object NotificationUtils {
|
||||
|
||||
dmChannel =
|
||||
NotificationChannel(
|
||||
applicationContext.getString(R.string.app_notification_dms_channel_id),
|
||||
applicationContext.getString(R.string.app_notification_dms_channel_name),
|
||||
stringRes(applicationContext, R.string.app_notification_dms_channel_id),
|
||||
stringRes(applicationContext, R.string.app_notification_dms_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
)
|
||||
.apply {
|
||||
description =
|
||||
applicationContext.getString(R.string.app_notification_dms_channel_description)
|
||||
}
|
||||
).apply {
|
||||
description =
|
||||
stringRes(applicationContext, R.string.app_notification_dms_channel_description)
|
||||
}
|
||||
|
||||
// Register the channel with the system
|
||||
val notificationManager: NotificationManager =
|
||||
@@ -69,14 +69,13 @@ object NotificationUtils {
|
||||
|
||||
zapChannel =
|
||||
NotificationChannel(
|
||||
applicationContext.getString(R.string.app_notification_zaps_channel_id),
|
||||
applicationContext.getString(R.string.app_notification_zaps_channel_name),
|
||||
stringRes(applicationContext, R.string.app_notification_zaps_channel_id),
|
||||
stringRes(applicationContext, R.string.app_notification_zaps_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
)
|
||||
.apply {
|
||||
description =
|
||||
applicationContext.getString(R.string.app_notification_zaps_channel_description)
|
||||
}
|
||||
).apply {
|
||||
description =
|
||||
stringRes(applicationContext, R.string.app_notification_zaps_channel_description)
|
||||
}
|
||||
|
||||
// Register the channel with the system
|
||||
val notificationManager: NotificationManager =
|
||||
@@ -91,17 +90,19 @@ object NotificationUtils {
|
||||
id: String,
|
||||
messageBody: String,
|
||||
messageTitle: String,
|
||||
time: Long,
|
||||
pictureUrl: String?,
|
||||
uri: String,
|
||||
applicationContext: Context,
|
||||
) {
|
||||
val zapChannel = getOrCreateZapChannel(applicationContext)
|
||||
val channelId = applicationContext.getString(R.string.app_notification_zaps_channel_id)
|
||||
val channelId = stringRes(applicationContext, R.string.app_notification_zaps_channel_id)
|
||||
|
||||
sendNotification(
|
||||
id,
|
||||
messageBody,
|
||||
messageTitle,
|
||||
time,
|
||||
pictureUrl,
|
||||
uri,
|
||||
channelId,
|
||||
@@ -114,17 +115,19 @@ object NotificationUtils {
|
||||
id: String,
|
||||
messageBody: String,
|
||||
messageTitle: String,
|
||||
time: Long,
|
||||
pictureUrl: String?,
|
||||
uri: String,
|
||||
applicationContext: Context,
|
||||
) {
|
||||
val dmChannel = getOrCreateDMChannel(applicationContext)
|
||||
val channelId = applicationContext.getString(R.string.app_notification_dms_channel_id)
|
||||
val channelId = stringRes(applicationContext, R.string.app_notification_dms_channel_id)
|
||||
|
||||
sendNotification(
|
||||
id,
|
||||
messageBody,
|
||||
messageTitle,
|
||||
time,
|
||||
pictureUrl,
|
||||
uri,
|
||||
channelId,
|
||||
@@ -137,6 +140,7 @@ object NotificationUtils {
|
||||
id: String,
|
||||
messageBody: String,
|
||||
messageTitle: String,
|
||||
time: Long,
|
||||
pictureUrl: String?,
|
||||
uri: String,
|
||||
channelId: String,
|
||||
@@ -152,6 +156,7 @@ object NotificationUtils {
|
||||
id = id,
|
||||
messageBody = messageBody,
|
||||
messageTitle = messageTitle,
|
||||
time = time,
|
||||
picture = imageResult.drawable as? BitmapDrawable,
|
||||
uri = uri,
|
||||
channelId,
|
||||
@@ -163,6 +168,7 @@ object NotificationUtils {
|
||||
id = id,
|
||||
messageBody = messageBody,
|
||||
messageTitle = messageTitle,
|
||||
time = time,
|
||||
picture = null,
|
||||
uri = uri,
|
||||
channelId,
|
||||
@@ -176,6 +182,7 @@ object NotificationUtils {
|
||||
id: String,
|
||||
messageBody: String,
|
||||
messageTitle: String,
|
||||
time: Long,
|
||||
picture: BitmapDrawable?,
|
||||
uri: String,
|
||||
channelId: String,
|
||||
@@ -205,13 +212,13 @@ object NotificationUtils {
|
||||
|
||||
// Build the notification
|
||||
val builderPublic =
|
||||
NotificationCompat.Builder(
|
||||
applicationContext,
|
||||
channelId,
|
||||
)
|
||||
.setSmallIcon(R.drawable.amethyst)
|
||||
NotificationCompat
|
||||
.Builder(
|
||||
applicationContext,
|
||||
channelId,
|
||||
).setSmallIcon(R.drawable.amethyst)
|
||||
.setContentTitle(messageTitle)
|
||||
.setContentText(applicationContext.getString(R.string.app_notification_private_message))
|
||||
.setContentText(stringRes(applicationContext, R.string.app_notification_private_message))
|
||||
.setLargeIcon(picture?.bitmap)
|
||||
// .setGroup(messageTitle)
|
||||
// .setGroup(notificationGroupKey) //-> Might need a Group summary as well before we
|
||||
@@ -219,14 +226,15 @@ object NotificationUtils {
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setWhen(time * 1000)
|
||||
|
||||
// Build the notification
|
||||
val builder =
|
||||
NotificationCompat.Builder(
|
||||
applicationContext,
|
||||
channelId,
|
||||
)
|
||||
.setSmallIcon(R.drawable.amethyst)
|
||||
NotificationCompat
|
||||
.Builder(
|
||||
applicationContext,
|
||||
channelId,
|
||||
).setSmallIcon(R.drawable.amethyst)
|
||||
.setContentTitle(messageTitle)
|
||||
.setContentText(messageBody)
|
||||
.setLargeIcon(picture?.bitmap)
|
||||
@@ -237,6 +245,7 @@ object NotificationUtils {
|
||||
.setPublicVersion(builderPublic.build())
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setWhen(time * 1000)
|
||||
|
||||
notify(notId, builder.build())
|
||||
}
|
||||
@@ -25,19 +25,35 @@ import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class RegisterAccounts(
|
||||
private val accounts: List<AccountInfo>,
|
||||
) {
|
||||
private fun recursiveAuthCreation(
|
||||
val tag =
|
||||
if (BuildConfig.FLAVOR == "play") {
|
||||
"RegisterAccounts FirebaseMsgService"
|
||||
} else {
|
||||
"RegisterAccounts UnifiedPushService"
|
||||
}
|
||||
|
||||
private suspend fun signAllAuths(
|
||||
notificationToken: String,
|
||||
remainingTos: List<Pair<Account, String>>,
|
||||
output: MutableList<RelayAuthEvent>,
|
||||
@@ -48,12 +64,32 @@ class RegisterAccounts(
|
||||
return
|
||||
}
|
||||
|
||||
val next = remainingTos.first()
|
||||
coroutineScope {
|
||||
val jobs =
|
||||
remainingTos.map { accountRelayPair ->
|
||||
async {
|
||||
val result =
|
||||
withTimeoutOrNull(10000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
accountRelayPair.first.createAuthEvent(accountRelayPair.second, notificationToken) { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next.first.createAuthEvent(next.second, notificationToken) {
|
||||
output.add(it)
|
||||
recursiveAuthCreation(notificationToken, remainingTos.filter { next != it }, output, onReady)
|
||||
if (result != null) {
|
||||
output.add(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runs in parallel to avoid overcrowding Amber.
|
||||
withTimeoutOrNull(15000) {
|
||||
jobs.joinAll()
|
||||
}
|
||||
}
|
||||
|
||||
onReady(output)
|
||||
}
|
||||
|
||||
// creates proof that it controls all accounts
|
||||
@@ -63,25 +99,48 @@ class RegisterAccounts(
|
||||
onReady: (List<RelayAuthEvent>) -> Unit,
|
||||
) {
|
||||
val readyToSend =
|
||||
accounts.mapNotNull {
|
||||
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
|
||||
if (acc != null && acc.isWriteable()) {
|
||||
val readRelays =
|
||||
acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays()
|
||||
accounts
|
||||
.map {
|
||||
Log.d(tag, "Register Account ${it.npub}")
|
||||
|
||||
val relayToUse = readRelays?.firstNotNullOfOrNull { if (it.value.read) it.key else null }
|
||||
if (relayToUse != null) {
|
||||
Pair(acc, relayToUse)
|
||||
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
|
||||
|
||||
if (acc != null && acc.isWriteable()) {
|
||||
val nip65Read =
|
||||
(
|
||||
LocalCache
|
||||
.getAddressableNoteIfExists(
|
||||
AdvertisedRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex),
|
||||
)?.event as? AdvertisedRelayListEvent
|
||||
)?.readRelays() ?: acc.backupNIP65RelayList?.readRelays() ?: emptyList<String>()
|
||||
|
||||
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP65 Reads ${nip65Read.joinToString(", ")}")
|
||||
|
||||
val nip17Read =
|
||||
(
|
||||
LocalCache
|
||||
.getAddressableNoteIfExists(
|
||||
ChatMessageRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex),
|
||||
)?.event as? ChatMessageRelayListEvent
|
||||
)?.relays() ?: acc.backupDMRelayList?.relays() ?: emptyList<String>()
|
||||
|
||||
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP17 Reads ${nip17Read.joinToString(", ")}")
|
||||
|
||||
val kind3Relays = (acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays())
|
||||
val readKind3Relays = kind3Relays?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList<String>()
|
||||
|
||||
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} Kind3 Reads ${readKind3Relays.joinToString(", ")}")
|
||||
|
||||
(nip65Read + nip17Read + readKind3Relays).map {
|
||||
Pair(acc, it)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
emptyList<Pair<Account, String>>()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}.flatten()
|
||||
|
||||
val listOfAuthEvents = mutableListOf<RelayAuthEvent>()
|
||||
recursiveAuthCreation(
|
||||
signAllAuths(
|
||||
notificationToken,
|
||||
readyToSend,
|
||||
listOfAuthEvents,
|
||||
@@ -101,7 +160,8 @@ class RegisterAccounts(
|
||||
val body = jsonObject.toRequestBody(mediaType)
|
||||
|
||||
val request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url("https://push.amethyst.social/register")
|
||||
.post(body)
|
||||
@@ -112,12 +172,6 @@ class RegisterAccounts(
|
||||
val isSucess = client.newCall(request).execute().use { it.isSuccessful }
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
val tag =
|
||||
if (BuildConfig.FLAVOR == "play") {
|
||||
"FirebaseMsgService"
|
||||
} else {
|
||||
"UnifiedPushService"
|
||||
}
|
||||
Log.e(tag, "Unable to register with push server", e)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.ots
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.ots.BitcoinExplorer
|
||||
import com.vitorpamplona.quartz.ots.BlockHeader
|
||||
import com.vitorpamplona.quartz.ots.exceptions.UrlException
|
||||
@@ -42,7 +42,8 @@ class OkHttpBlockstreamExplorer : BitcoinExplorer {
|
||||
val url = "$BLOCKSTREAM_API_URL/block/$hash"
|
||||
|
||||
val request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.header("Accept", "application/json")
|
||||
.url(url)
|
||||
@@ -79,7 +80,8 @@ class OkHttpBlockstreamExplorer : BitcoinExplorer {
|
||||
val url = "$BLOCKSTREAM_API_URL/block-height/$height"
|
||||
|
||||
val request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.get()
|
||||
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.ots
|
||||
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.ots.ICalendar
|
||||
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
|
||||
@@ -30,14 +30,15 @@ import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException
|
||||
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException
|
||||
import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException
|
||||
import com.vitorpamplona.quartz.ots.exceptions.UrlException
|
||||
import com.vitorpamplona.quartz.ots.http.Request
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
/**
|
||||
* Class representing remote calendar server interface.
|
||||
*/
|
||||
class OkHttpCalendar(val url: String) : ICalendar {
|
||||
class OkHttpCalendar(
|
||||
val url: String,
|
||||
) : ICalendar {
|
||||
/**
|
||||
* Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest.
|
||||
*
|
||||
@@ -57,7 +58,8 @@ class OkHttpCalendar(val url: String) : ICalendar {
|
||||
val requestBody = digest.toRequestBody(mediaType)
|
||||
|
||||
val request =
|
||||
okhttp3.Request.Builder()
|
||||
okhttp3.Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.header("Accept", "application/vnd.opentimestamps.v1")
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
@@ -104,7 +106,8 @@ class OkHttpCalendar(val url: String) : ICalendar {
|
||||
val url = url + "/timestamp/" + Hex.encode(commitment)
|
||||
|
||||
val request =
|
||||
okhttp3.Request.Builder()
|
||||
okhttp3.Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.header("Accept", "application/vnd.opentimestamps.v1")
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.ots
|
||||
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
|
||||
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
|
||||
import com.vitorpamplona.quartz.ots.Timestamp
|
||||
@@ -33,7 +33,10 @@ import java.util.concurrent.BlockingQueue
|
||||
/**
|
||||
* For making async calls to a calendar server
|
||||
*/
|
||||
class OkHttpCalendarAsyncSubmit(private val url: String, private val digest: ByteArray) : ICalendarAsyncSubmit {
|
||||
class OkHttpCalendarAsyncSubmit(
|
||||
private val url: String,
|
||||
private val digest: ByteArray,
|
||||
) : ICalendarAsyncSubmit {
|
||||
private var queue: BlockingQueue<Optional<Timestamp>>? = null
|
||||
|
||||
fun setQueue(queue: BlockingQueue<Optional<Timestamp>>?) {
|
||||
@@ -49,7 +52,8 @@ class OkHttpCalendarAsyncSubmit(private val url: String, private val digest: Byt
|
||||
val requestBody = digest.toRequestBody(mediaType)
|
||||
|
||||
val request =
|
||||
okhttp3.Request.Builder()
|
||||
okhttp3.Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.header("Accept", "application/vnd.opentimestamps.v1")
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
@@ -25,14 +25,10 @@ import com.vitorpamplona.quartz.ots.ICalendar
|
||||
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
|
||||
|
||||
class OkHttpCalendarBuilder : CalendarBuilder {
|
||||
override fun newSyncCalendar(url: String): ICalendar {
|
||||
return OkHttpCalendar(url)
|
||||
}
|
||||
override fun newSyncCalendar(url: String): ICalendar = OkHttpCalendar(url)
|
||||
|
||||
override fun newAsyncCalendar(
|
||||
url: String,
|
||||
digest: ByteArray,
|
||||
): ICalendarAsyncSubmit {
|
||||
return OkHttpCalendarAsyncSubmit(url, digest)
|
||||
}
|
||||
): ICalendarAsyncSubmit = OkHttpCalendarAsyncSubmit(url, digest)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.service.playback
|
||||
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
|
||||
/**
|
||||
* HLS LiveStreams cannot use cache.
|
||||
*/
|
||||
@UnstableApi
|
||||
class CustomMediaSourceFactory : MediaSource.Factory {
|
||||
private var cachingFactory: MediaSource.Factory =
|
||||
DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
|
||||
private var nonCachingFactory: MediaSource.Factory =
|
||||
DefaultMediaSourceFactory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
|
||||
|
||||
override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory {
|
||||
cachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
|
||||
nonCachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory {
|
||||
cachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
|
||||
nonCachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getSupportedTypes(): IntArray = nonCachingFactory.supportedTypes
|
||||
|
||||
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
|
||||
if (mediaItem.mediaId.contains(".m3u8", true)) {
|
||||
return nonCachingFactory.createMediaSource(mediaItem)
|
||||
}
|
||||
return cachingFactory.createMediaSource(mediaItem)
|
||||
}
|
||||
}
|
||||
@@ -68,14 +68,13 @@ class MultiPlayerPlaybackManager(
|
||||
private fun getCallbackIntent(
|
||||
callbackUri: String,
|
||||
applicationContext: Context,
|
||||
): PendingIntent {
|
||||
return PendingIntent.getActivity(
|
||||
): PendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
0,
|
||||
Intent(Intent.ACTION_VIEW, callbackUri.toUri(), applicationContext, MainActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
fun getMediaSession(
|
||||
@@ -86,8 +85,6 @@ class MultiPlayerPlaybackManager(
|
||||
applicationContext: Context,
|
||||
): MediaSession {
|
||||
val existingSession = playingMap.get(id) ?: cache.get(id)
|
||||
// avoids saving positions for live streams otherwise caching goes crazy
|
||||
val mustCachePositions = !uri.contains(".m3u8", true)
|
||||
if (existingSession != null) return existingSession
|
||||
|
||||
val player =
|
||||
@@ -111,6 +108,9 @@ class MultiPlayerPlaybackManager(
|
||||
|
||||
player.addListener(
|
||||
object : Player.Listener {
|
||||
// avoids saving positions for live streams otherwise caching goes crazy
|
||||
val mustCachePositions = !uri.contains(".m3u8", true)
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (isPlaying) {
|
||||
player.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
@@ -180,7 +180,5 @@ class MultiPlayerPlaybackManager(
|
||||
}
|
||||
}
|
||||
|
||||
fun playingContent(): Collection<MediaSession> {
|
||||
return playingMap.values
|
||||
}
|
||||
fun playingContent(): Collection<MediaSession> = playingMap.values
|
||||
}
|
||||
@@ -23,51 +23,10 @@ package com.vitorpamplona.amethyst.service.playback
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
|
||||
/**
|
||||
* HLS LiveStreams cannot use cache.
|
||||
*/
|
||||
@UnstableApi
|
||||
class CustomMediaSourceFactory() : MediaSource.Factory {
|
||||
private var cachingFactory: MediaSource.Factory =
|
||||
DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
|
||||
private var nonCachingFactory: MediaSource.Factory =
|
||||
DefaultMediaSourceFactory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
|
||||
|
||||
override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory {
|
||||
cachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
|
||||
nonCachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory {
|
||||
cachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
|
||||
nonCachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getSupportedTypes(): IntArray {
|
||||
return nonCachingFactory.supportedTypes
|
||||
}
|
||||
|
||||
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
|
||||
if (mediaItem.mediaId.contains(".m3u8", true)) {
|
||||
return nonCachingFactory.createMediaSource(mediaItem)
|
||||
}
|
||||
return cachingFactory.createMediaSource(mediaItem)
|
||||
}
|
||||
}
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
|
||||
class PlaybackService : MediaSessionService() {
|
||||
private var videoViewedPositionCache = VideoViewedPositionCache()
|
||||
@@ -75,18 +34,12 @@ class PlaybackService : MediaSessionService() {
|
||||
private var managerAllInOne: MultiPlayerPlaybackManager? = null
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun newAllInOneDataSource(): MediaSource.Factory {
|
||||
// This might be needed for live kit.
|
||||
// return WssOrHttpFactory(HttpClientManager.getHttpClient())
|
||||
return CustomMediaSourceFactory()
|
||||
}
|
||||
|
||||
fun lazyDS(): MultiPlayerPlaybackManager {
|
||||
managerAllInOne?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val newInstance = MultiPlayerPlaybackManager(newAllInOneDataSource(), videoViewedPositionCache)
|
||||
val newInstance = MultiPlayerPlaybackManager(CustomMediaSourceFactory(), videoViewedPositionCache)
|
||||
managerAllInOne = newInstance
|
||||
return newInstance
|
||||
}
|
||||
@@ -102,10 +55,11 @@ class PlaybackService : MediaSessionService() {
|
||||
HttpClientManager.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun onProxyUpdated() {
|
||||
val toDestroyAllInOne = managerAllInOne
|
||||
|
||||
managerAllInOne = MultiPlayerPlaybackManager(newAllInOneDataSource(), videoViewedPositionCache)
|
||||
managerAllInOne = MultiPlayerPlaybackManager(CustomMediaSourceFactory(), videoViewedPositionCache)
|
||||
|
||||
toDestroyAllInOne?.releaseAppPlayers()
|
||||
}
|
||||
@@ -59,12 +59,12 @@ class VideoCache {
|
||||
// This method should be called when proxy setting changes.
|
||||
fun renewCacheFactory(client: OkHttpClient) {
|
||||
cacheDataSourceFactory =
|
||||
CacheDataSource.Factory()
|
||||
CacheDataSource
|
||||
.Factory()
|
||||
.setCache(simpleCache)
|
||||
.setUpstreamDataSourceFactory(
|
||||
OkHttpDataSource.Factory(client),
|
||||
)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
||||
).setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
||||
}
|
||||
|
||||
fun get(client: OkHttpClient): CacheDataSource.Factory {
|
||||
@@ -32,7 +32,5 @@ class VideoViewedPositionCache {
|
||||
cachedPosition.put(uri, position)
|
||||
}
|
||||
|
||||
fun get(uri: String): Long? {
|
||||
return cachedPosition.get(uri)
|
||||
}
|
||||
fun get(uri: String): Long? = cachedPosition.get(uri)
|
||||
}
|
||||
@@ -44,11 +44,7 @@ class WssDataStreamCollector : WebSocketListener() {
|
||||
wssData.removeAll(wssData)
|
||||
}
|
||||
|
||||
fun canStream(): Boolean {
|
||||
return wssData.size > 0
|
||||
}
|
||||
fun canStream(): Boolean = wssData.size > 0
|
||||
|
||||
fun getNextStream(): ByteString {
|
||||
return wssData.pollFirst()
|
||||
}
|
||||
fun getNextStream(): ByteString = wssData.pollFirst()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user