Compare commits
422 Commits
v1.2.3+for
...
v2.0.1+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdcebf1576 | ||
|
|
239b6f8202 | ||
|
|
8404c79148 | ||
|
|
5b2d04e09d | ||
|
|
6bd13f99d2 | ||
|
|
2e8e12c1c8 | ||
|
|
17929a6b2d | ||
|
|
9455eaf820 | ||
|
|
cc054487ba | ||
|
|
e2df320d00 | ||
|
|
d74313f996 | ||
|
|
b3cab67049 | ||
|
|
996f0b22b9 | ||
|
|
67952ea98e | ||
|
|
7a02ca435f | ||
|
|
8d55f62da9 | ||
|
|
ee0048a406 | ||
|
|
14dcc769f2 | ||
|
|
f2e6255eb3 | ||
|
|
7d392e20fb | ||
|
|
3f2e6d2be6 | ||
|
|
a74c285f77 | ||
|
|
8aec4a2717 | ||
|
|
a106193039 | ||
|
|
5736605771 | ||
|
|
9f0a710e40 | ||
|
|
f6f8cfb8e8 | ||
|
|
4e28f011dd | ||
|
|
4bb255e0bb | ||
|
|
a48d545989 | ||
|
|
e58cf54a5a | ||
|
|
2a45776f01 | ||
|
|
332aa906b5 | ||
|
|
ef624dfe30 | ||
|
|
1b0b9bedea | ||
|
|
e2672bea6e | ||
|
|
5916001462 | ||
|
|
cacd403b89 | ||
|
|
2732f710da | ||
|
|
070268eafd | ||
|
|
7149971351 | ||
|
|
f46668c023 | ||
|
|
74d0937078 | ||
|
|
b236e88ef2 | ||
|
|
12acf6761b | ||
|
|
8522f1fd29 | ||
|
|
7677ad39ca | ||
|
|
91b4dc412b | ||
|
|
3cfea0e660 | ||
|
|
c896fd8df8 | ||
|
|
72cd987284 | ||
|
|
d868d05080 | ||
|
|
9dfb039a69 | ||
|
|
f2920e877b | ||
|
|
ecb0b3f9d7 | ||
|
|
13ff54663c | ||
|
|
604d9b008a | ||
|
|
7044a69a71 | ||
|
|
67c128be69 | ||
|
|
1ecbbc2d4b | ||
|
|
82c481b014 | ||
|
|
e7da6d7897 | ||
|
|
e9f1e3038b | ||
|
|
a591096819 | ||
|
|
f1ef60475f | ||
|
|
ce75bb3984 | ||
|
|
d59235e04c | ||
|
|
42f8f7e58f | ||
|
|
f9cbc9ae27 | ||
|
|
a50d6599bf | ||
|
|
cc578b496e | ||
|
|
662944d246 | ||
|
|
42810df4a5 | ||
|
|
c3a058d2e1 | ||
|
|
ce5d835ae5 | ||
|
|
60dd561729 | ||
|
|
08c9f9ad7d | ||
|
|
d47907906d | ||
|
|
935f0f6e05 | ||
|
|
ac0b21d574 | ||
|
|
9b5d05369f | ||
|
|
489f9f5e59 | ||
|
|
e401dc8d6e | ||
|
|
3e89087aba | ||
|
|
e62f7c23c9 | ||
|
|
91ed7d49b5 | ||
|
|
c9e9abd811 | ||
|
|
b1e5023f62 | ||
|
|
604690b3f5 | ||
|
|
13ada6ecc6 | ||
|
|
b897eb913e | ||
|
|
c25602a650 | ||
|
|
2defc9af3f | ||
|
|
446525389b | ||
|
|
756b30d04f | ||
|
|
51ec842815 | ||
|
|
c38822849e | ||
|
|
3c69201f67 | ||
|
|
ed9d701406 | ||
|
|
e70c5aa2e9 | ||
|
|
0c4589b257 | ||
|
|
84d08392fb | ||
|
|
8ff117308d | ||
|
|
b6c703adbc | ||
|
|
22e6934de5 | ||
|
|
1b8a1d69ac | ||
|
|
b6ae83937b | ||
|
|
7115556663 | ||
|
|
cb3296661e | ||
|
|
6dd20a6df9 | ||
|
|
71c1d0e59a | ||
|
|
2b275e1ff7 | ||
|
|
8520ee42cb | ||
|
|
a7fcae1033 | ||
|
|
19bd189b33 | ||
|
|
2d5089c047 | ||
|
|
be7469bd54 | ||
|
|
146d8daa6e | ||
|
|
f3928d9e09 | ||
|
|
d4090d459d | ||
|
|
7dd7554c08 | ||
|
|
9de9a1d97d | ||
|
|
04ee366fbe | ||
|
|
c8784150fc | ||
|
|
7b7bccb37a | ||
|
|
84e2636bca | ||
|
|
dc73613b56 | ||
|
|
fd8868ef4d | ||
|
|
f443d659a3 | ||
|
|
24eb82a79d | ||
|
|
43d806fb01 | ||
|
|
127df0b8e0 | ||
|
|
c2989df902 | ||
|
|
3f9ee99b69 | ||
|
|
d47c4e63d7 | ||
|
|
7204c4e804 | ||
|
|
f1131cf8e7 | ||
|
|
5b9a8beb07 | ||
|
|
9e18e35c66 | ||
|
|
9db3dfa955 | ||
|
|
4a3e56d300 | ||
|
|
3bb4125c50 | ||
|
|
ce58883618 | ||
|
|
e7d3c60bac | ||
|
|
08e90139ad | ||
|
|
0d4dc34453 | ||
|
|
fe142c4626 | ||
|
|
d8dfa6017d | ||
|
|
b7a5d4296b | ||
|
|
85d4c1fc24 | ||
|
|
66489d79be | ||
|
|
30a66a26c6 | ||
|
|
fbc3081e68 | ||
|
|
4ac7615cfb | ||
|
|
a96b0d06a4 | ||
|
|
7a66c94907 | ||
|
|
4e38bc5769 | ||
|
|
0dc428dbd6 | ||
|
|
6ac7fc94ea | ||
|
|
af28ed1783 | ||
|
|
00daf084f2 | ||
|
|
5518848e28 | ||
|
|
6ded856b2f | ||
|
|
d302f5132e | ||
|
|
012e29ee3a | ||
|
|
dbe9579d7f | ||
|
|
c8d0221d9b | ||
|
|
885c663d93 | ||
|
|
3b399d5815 | ||
|
|
9f18d1bc8b | ||
|
|
9a8cf61e38 | ||
|
|
eb822282c0 | ||
|
|
273823a65f | ||
|
|
f2aa1400c5 | ||
|
|
cf8a9e1823 | ||
|
|
b3320d534b | ||
|
|
f58b4c2989 | ||
|
|
47b3b1e307 | ||
|
|
e5649c4a42 | ||
|
|
cea77eca02 | ||
|
|
4fbbcfba59 | ||
|
|
6a24f70537 | ||
|
|
4681160924 | ||
|
|
6f8c2f4a44 | ||
|
|
69de9dce38 | ||
|
|
721ae9c68d | ||
|
|
6a6ed89d29 | ||
|
|
3e4377a366 | ||
|
|
287de66e0c | ||
|
|
df2ae9d964 | ||
|
|
277282d7f5 | ||
|
|
4f983829b7 | ||
|
|
ef55f1f49b | ||
|
|
aebf7e9f1f | ||
|
|
a014ce6eb5 | ||
|
|
aed1efceb9 | ||
|
|
124c375b14 | ||
|
|
1d46e22a7f | ||
|
|
28c334429d | ||
|
|
4726f98d4f | ||
|
|
985f382436 | ||
|
|
f822c788b0 | ||
|
|
664851cac5 | ||
|
|
46f8982aa6 | ||
|
|
7b3cec9289 | ||
|
|
a3f227cb8d | ||
|
|
d71feb2cfc | ||
|
|
b69565e9e6 | ||
|
|
bf996feccf | ||
|
|
b1143b0eec | ||
|
|
4c6914c271 | ||
|
|
e03a62064e | ||
|
|
ce977163c2 | ||
|
|
ded74bda83 | ||
|
|
7180113397 | ||
|
|
81ac8a3bc9 | ||
|
|
4b74da5d38 | ||
|
|
0375cfa260 | ||
|
|
52108a675a | ||
|
|
406e95d3f7 | ||
|
|
a9f355dea9 | ||
|
|
1a63c12327 | ||
|
|
7bc2f8b352 | ||
|
|
d56ef227a7 | ||
|
|
fcc371ab5a | ||
|
|
9f43a99772 | ||
|
|
9fc5a4e390 | ||
|
|
a2b3f873f6 | ||
|
|
f6c1509e48 | ||
|
|
d9bbb32a28 | ||
|
|
d2ef6e77af | ||
|
|
6133e9bac3 | ||
|
|
6ca48f35f1 | ||
|
|
cb016b4383 | ||
|
|
d0b21df28b | ||
|
|
ce48ee888a | ||
|
|
5cd8bc5a46 | ||
|
|
4109cd75d3 | ||
|
|
97a889e019 | ||
|
|
b9a1b3591d | ||
|
|
fde84e3cfb | ||
|
|
0985eb4fac | ||
|
|
5957c1a221 | ||
|
|
e7e34aa2c8 | ||
|
|
b8742591b8 | ||
|
|
50e73ac12e | ||
|
|
9ed277a9b2 | ||
|
|
c9e2984d68 | ||
|
|
00aaff10a7 | ||
|
|
6da4256adf | ||
|
|
da9c826791 | ||
|
|
bed02e248e | ||
|
|
f53531889f | ||
|
|
ef3246ae2a | ||
|
|
5504b534f7 | ||
|
|
0e553d7868 | ||
|
|
38be51367e | ||
|
|
0d4af0970c | ||
|
|
b51335ffd6 | ||
|
|
444fa07984 | ||
|
|
feb0f304fb | ||
|
|
ccd4a1aa9f | ||
|
|
b3723c2977 | ||
|
|
c5b70a9ada | ||
|
|
82789179e7 | ||
|
|
b0a309a817 | ||
|
|
32a27b6e59 | ||
|
|
f73a318dad | ||
|
|
4fff2c5f5c | ||
|
|
81abac657f | ||
|
|
74decd3ec7 | ||
|
|
11d17d1f3f | ||
|
|
ca2384ba8c | ||
|
|
ded23342db | ||
|
|
a35c14865f | ||
|
|
0952d97557 | ||
|
|
e1db5f15ca | ||
|
|
a99741c732 | ||
|
|
d6c8e8afc1 | ||
|
|
879981e335 | ||
|
|
87840dd731 | ||
|
|
c9e467ac2f | ||
|
|
74a5e970d9 | ||
|
|
935de7d02e | ||
|
|
cb8fddd156 | ||
|
|
40ca834880 | ||
|
|
c10206ef6f | ||
|
|
e752d10e31 | ||
|
|
119bbc2b5c | ||
|
|
6136260adc | ||
|
|
979ee9fdff | ||
|
|
ab71e06ef1 | ||
|
|
6d487f011f | ||
|
|
50403cf674 | ||
|
|
276df264cf | ||
|
|
55e18cf9af | ||
|
|
e5392d3265 | ||
|
|
2de7c1d3b9 | ||
|
|
c21885139c | ||
|
|
275cbaa924 | ||
|
|
4b6c35c9c0 | ||
|
|
d339fa1e12 | ||
|
|
31a5fc9153 | ||
|
|
966f758d9f | ||
|
|
6979c5097d | ||
|
|
68005c762f | ||
|
|
06543d5fc2 | ||
|
|
a70d39065c | ||
|
|
dc75882cee | ||
|
|
e47f253c0e | ||
|
|
d05f3932b2 | ||
|
|
be425282a6 | ||
|
|
a04522ff72 | ||
|
|
90a93ffba6 | ||
|
|
069c55d4b9 | ||
|
|
1efaf4b605 | ||
|
|
ff74516ce2 | ||
|
|
aee4b7aaab | ||
|
|
1d94479bde | ||
|
|
85c95d899e | ||
|
|
18d4210e7d | ||
|
|
ac25bc6d42 | ||
|
|
ec05ef3b4c | ||
|
|
9827d97374 | ||
|
|
4fab92d516 | ||
|
|
44bc9c4e40 | ||
|
|
1030773ef6 | ||
|
|
1a0cb4b8c8 | ||
|
|
4295a3672c | ||
|
|
fd2a8fe230 | ||
|
|
e2d1eccfb9 | ||
|
|
bb4a52f03a | ||
|
|
50360059ce | ||
|
|
63bcef990b | ||
|
|
94eb6b5775 | ||
|
|
6595a088fb | ||
|
|
b463ef65ce | ||
|
|
b22a25e7af | ||
|
|
05d1d3e725 | ||
|
|
37261928c2 | ||
|
|
63132110d9 | ||
|
|
ffd7e415a2 | ||
|
|
d43664d018 | ||
|
|
8e06362ff8 | ||
|
|
625ccfd31f | ||
|
|
d7b5d242ff | ||
|
|
7973c87b9a | ||
|
|
3085b1507b | ||
|
|
f17ef17492 | ||
|
|
8d0a31d0f9 | ||
|
|
6c6d3fed05 | ||
|
|
b616e2e11b | ||
|
|
b18771bb79 | ||
|
|
40ef4c179a | ||
|
|
62212dc6c9 | ||
|
|
3ed2b67037 | ||
|
|
d240750606 | ||
|
|
ed009d3e2e | ||
|
|
3af7518cf4 | ||
|
|
91b9fdf5ce | ||
|
|
be2fa0f217 | ||
|
|
22f5667549 | ||
|
|
a8d9b4538b | ||
|
|
2b7b3de043 | ||
|
|
aa9caefed1 | ||
|
|
14bbe1ffef | ||
|
|
17957b69d1 | ||
|
|
a24b4363d7 | ||
|
|
02ddad22e7 | ||
|
|
a705512dc5 | ||
|
|
cfd6954755 | ||
|
|
702ac43f86 | ||
|
|
6ede2d22bb | ||
|
|
315d26ad52 | ||
|
|
a78b0687f7 | ||
|
|
148b8e9369 | ||
|
|
ae6ce0f9b0 | ||
|
|
31c8665653 | ||
|
|
7c6ec2e3d7 | ||
|
|
31c7116a15 | ||
|
|
5c480b37b3 | ||
|
|
92335d8678 | ||
|
|
1dcb5717ea | ||
|
|
36f1a557d7 | ||
|
|
bd7157c172 | ||
|
|
34a2af8429 | ||
|
|
15883f2138 | ||
|
|
89501271ce | ||
|
|
968a6ea9b3 | ||
|
|
e253d8f4f3 | ||
|
|
cfabe47e10 | ||
|
|
d3fe7857b7 | ||
|
|
642e96a439 | ||
|
|
2b8451e045 | ||
|
|
62074e554a | ||
|
|
0434cda2da | ||
|
|
4b4c88d44d | ||
|
|
9e116bec97 | ||
|
|
dc25e16c00 | ||
|
|
42a7c324fa | ||
|
|
849888d128 | ||
|
|
8a7e910e7c | ||
|
|
72d72d443e | ||
|
|
7b0a3f0f96 | ||
|
|
cf4604e0d8 | ||
|
|
e873dd7d0a | ||
|
|
4492e940e5 | ||
|
|
c833c03dc3 | ||
|
|
30b0d226b5 | ||
|
|
8afad21113 | ||
|
|
6b5e5b0f25 | ||
|
|
3c0ab6822f | ||
|
|
f7215d00ca | ||
|
|
43bbe9be0f | ||
|
|
477a691c9e | ||
|
|
e5d60050a2 | ||
|
|
9a698fda18 | ||
|
|
d5701c1073 | ||
|
|
955b9a4b2b | ||
|
|
09ffda2605 | ||
|
|
039fb0c505 | ||
|
|
20799ef1a8 |
18
README.md
18
README.md
@@ -10,7 +10,7 @@
|
||||
|
||||
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
|
||||
> A fork of the [Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app, focusing on [Glitch](https://glitch-soc.github.io/docs) compatibility, a pretty UI and adding new features that I feel make using the Fediverse a more pleasent experience.
|
||||
> A fork of the [Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app, focusing on [Glitch](https://glitch-soc.github.io/docs) compatibility, a pretty UI and adding new features that I feel make using the Fediverse a more pleasant experience.
|
||||
|
||||
|
||||
## Key features
|
||||
@@ -54,9 +54,15 @@ You can create drafts, edit them, send them manually later or set a scheduled da
|
||||
|
||||
## Installation
|
||||
|
||||
### IzzyOnDroid
|
||||
### Google Play Store
|
||||
|
||||
[apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
|
||||
[https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
|
||||
|
||||
### F-Droid via IzzyOnDroid
|
||||
|
||||
[https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
|
||||
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
|
||||
@@ -64,11 +70,11 @@ Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first
|
||||
|
||||
[`https://apt.izzysoft.de/fdroid/repo`](https://apt.izzysoft.de/fdroid/repo)
|
||||
|
||||
### Google Play Store
|
||||
### F-Droid via saunarepo
|
||||
|
||||
[play.google.com/store/apps/details?id=org.joinmastodon.android.sk](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)
|
||||
[https://repo.the-sauna.icu](https://repo.the-sauna.icu/)
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
|
||||
<a href="https://repo.the-sauna.icu"><img height="28" alt="Get it on SaunaRepo" src="img/saunarepo-badge.svg"></a>
|
||||
|
||||
### F-Droid
|
||||
|
||||
|
||||
1
img/saunarepo-badge.svg
Normal file
1
img/saunarepo-badge.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="124.25" height="28" role="img" aria-label="SAUNAREPO"><title>SAUNAREPO</title><g shape-rendering="crispEdges"><rect width="124.25" height="28" fill="#fb8441"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><image x="9" y="7" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPkFuZHJvaWQ8L3RpdGxlPjxwYXRoIGQ9Ik0xNy41MjMgMTUuMzQxNGMtLjU1MTEgMC0uOTk5My0uNDQ4Ni0uOTk5My0uOTk5N3MuNDQ4My0uOTk5My45OTkzLS45OTkzYy41NTExIDAgLjk5OTMuNDQ4My45OTkzLjk5OTMuMDAwMS41NTExLS40NDgyLjk5OTctLjk5OTMuOTk5N20tMTEuMDQ2IDBjLS41NTExIDAtLjk5OTMtLjQ0ODYtLjk5OTMtLjk5OTdzLjQ0ODItLjk5OTMuOTk5My0uOTk5M2MuNTUxMSAwIC45OTkzLjQ0ODMuOTk5My45OTkzIDAgLjU1MTEtLjQ0ODMuOTk5Ny0uOTk5My45OTk3bTExLjQwNDUtNi4wMmwxLjk5NzMtMy40NTkyYS40MTYuNDE2IDAgMDAtLjE1MjEtLjU2NzYuNDE2LjQxNiAwIDAwLS41Njc2LjE1MjFsLTIuMDIyMyAzLjUwM0MxNS41OTAyIDguMjQzOSAxMy44NTMzIDcuODUwOCAxMiA3Ljg1MDhzLTMuNTkwMi4zOTMxLTUuMTM2NyAxLjA5ODlMNC44NDEgNS40NDY3YS40MTYxLjQxNjEgMCAwMC0uNTY3Ny0uMTUyMS40MTU3LjQxNTcgMCAwMC0uMTUyMS41Njc2bDEuOTk3MyAzLjQ1OTJDMi42ODg5IDExLjE4NjcuMzQzMiAxNC42NTg5IDAgMTguNzYxaDI0Yy0uMzQzNS00LjEwMjEtMi42ODkyLTcuNTc0My02LjExODUtOS40Mzk2Ii8+PC9zdmc+"/><text transform="scale(.1)" x="721.25" y="175" textLength="802.5" fill="#fff" font-weight="bold">SAUNAREPO</text></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
279
mastodon/.editorconfig
Normal file
279
mastodon/.editorconfig
Normal file
@@ -0,0 +1,279 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
insert_final_newline = false
|
||||
max_line_length = 300
|
||||
tab_width = 4
|
||||
ij_continuation_indent_size = 8
|
||||
ij_formatter_off_tag = @formatter:off
|
||||
ij_formatter_on_tag = @formatter:on
|
||||
ij_formatter_tags_enabled = false
|
||||
ij_smart_tabs = false
|
||||
ij_visual_guides = none
|
||||
ij_wrap_on_typing = false
|
||||
|
||||
[*.java]
|
||||
ij_java_align_consecutive_assignments = false
|
||||
ij_java_align_consecutive_variable_declarations = false
|
||||
ij_java_align_group_field_declarations = false
|
||||
ij_java_align_multiline_annotation_parameters = false
|
||||
ij_java_align_multiline_array_initializer_expression = false
|
||||
ij_java_align_multiline_assignment = false
|
||||
ij_java_align_multiline_binary_operation = false
|
||||
ij_java_align_multiline_chained_methods = false
|
||||
ij_java_align_multiline_extends_list = false
|
||||
ij_java_align_multiline_for = true
|
||||
ij_java_align_multiline_method_parentheses = false
|
||||
ij_java_align_multiline_parameters = true
|
||||
ij_java_align_multiline_parameters_in_calls = false
|
||||
ij_java_align_multiline_parenthesized_expression = false
|
||||
ij_java_align_multiline_records = true
|
||||
ij_java_align_multiline_resources = true
|
||||
ij_java_align_multiline_ternary_operation = false
|
||||
ij_java_align_multiline_text_blocks = false
|
||||
ij_java_align_multiline_throws_list = false
|
||||
ij_java_align_subsequent_simple_methods = false
|
||||
ij_java_align_throws_keyword = false
|
||||
ij_java_align_types_in_multi_catch = true
|
||||
ij_java_annotation_parameter_wrap = off
|
||||
ij_java_array_initializer_new_line_after_left_brace = false
|
||||
ij_java_array_initializer_right_brace_on_new_line = false
|
||||
ij_java_array_initializer_wrap = off
|
||||
ij_java_assert_statement_colon_on_next_line = false
|
||||
ij_java_assert_statement_wrap = off
|
||||
ij_java_assignment_wrap = off
|
||||
ij_java_binary_operation_sign_on_next_line = false
|
||||
ij_java_binary_operation_wrap = off
|
||||
ij_java_blank_lines_after_anonymous_class_header = 0
|
||||
ij_java_blank_lines_after_class_header = 0
|
||||
ij_java_blank_lines_after_imports = 1
|
||||
ij_java_blank_lines_after_package = 1
|
||||
ij_java_blank_lines_around_class = 1
|
||||
ij_java_blank_lines_around_field = 0
|
||||
ij_java_blank_lines_around_field_in_interface = 0
|
||||
ij_java_blank_lines_around_initializer = 1
|
||||
ij_java_blank_lines_around_method = 1
|
||||
ij_java_blank_lines_around_method_in_interface = 1
|
||||
ij_java_blank_lines_before_class_end = 0
|
||||
ij_java_blank_lines_before_imports = 1
|
||||
ij_java_blank_lines_before_method_body = 0
|
||||
ij_java_blank_lines_before_package = 0
|
||||
ij_java_block_brace_style = end_of_line
|
||||
ij_java_block_comment_add_space = false
|
||||
ij_java_block_comment_at_first_column = true
|
||||
ij_java_builder_methods = none
|
||||
ij_java_call_parameters_new_line_after_left_paren = false
|
||||
ij_java_call_parameters_right_paren_on_new_line = false
|
||||
ij_java_call_parameters_wrap = off
|
||||
ij_java_case_statement_on_separate_line = true
|
||||
ij_java_catch_on_new_line = false
|
||||
ij_java_class_annotation_wrap = split_into_lines
|
||||
ij_java_class_brace_style = end_of_line
|
||||
ij_java_class_count_to_use_import_on_demand = 99
|
||||
ij_java_class_names_in_javadoc = 1
|
||||
ij_java_do_not_indent_top_level_class_members = false
|
||||
ij_java_do_not_wrap_after_single_annotation = false
|
||||
ij_java_do_not_wrap_after_single_annotation_in_parameter = false
|
||||
ij_java_do_while_brace_force = never
|
||||
ij_java_doc_add_blank_line_after_description = true
|
||||
ij_java_doc_add_blank_line_after_param_comments = false
|
||||
ij_java_doc_add_blank_line_after_return = false
|
||||
ij_java_doc_add_p_tag_on_empty_lines = true
|
||||
ij_java_doc_align_exception_comments = true
|
||||
ij_java_doc_align_param_comments = true
|
||||
ij_java_doc_do_not_wrap_if_one_line = false
|
||||
ij_java_doc_enable_formatting = true
|
||||
ij_java_doc_enable_leading_asterisks = true
|
||||
ij_java_doc_indent_on_continuation = false
|
||||
ij_java_doc_keep_empty_lines = true
|
||||
ij_java_doc_keep_empty_parameter_tag = true
|
||||
ij_java_doc_keep_empty_return_tag = true
|
||||
ij_java_doc_keep_empty_throws_tag = true
|
||||
ij_java_doc_keep_invalid_tags = true
|
||||
ij_java_doc_param_description_on_new_line = false
|
||||
ij_java_doc_preserve_line_breaks = false
|
||||
ij_java_doc_use_throws_not_exception_tag = true
|
||||
ij_java_else_on_new_line = false
|
||||
ij_java_enum_constants_wrap = off
|
||||
ij_java_extends_keyword_wrap = off
|
||||
ij_java_extends_list_wrap = off
|
||||
ij_java_field_annotation_wrap = split_into_lines
|
||||
ij_java_finally_on_new_line = false
|
||||
ij_java_for_brace_force = never
|
||||
ij_java_for_statement_new_line_after_left_paren = false
|
||||
ij_java_for_statement_right_paren_on_new_line = false
|
||||
ij_java_for_statement_wrap = off
|
||||
ij_java_generate_final_locals = false
|
||||
ij_java_generate_final_parameters = false
|
||||
ij_java_if_brace_force = never
|
||||
ij_java_imports_layout = android.**,|,com.**,|,junit.**,|,net.**,|,org.**,|,java.**,|,javax.**,|,*,|,$*,|
|
||||
ij_java_indent_case_from_switch = true
|
||||
ij_java_insert_inner_class_imports = false
|
||||
ij_java_insert_override_annotation = true
|
||||
ij_java_keep_blank_lines_before_right_brace = 2
|
||||
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
|
||||
ij_java_keep_blank_lines_in_code = 2
|
||||
ij_java_keep_blank_lines_in_declarations = 2
|
||||
ij_java_keep_builder_methods_indents = false
|
||||
ij_java_keep_control_statement_in_one_line = true
|
||||
ij_java_keep_first_column_comment = true
|
||||
ij_java_keep_indents_on_empty_lines = false
|
||||
ij_java_keep_line_breaks = true
|
||||
ij_java_keep_multiple_expressions_in_one_line = false
|
||||
ij_java_keep_simple_blocks_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = false
|
||||
ij_java_keep_simple_lambdas_in_one_line = false
|
||||
ij_java_keep_simple_methods_in_one_line = false
|
||||
ij_java_label_indent_absolute = false
|
||||
ij_java_label_indent_size = 0
|
||||
ij_java_lambda_brace_style = end_of_line
|
||||
ij_java_layout_static_imports_separately = true
|
||||
ij_java_line_comment_add_space = false
|
||||
ij_java_line_comment_add_space_on_reformat = false
|
||||
ij_java_line_comment_at_first_column = true
|
||||
ij_java_method_annotation_wrap = split_into_lines
|
||||
ij_java_method_brace_style = end_of_line
|
||||
ij_java_method_call_chain_wrap = off
|
||||
ij_java_method_parameters_new_line_after_left_paren = false
|
||||
ij_java_method_parameters_right_paren_on_new_line = false
|
||||
ij_java_method_parameters_wrap = off
|
||||
ij_java_modifier_list_wrap = false
|
||||
ij_java_multi_catch_types_wrap = normal
|
||||
ij_java_names_count_to_use_import_on_demand = 99
|
||||
ij_java_new_line_after_lparen_in_annotation = false
|
||||
ij_java_new_line_after_lparen_in_record_header = false
|
||||
ij_java_parameter_annotation_wrap = off
|
||||
ij_java_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_java_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_java_place_assignment_sign_on_next_line = false
|
||||
ij_java_prefer_longer_names = true
|
||||
ij_java_prefer_parameters_wrap = false
|
||||
ij_java_record_components_wrap = normal
|
||||
ij_java_repeat_synchronized = true
|
||||
ij_java_replace_instanceof_and_cast = false
|
||||
ij_java_replace_null_check = true
|
||||
ij_java_replace_sum_lambda_with_method_ref = true
|
||||
ij_java_resource_list_new_line_after_left_paren = false
|
||||
ij_java_resource_list_right_paren_on_new_line = false
|
||||
ij_java_resource_list_wrap = off
|
||||
ij_java_rparen_on_new_line_in_annotation = false
|
||||
ij_java_rparen_on_new_line_in_record_header = false
|
||||
ij_java_space_after_closing_angle_bracket_in_type_argument = false
|
||||
ij_java_space_after_colon = true
|
||||
ij_java_space_after_comma = true
|
||||
ij_java_space_after_comma_in_type_arguments = true
|
||||
ij_java_space_after_for_semicolon = true
|
||||
ij_java_space_after_quest = true
|
||||
ij_java_space_after_type_cast = true
|
||||
ij_java_space_before_annotation_array_initializer_left_brace = false
|
||||
ij_java_space_before_annotation_parameter_list = false
|
||||
ij_java_space_before_array_initializer_left_brace = false
|
||||
ij_java_space_before_catch_keyword = false
|
||||
ij_java_space_before_catch_left_brace = false
|
||||
ij_java_space_before_catch_parentheses = false
|
||||
ij_java_space_before_class_left_brace = false
|
||||
ij_java_space_before_colon = true
|
||||
ij_java_space_before_colon_in_foreach = true
|
||||
ij_java_space_before_comma = false
|
||||
ij_java_space_before_do_left_brace = false
|
||||
ij_java_space_before_else_keyword = false
|
||||
ij_java_space_before_else_left_brace = false
|
||||
ij_java_space_before_finally_keyword = false
|
||||
ij_java_space_before_finally_left_brace = false
|
||||
ij_java_space_before_for_left_brace = false
|
||||
ij_java_space_before_for_parentheses = false
|
||||
ij_java_space_before_for_semicolon = false
|
||||
ij_java_space_before_if_left_brace = false
|
||||
ij_java_space_before_if_parentheses = false
|
||||
ij_java_space_before_method_call_parentheses = false
|
||||
ij_java_space_before_method_left_brace = false
|
||||
ij_java_space_before_method_parentheses = false
|
||||
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
|
||||
ij_java_space_before_quest = true
|
||||
ij_java_space_before_switch_left_brace = false
|
||||
ij_java_space_before_switch_parentheses = false
|
||||
ij_java_space_before_synchronized_left_brace = false
|
||||
ij_java_space_before_synchronized_parentheses = false
|
||||
ij_java_space_before_try_left_brace = false
|
||||
ij_java_space_before_try_parentheses = false
|
||||
ij_java_space_before_type_parameter_list = false
|
||||
ij_java_space_before_while_keyword = false
|
||||
ij_java_space_before_while_left_brace = false
|
||||
ij_java_space_before_while_parentheses = false
|
||||
ij_java_space_inside_one_line_enum_braces = false
|
||||
ij_java_space_within_empty_array_initializer_braces = false
|
||||
ij_java_space_within_empty_method_call_parentheses = false
|
||||
ij_java_space_within_empty_method_parentheses = false
|
||||
ij_java_spaces_around_additive_operators = false
|
||||
ij_java_spaces_around_annotation_eq = true
|
||||
ij_java_spaces_around_assignment_operators = false
|
||||
ij_java_spaces_around_bitwise_operators = false
|
||||
ij_java_spaces_around_equality_operators = false
|
||||
ij_java_spaces_around_lambda_arrow = false
|
||||
ij_java_spaces_around_logical_operators = true
|
||||
ij_java_spaces_around_method_ref_dbl_colon = false
|
||||
ij_java_spaces_around_multiplicative_operators = false
|
||||
ij_java_spaces_around_relational_operators = false
|
||||
ij_java_spaces_around_shift_operators = false
|
||||
ij_java_spaces_around_type_bounds_in_type_parameters = true
|
||||
ij_java_spaces_around_unary_operator = false
|
||||
ij_java_spaces_within_angle_brackets = false
|
||||
ij_java_spaces_within_annotation_parentheses = false
|
||||
ij_java_spaces_within_array_initializer_braces = false
|
||||
ij_java_spaces_within_braces = false
|
||||
ij_java_spaces_within_brackets = false
|
||||
ij_java_spaces_within_cast_parentheses = false
|
||||
ij_java_spaces_within_catch_parentheses = false
|
||||
ij_java_spaces_within_for_parentheses = false
|
||||
ij_java_spaces_within_if_parentheses = false
|
||||
ij_java_spaces_within_method_call_parentheses = false
|
||||
ij_java_spaces_within_method_parentheses = false
|
||||
ij_java_spaces_within_parentheses = false
|
||||
ij_java_spaces_within_record_header = false
|
||||
ij_java_spaces_within_switch_parentheses = false
|
||||
ij_java_spaces_within_synchronized_parentheses = false
|
||||
ij_java_spaces_within_try_parentheses = false
|
||||
ij_java_spaces_within_while_parentheses = false
|
||||
ij_java_special_else_if_treatment = true
|
||||
ij_java_subclass_name_suffix = Impl
|
||||
ij_java_ternary_operation_signs_on_next_line = false
|
||||
ij_java_ternary_operation_wrap = off
|
||||
ij_java_test_name_suffix = Test
|
||||
ij_java_throws_keyword_wrap = off
|
||||
ij_java_throws_list_wrap = off
|
||||
ij_java_use_external_annotations = false
|
||||
ij_java_use_fq_class_names = false
|
||||
ij_java_use_relative_indents = false
|
||||
ij_java_use_single_class_imports = true
|
||||
ij_java_variable_annotation_wrap = off
|
||||
ij_java_visibility = public
|
||||
ij_java_while_brace_force = never
|
||||
ij_java_while_on_new_line = false
|
||||
ij_java_wrap_comments = false
|
||||
ij_java_wrap_first_method_in_call_chain = false
|
||||
ij_java_wrap_long_lines = false
|
||||
|
||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||
ij_continuation_indent_size = 4
|
||||
ij_xml_align_attributes = false
|
||||
ij_xml_align_text = false
|
||||
ij_xml_attribute_wrap = normal
|
||||
ij_xml_block_comment_add_space = false
|
||||
ij_xml_block_comment_at_first_column = true
|
||||
ij_xml_keep_blank_lines = 2
|
||||
ij_xml_keep_indents_on_empty_lines = false
|
||||
ij_xml_keep_line_breaks = false
|
||||
ij_xml_keep_line_breaks_in_text = true
|
||||
ij_xml_keep_whitespaces = false
|
||||
ij_xml_keep_whitespaces_around_cdata = preserve
|
||||
ij_xml_keep_whitespaces_inside_cdata = false
|
||||
ij_xml_line_comment_at_first_column = true
|
||||
ij_xml_space_after_tag_name = false
|
||||
ij_xml_space_around_equals_in_attribute = false
|
||||
ij_xml_space_inside_empty_tag = true
|
||||
ij_xml_text_wrap = normal
|
||||
ij_xml_use_custom_settings = true
|
||||
@@ -15,16 +15,16 @@ android {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 94
|
||||
versionName "1.2.3+fork.94"
|
||||
versionCode 97
|
||||
versionName "2.0.1+fork.97"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// minifyEnabled true
|
||||
// shrinkResources true
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug{
|
||||
@@ -32,15 +32,8 @@ android {
|
||||
versionNameSuffix '-debug'
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
githubRelease{
|
||||
initWith release
|
||||
}
|
||||
playRelease{
|
||||
initWith release
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
versionNameSuffix '-play'
|
||||
}
|
||||
githubRelease { initWith release }
|
||||
playRelease { initWith release }
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -75,7 +68,8 @@ dependencies {
|
||||
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.8'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.9'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
@@ -83,10 +77,10 @@ dependencies {
|
||||
implementation 'org.parceler:parceler-api:1.1.12'
|
||||
implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0'
|
||||
annotationProcessor 'org.parceler:parceler:1.1.12'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.4.1-alpha05'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
|
||||
androidTestImplementation 'androidx.test:core:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
|
||||
41
mastodon/proguard-rules.pro
vendored
41
mastodon/proguard-rules.pro
vendored
@@ -30,6 +30,9 @@
|
||||
*;
|
||||
}
|
||||
|
||||
# i don't know how proguard works
|
||||
-keep class org.joinmastodon.android.** { *; }
|
||||
|
||||
# Keep all enums for debugging purposes
|
||||
-keepnames public enum * {
|
||||
*;
|
||||
@@ -47,6 +50,38 @@
|
||||
-keep @org.parceler.Parcel class * { *; }
|
||||
-keep class **$$Parcelable { *; }
|
||||
|
||||
-keep class com.google.gson.reflect.TypeToken
|
||||
-keep class * extends com.google.gson.reflect.TypeToken
|
||||
-keep public class * implements java.lang.reflect.Type
|
||||
##---------------Begin: proguard configuration for Gson ----------
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-dontwarn sun.misc.**
|
||||
#-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
# Application classes that will be serialized/deserialized over Gson
|
||||
-keep class com.google.gson.examples.android.model.** { <fields>; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * extends com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Prevent R8 from leaving Data object members always null
|
||||
-keepclassmembers,allowobfuscation class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
@@ -65,6 +65,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.*;
|
||||
@LargeTest
|
||||
public class StoreScreenshotsGenerator{
|
||||
private static final String PHOTO_FILE="IMG_1010.jpg";
|
||||
private static final long LOAD_WAIT_TIMEOUT=20_000;
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<MainActivity> activityScenarioRule=new ActivityScenarioRule<>(MainActivity.class);
|
||||
@@ -84,14 +85,14 @@ public class StoreScreenshotsGenerator{
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(AccountSessionManager.getInstance().getLastActiveAccountID());
|
||||
MastodonApp.context.deleteDatabase(session.getID()+".db");
|
||||
|
||||
onView(isRoot()).perform(waitId(R.id.more, 5000));
|
||||
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
|
||||
Thread.sleep(500);
|
||||
takeScreenshot("HomeTimeline");
|
||||
|
||||
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.DARK;
|
||||
activityScenarioRule.getScenario().recreate();
|
||||
|
||||
onView(isRoot()).perform(waitId(R.id.more, 5000));
|
||||
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
|
||||
Thread.sleep(500);
|
||||
takeScreenshot("HomeTimeline_Dark");
|
||||
|
||||
@@ -100,8 +101,8 @@ public class StoreScreenshotsGenerator{
|
||||
|
||||
activityScenarioRule.getScenario().onActivity(activity->UiUtils.openProfileByID(activity, session.getID(), args.getString("profileAccountID")));
|
||||
Thread.sleep(500);
|
||||
onView(isRoot()).perform(waitId(R.id.avatar_border, 5000)); // wait for profile to load
|
||||
onView(isRoot()).perform(waitId(R.id.more, 5000)); // wait for timeline to load
|
||||
onView(isRoot()).perform(waitId(R.id.avatar_border, LOAD_WAIT_TIMEOUT)); // wait for profile to load
|
||||
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT)); // wait for timeline to load
|
||||
Thread.sleep(500);
|
||||
takeScreenshot("Profile");
|
||||
|
||||
|
||||
@@ -2,15 +2,22 @@ package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public class UiUtilsTest {
|
||||
@@ -103,4 +110,152 @@ public class UiUtilsTest {
|
||||
"somewhere.else"
|
||||
));
|
||||
}
|
||||
|
||||
private final String[] args = new String[] { "Megalodon", "♡" };
|
||||
|
||||
private String gen(String format, CharSequence... args) {
|
||||
return UiUtils.generateFormattedString(format, args).toString();
|
||||
}
|
||||
@Test
|
||||
public void generateFormattedString() {
|
||||
assertEquals(
|
||||
"ordered substitution",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("%s reacted with %s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"1 2 3 4 5",
|
||||
gen("%s %s %s %s %s", "1", "2", "3", "4", "5")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"indexed substitution",
|
||||
"with ♡ was reacted by Megalodon",
|
||||
gen("with %2$s was reacted by %1$s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"indexed substitution, in order",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("%1$s reacted with %2$s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"indexed substitution, 0-based",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("%0$s reacted with %1$s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"indexed substitution, 5 items",
|
||||
"5 4 3 2 1",
|
||||
gen("%5$s %4$s %3$s %2$s %1$s", "1", "2", "3", "4", "5")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"one argument missing",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("reacted with %s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"multiple arguments missing",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("reacted with", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"multiple arguments missing, numbers in expeced positions",
|
||||
"1 2 x 3 4 5",
|
||||
gen("%s x %s", "1", "2", "3", "4", "5")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"one leading and trailing space",
|
||||
"Megalodon reacted with ♡",
|
||||
gen(" reacted with ", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"multiple leading and trailing spaces",
|
||||
"Megalodon reacted with ♡",
|
||||
gen(" reacted with ", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"invalid format produces expected invalid result",
|
||||
"Megalodon reacted with % s ♡",
|
||||
gen("reacted with % s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"plain string as format, all arguments get added",
|
||||
"a x b c",
|
||||
gen("x", new String[] { "a", "b", "c" })
|
||||
);
|
||||
|
||||
assertEquals("empty input produces empty output", "", gen(""));
|
||||
|
||||
// not supported:
|
||||
// assertEquals("a b a", gen("%1$s %2$s %2$s %1$s", new String[] { "a", "b", "c" }));
|
||||
// assertEquals("x", gen("%s %1$s %2$s %1$s %s", new String[] { "a", "b", "c" }));
|
||||
}
|
||||
|
||||
private AccountField makeField(String name, String value) {
|
||||
AccountField f = new AccountField();
|
||||
f.name = name;
|
||||
f.value = value;
|
||||
return f;
|
||||
}
|
||||
|
||||
private Account fakeAccount(AccountField... fields) {
|
||||
Account a = new Account();
|
||||
a.fields = Arrays.asList(fields);
|
||||
return a;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractPronouns() {
|
||||
assertEquals("they", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("name and pronouns", "https://pronouns.site"),
|
||||
makeField("pronouns", "they"),
|
||||
makeField("pronouns something", "bla bla")
|
||||
)).orElseThrow());
|
||||
|
||||
assertTrue(UiUtils.extractPronouns(MastodonApp.context, fakeAccount()).isEmpty());
|
||||
|
||||
assertEquals("it/its", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns pronouns pronouns", "hi hi hi"),
|
||||
makeField("pronouns", "it/its"),
|
||||
makeField("the pro's nouns", "professional")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("she/he", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("my name is", "jeanette shork, apparently"),
|
||||
makeField("my pronouns are", "she/he")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("they/them", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "https://pronouns.cc/pronouns/they/them")
|
||||
)).orElseThrow());
|
||||
|
||||
Context german = UiUtils.getLocalizedContext(MastodonApp.context, Locale.GERMAN);
|
||||
|
||||
assertEquals("sie/ihr", UiUtils.extractPronouns(german, fakeAccount(
|
||||
makeField("pronomen lauten", "sie/ihr"),
|
||||
makeField("pronouns are", "she/her"),
|
||||
makeField("die pronomen", "stehen oben")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("er/ihm", UiUtils.extractPronouns(german, fakeAccount(
|
||||
makeField("die pronomen", "stehen unten"),
|
||||
makeField("pronomen sind", "er/ihm"),
|
||||
makeField("pronouns are", "he/him")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "-- * (asterisk) --")
|
||||
)).orElseThrow());
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.joinmastodon.android.utils;
|
||||
|
||||
import static org.joinmastodon.android.model.Filter.FilterAction.*;
|
||||
import static org.joinmastodon.android.model.Filter.FilterContext.*;
|
||||
import static org.joinmastodon.android.model.FilterAction.*;
|
||||
import static org.joinmastodon.android.model.FilterContext.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -14,8 +14,8 @@ import java.util.List;
|
||||
|
||||
public class StatusFilterPredicateTest {
|
||||
|
||||
private static final Filter hideMeFilter = new Filter(), warnMeFilter = new Filter();
|
||||
private static final List<Filter> allFilters = List.of(hideMeFilter, warnMeFilter);
|
||||
private static final LegacyFilter hideMeFilter = new LegacyFilter(), warnMeFilter = new LegacyFilter();
|
||||
private static final List<LegacyFilter> allFilters = List.of(hideMeFilter, warnMeFilter);
|
||||
|
||||
private static final Status
|
||||
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
|
||||
|
||||
@@ -100,8 +100,8 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
public void maybeCheckForUpdates(){
|
||||
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
||||
return;
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", CHECK_PERIOD);
|
||||
if(timeSinceLastCheck>=CHECK_PERIOD){
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
|
||||
if(timeSinceLastCheck>CHECK_PERIOD || forceUpdate){
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
@@ -148,7 +148,8 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
curForkNumber=Integer.parseInt(matcher.group(4));
|
||||
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
||||
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
||||
if(newVersion>curVersion || newForkNumber>curForkNumber){
|
||||
if(newVersion>curVersion || newForkNumber>curForkNumber || forceUpdate){
|
||||
forceUpdate=false;
|
||||
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
|
||||
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||
@@ -323,6 +324,15 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
getPrefs().edit().clear().apply();
|
||||
File apk=getUpdateApkFile();
|
||||
if(apk.exists())
|
||||
apk.delete();
|
||||
state=UpdateState.NO_UPDATE;
|
||||
}
|
||||
|
||||
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
android:supportsRtl="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Mastodon.AutoLightDark"
|
||||
android:largeHeap="true">
|
||||
|
||||
@@ -46,7 +45,6 @@
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
13bells.com
|
||||
1611.social
|
||||
4aem.com
|
||||
aethy.com
|
||||
adachi.party
|
||||
anime.website
|
||||
annihilation.social
|
||||
anon-kenkai.com
|
||||
@@ -9,14 +10,17 @@ bae.st
|
||||
bajax.us
|
||||
banepo.st
|
||||
baraag.net
|
||||
bassam.social
|
||||
beefyboys.win
|
||||
beepboop.ga
|
||||
berserker.town
|
||||
bikeshed.party
|
||||
boks.moe
|
||||
boymoder.biz
|
||||
brainsoap.net
|
||||
breastmilk.club
|
||||
brighteon.social
|
||||
bungle.online
|
||||
cawfee.club
|
||||
clew.lol
|
||||
clubcyberia.co
|
||||
@@ -25,8 +29,8 @@ comfyboy.club
|
||||
contrapointsfan.club
|
||||
cum.camp
|
||||
cum.salon
|
||||
cybercriminal.eu
|
||||
darknight-coffee.org
|
||||
decayable.ink
|
||||
dembased.xyz
|
||||
desupost.soy
|
||||
detroitriotcity.com
|
||||
@@ -40,7 +44,6 @@ fluf.club
|
||||
foxfam.club
|
||||
freak.university
|
||||
freeatlantis.com
|
||||
freecumextremist.com
|
||||
freedomstrike.org
|
||||
freesoftwareextremist.com
|
||||
freespeech.group
|
||||
@@ -59,7 +62,6 @@ goyim.app
|
||||
goyslop.cafe
|
||||
haeder.net
|
||||
handholding.io
|
||||
hidamari.apartments
|
||||
hitchhiker.social
|
||||
hunk.city
|
||||
iddqd.social
|
||||
@@ -75,14 +77,13 @@ leftychan.net
|
||||
lewdieheaven.com
|
||||
liberdon.com
|
||||
ligma.pro
|
||||
lizards.live
|
||||
lolicon.rocks
|
||||
lolison.top
|
||||
lovingexpressions.net
|
||||
lucasvl.nl
|
||||
mahodou.moe
|
||||
makemysarcophagus.com
|
||||
maladaptive.art
|
||||
marsey.moe
|
||||
masochi.st
|
||||
mastinator.com
|
||||
merovingian.club
|
||||
@@ -105,7 +106,6 @@ nobodyhasthe.biz
|
||||
nukem.biz
|
||||
obo.sh
|
||||
onionfarms.org
|
||||
outpoa.st
|
||||
pawlicker.com
|
||||
pawoo.net
|
||||
pedo.school
|
||||
@@ -121,6 +121,7 @@ poast.tv
|
||||
poster.place
|
||||
prospeech.space
|
||||
quodverum.com
|
||||
r18.social
|
||||
rakket.app
|
||||
rapemeat.solutions
|
||||
rdrama.cc
|
||||
@@ -132,7 +133,6 @@ schwartzwelt.xyz
|
||||
seal.cafe
|
||||
shigusegubu.club
|
||||
shitpost.cloud
|
||||
shitposter.club
|
||||
shota.house
|
||||
silliness.observer
|
||||
skinheads.eu
|
||||
@@ -149,7 +149,6 @@ sonichu.com
|
||||
spinster.xyz
|
||||
springbo.cc
|
||||
starnix.network
|
||||
stereophonic.space
|
||||
strelizia.net
|
||||
syspxl.xyz
|
||||
tastingtraffic.net
|
||||
|
||||
51
mastodon/src/main/assets/server_about_template.htm
Normal file
51
mastodon/src/main/assets/server_about_template.htm
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
body{
|
||||
background: {{colorSurface}};
|
||||
padding: 16px 16px 0 16px;
|
||||
margin: 0;
|
||||
color: {{colorOnSurface}};
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
-webkit-tap-highlight-color: {{colorPrimaryTransparent}};
|
||||
}
|
||||
a{
|
||||
text-decoration: none;
|
||||
color: {{colorPrimary}};
|
||||
}
|
||||
p, h1, h2, h3, h4, h5, h6, ul, ol{
|
||||
margin-bottom: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
h1, h2{
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
h3, h4, h5, h6{
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
b, strong{
|
||||
font-weight: 500;
|
||||
}
|
||||
ul, ol{
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
ul>li, ol>li{
|
||||
padding-inline-start: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{content}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.hootsuite.nachos;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
|
||||
public class ChipConfiguration {
|
||||
|
||||
private final int mChipHorizontalSpacing;
|
||||
private final ColorStateList mChipBackground;
|
||||
private final int mChipCornerRadius;
|
||||
private final int mChipTextColor;
|
||||
private final int mChipTextSize;
|
||||
private final int mChipHeight;
|
||||
private final int mChipVerticalSpacing;
|
||||
private final int mMaxAvailableWidth;
|
||||
|
||||
/**
|
||||
* Creates a new ChipConfiguration. You can pass in {@code -1} or {@code null} for any of the parameters to indicate that parameter should be
|
||||
* ignored.
|
||||
*
|
||||
* @param chipHorizontalSpacing the amount of horizontal space (in pixels) to put between consecutive chips
|
||||
* @param chipBackground the {@link ColorStateList} to set as the background of the chips
|
||||
* @param chipCornerRadius the corner radius of the chip background, in pixels
|
||||
* @param chipTextColor the color to set as the text color of the chips
|
||||
* @param chipTextSize the font size (in pixels) to use for the text of the chips
|
||||
* @param chipHeight the height (in pixels) of each chip
|
||||
* @param chipVerticalSpacing the amount of vertical space (in pixels) to put between chips on consecutive lines
|
||||
* @param maxAvailableWidth the maximum available with for a chip (the width of a full line of text in the text view)
|
||||
*/
|
||||
ChipConfiguration(int chipHorizontalSpacing,
|
||||
ColorStateList chipBackground,
|
||||
int chipCornerRadius,
|
||||
int chipTextColor,
|
||||
int chipTextSize,
|
||||
int chipHeight,
|
||||
int chipVerticalSpacing,
|
||||
int maxAvailableWidth) {
|
||||
mChipHorizontalSpacing = chipHorizontalSpacing;
|
||||
mChipBackground = chipBackground;
|
||||
mChipCornerRadius = chipCornerRadius;
|
||||
mChipTextColor = chipTextColor;
|
||||
mChipTextSize = chipTextSize;
|
||||
mChipHeight = chipHeight;
|
||||
mChipVerticalSpacing = chipVerticalSpacing;
|
||||
mMaxAvailableWidth = maxAvailableWidth;
|
||||
}
|
||||
|
||||
public int getChipHorizontalSpacing() {
|
||||
return mChipHorizontalSpacing;
|
||||
}
|
||||
|
||||
public ColorStateList getChipBackground() {
|
||||
return mChipBackground;
|
||||
}
|
||||
|
||||
public int getChipCornerRadius() {
|
||||
return mChipCornerRadius;
|
||||
}
|
||||
|
||||
public int getChipTextColor() {
|
||||
return mChipTextColor;
|
||||
}
|
||||
|
||||
public int getChipTextSize() {
|
||||
return mChipTextSize;
|
||||
}
|
||||
|
||||
public int getChipHeight() {
|
||||
return mChipHeight;
|
||||
}
|
||||
|
||||
public int getChipVerticalSpacing() {
|
||||
return mChipVerticalSpacing;
|
||||
}
|
||||
|
||||
public int getMaxAvailableWidth() {
|
||||
return mMaxAvailableWidth;
|
||||
}
|
||||
}
|
||||
1132
mastodon/src/main/java/com/hootsuite/nachos/NachoTextView.java
Normal file
1132
mastodon/src/main/java/com/hootsuite/nachos/NachoTextView.java
Normal file
File diff suppressed because it is too large
Load Diff
29
mastodon/src/main/java/com/hootsuite/nachos/chip/Chip.java
Normal file
29
mastodon/src/main/java/com/hootsuite/nachos/chip/Chip.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public interface Chip {
|
||||
|
||||
/**
|
||||
* @return the text represented by this Chip
|
||||
*/
|
||||
CharSequence getText();
|
||||
|
||||
/**
|
||||
* @return the data associated with this Chip or null if no data is associated with it
|
||||
*/
|
||||
@Nullable
|
||||
Object getData();
|
||||
|
||||
/**
|
||||
* @return the width of the Chip or -1 if the Chip hasn't been given the chance to calculate its width
|
||||
*/
|
||||
int getWidth();
|
||||
|
||||
/**
|
||||
* Sets the UI state.
|
||||
*
|
||||
* @param stateSet one of the state constants in {@link android.view.View}
|
||||
*/
|
||||
void setState(int[] stateSet);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
|
||||
/**
|
||||
* Interface to allow the creation and configuration of chips
|
||||
*
|
||||
* @param <C> The type of {@link Chip} that the implementation will create/configure
|
||||
*/
|
||||
public interface ChipCreator<C extends Chip> {
|
||||
|
||||
/**
|
||||
* Creates a chip from the given context and text. Use this method when creating a brand new chip from a piece of text.
|
||||
*
|
||||
* @param context the {@link Context} to use to initialize the chip
|
||||
* @param text the text the Chip should represent
|
||||
* @param data the data to associate with the Chip, or null to associate no data
|
||||
* @return the created chip
|
||||
*/
|
||||
C createChip(@NonNull Context context, @NonNull CharSequence text, @Nullable Object data);
|
||||
|
||||
/**
|
||||
* Creates a chip from the given context and existing chip. Use this method when recreating a chip from an existing one.
|
||||
*
|
||||
* @param context the {@link Context} to use to initialize the chip
|
||||
* @param existingChip the chip that the created chip should be based on
|
||||
* @return the created chip
|
||||
*/
|
||||
C createChip(@NonNull Context context, @NonNull C existingChip);
|
||||
|
||||
/**
|
||||
* Applies the given {@link ChipConfiguration} to the given {@link Chip}. Use this method to customize the appearance/behavior of a chip before
|
||||
* adding it to the text.
|
||||
*
|
||||
* @param chip the chip to configure
|
||||
* @param chipConfiguration the configuration to apply to the chip
|
||||
*/
|
||||
void configureChip(@NonNull C chip, @NonNull ChipConfiguration chipConfiguration);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
public class ChipInfo {
|
||||
|
||||
private final CharSequence mText;
|
||||
private final Object mData;
|
||||
|
||||
public ChipInfo(CharSequence text, Object data) {
|
||||
this.mText = text;
|
||||
this.mData = data;
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
public Object getData() {
|
||||
return mData;
|
||||
}
|
||||
}
|
||||
510
mastodon/src/main/java/com/hootsuite/nachos/chip/ChipSpan.java
Normal file
510
mastodon/src/main/java/com/hootsuite/nachos/chip/ChipSpan.java
Normal file
@@ -0,0 +1,510 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.style.ImageSpan;
|
||||
|
||||
import androidx.annotation.Dimension;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
/**
|
||||
* A Span that displays text and an optional icon inside of a material design chip. The chip's dimensions, colors etc. can be extensively customized
|
||||
* through the various setter methods available in this class.
|
||||
* The basic structure of the chip is the following:
|
||||
* For chips with the icon on right:
|
||||
* <pre>
|
||||
*
|
||||
* (chip vertical spacing / 2)
|
||||
* ----------------------------------------------------------
|
||||
* | |
|
||||
* (left margin) | (padding edge) text (padding between image) icon | (right margin)
|
||||
* | |
|
||||
* ----------------------------------------------------------
|
||||
* (chip vertical spacing / 2)
|
||||
*
|
||||
* </pre>
|
||||
* For chips with the icon on the left (see {@link #setShowIconOnLeft(boolean)}):
|
||||
* <pre>
|
||||
*
|
||||
* (chip vertical spacing / 2)
|
||||
* ----------------------------------------------------------
|
||||
* | |
|
||||
* (left margin) | icon (padding between image) text (padding edge) | (right margin)
|
||||
* | |
|
||||
* ----------------------------------------------------------
|
||||
* (chip vertical spacing / 2)
|
||||
* </pre>
|
||||
*/
|
||||
public class ChipSpan extends ImageSpan implements Chip {
|
||||
|
||||
private static final float SCALE_PERCENT_OF_CHIP_HEIGHT = 0.70f;
|
||||
private static final boolean ICON_ON_LEFT_DEFAULT = true;
|
||||
|
||||
private int[] mStateSet = new int[]{};
|
||||
|
||||
private String mEllipsis;
|
||||
|
||||
private ColorStateList mDefaultBackgroundColor;
|
||||
private ColorStateList mBackgroundColor;
|
||||
private int mTextColor;
|
||||
private int mCornerRadius = -1;
|
||||
private int mIconBackgroundColor;
|
||||
|
||||
private int mTextSize = -1;
|
||||
private int mPaddingEdgePx;
|
||||
private int mPaddingBetweenImagePx;
|
||||
private int mLeftMarginPx;
|
||||
private int mRightMarginPx;
|
||||
private int mMaxAvailableWidth = -1;
|
||||
|
||||
private CharSequence mText;
|
||||
private String mTextToDraw;
|
||||
|
||||
private Drawable mIcon;
|
||||
private boolean mShowIconOnLeft = ICON_ON_LEFT_DEFAULT;
|
||||
|
||||
private int mChipVerticalSpacing = 0;
|
||||
private int mChipHeight = -1;
|
||||
private int mChipWidth = -1;
|
||||
private int mIconWidth;
|
||||
|
||||
private int mCachedSize = -1;
|
||||
|
||||
private Object mData;
|
||||
|
||||
/**
|
||||
* Constructs a new ChipSpan.
|
||||
*
|
||||
* @param context a {@link Context} that will be used to retrieve default configurations from resource files
|
||||
* @param text the text for the ChipSpan to display
|
||||
* @param icon an optional icon (can be {@code null}) for the ChipSpan to display
|
||||
*/
|
||||
public ChipSpan(@NonNull Context context, @NonNull CharSequence text, @Nullable Drawable icon, Object data) {
|
||||
super(icon);
|
||||
mIcon = icon;
|
||||
mText = text;
|
||||
mTextToDraw = mText.toString();
|
||||
|
||||
mEllipsis = context.getString(R.string.chip_ellipsis);
|
||||
|
||||
mDefaultBackgroundColor = context.getColorStateList(R.color.chip_material_background);
|
||||
mBackgroundColor = mDefaultBackgroundColor;
|
||||
|
||||
mTextColor = context.getColor(R.color.chip_default_text_color);
|
||||
mIconBackgroundColor = context.getColor(R.color.chip_default_icon_background_color);
|
||||
|
||||
Resources resources = context.getResources();
|
||||
mPaddingEdgePx = resources.getDimensionPixelSize(R.dimen.chip_default_padding_edge);
|
||||
mPaddingBetweenImagePx = resources.getDimensionPixelSize(R.dimen.chip_default_padding_between_image);
|
||||
mLeftMarginPx = resources.getDimensionPixelSize(R.dimen.chip_default_left_margin);
|
||||
mRightMarginPx = resources.getDimensionPixelSize(R.dimen.chip_default_right_margin);
|
||||
|
||||
mData = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor to recreate a ChipSpan from an existing one
|
||||
*
|
||||
* @param context a {@link Context} that will be used to retrieve default configurations from resource files
|
||||
* @param chipSpan the ChipSpan to copy
|
||||
*/
|
||||
public ChipSpan(@NonNull Context context, @NonNull ChipSpan chipSpan) {
|
||||
this(context, chipSpan.getText(), chipSpan.getDrawable(), chipSpan.getData());
|
||||
|
||||
mDefaultBackgroundColor = chipSpan.mDefaultBackgroundColor;
|
||||
mTextColor = chipSpan.mTextColor;
|
||||
mIconBackgroundColor = chipSpan.mIconBackgroundColor;
|
||||
mCornerRadius = chipSpan.mCornerRadius;
|
||||
|
||||
mTextSize = chipSpan.mTextSize;
|
||||
mPaddingEdgePx = chipSpan.mPaddingEdgePx;
|
||||
mPaddingBetweenImagePx = chipSpan.mPaddingBetweenImagePx;
|
||||
mLeftMarginPx = chipSpan.mLeftMarginPx;
|
||||
mRightMarginPx = chipSpan.mRightMarginPx;
|
||||
mMaxAvailableWidth = chipSpan.mMaxAvailableWidth;
|
||||
|
||||
mShowIconOnLeft = chipSpan.mShowIconOnLeft;
|
||||
|
||||
mChipVerticalSpacing = chipSpan.mChipVerticalSpacing;
|
||||
mChipHeight = chipSpan.mChipHeight;
|
||||
|
||||
mStateSet = chipSpan.mStateSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getData() {
|
||||
return mData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of the chip. This height should not include any extra spacing (for extra vertical spacing call {@link #setChipVerticalSpacing(int)}).
|
||||
* The background of the chip will fill the full height provided here. If this method is never called, the chip will have the height of one full line
|
||||
* of text by default. If {@code -1} is passed here, the chip will revert to this default behavior.
|
||||
*
|
||||
* @param chipHeight the height to set in pixels
|
||||
*/
|
||||
public void setChipHeight(int chipHeight) {
|
||||
mChipHeight = chipHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vertical spacing to include in between chips. Half of the value set here will be placed as empty space above the chip and half the value
|
||||
* will be placed as empty space below the chip. Therefore chips on consecutive lines will have the full value as vertical space in between them.
|
||||
* This spacing is achieved by adjusting the font metrics used by the text view containing these chips; however it does not come into effect until
|
||||
* at least one chip is created. Note that vertical spacing is dependent on having a fixed chip height (set in {@link #setChipHeight(int)}). If a
|
||||
* height is not specified in that method, the value set here will be ignored.
|
||||
*
|
||||
* @param chipVerticalSpacing the vertical spacing to set in pixels
|
||||
*/
|
||||
public void setChipVerticalSpacing(int chipVerticalSpacing) {
|
||||
mChipVerticalSpacing = chipVerticalSpacing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the font size for the chip's text. If this method is never called, the chip text will have the same font size as the text in the TextView
|
||||
* containing this chip by default. If {@code -1} is passed here, the chip will revert to this default behavior.
|
||||
*
|
||||
* @param size the font size to set in pixels
|
||||
*/
|
||||
public void setTextSize(int size) {
|
||||
mTextSize = size;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for the chip's text.
|
||||
*
|
||||
* @param color the color to set (as a hexadecimal number in the form 0xAARRGGBB)
|
||||
*/
|
||||
public void setTextColor(int color) {
|
||||
mTextColor = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets where the icon (if an icon was provided in the constructor) will appear.
|
||||
*
|
||||
* @param showIconOnLeft if true, the icon will appear on the left, otherwise the icon will appear on the right
|
||||
*/
|
||||
public void setShowIconOnLeft(boolean showIconOnLeft) {
|
||||
this.mShowIconOnLeft = showIconOnLeft;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the left margin. This margin will appear as empty space (it will not share the chip's background color) to the left of the chip.
|
||||
*
|
||||
* @param leftMarginPx the left margin to set in pixels
|
||||
*/
|
||||
public void setLeftMargin(int leftMarginPx) {
|
||||
mLeftMarginPx = leftMarginPx;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the right margin. This margin will appear as empty space (it will not share the chip's background color) to the right of the chip.
|
||||
*
|
||||
* @param rightMarginPx the right margin to set in pixels
|
||||
*/
|
||||
public void setRightMargin(int rightMarginPx) {
|
||||
this.mRightMarginPx = rightMarginPx;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background color. To configure which color in the {@link ColorStateList} is shown you can call {@link #setState(int[])}.
|
||||
* Passing {@code null} here will cause the chip to revert to it's default background.
|
||||
*
|
||||
* @param backgroundColor a {@link ColorStateList} containing backgrounds for different states.
|
||||
* @see #setState(int[])
|
||||
*/
|
||||
public void setBackgroundColor(@Nullable ColorStateList backgroundColor) {
|
||||
mBackgroundColor = backgroundColor != null ? backgroundColor : mDefaultBackgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the chip background corner radius.
|
||||
*
|
||||
* @param cornerRadius The corner radius value, in pixels.
|
||||
*/
|
||||
public void setCornerRadius(@Dimension int cornerRadius) {
|
||||
mCornerRadius = cornerRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the icon background color. This is the color of the circle that gets drawn behind the icon passed to the
|
||||
* {@link #ChipSpan(Context, CharSequence, Drawable, Object)} constructor}
|
||||
*
|
||||
* @param iconBackgroundColor the icon background color to set (as a hexadecimal number in the form 0xAARRGGBB)
|
||||
*/
|
||||
public void setIconBackgroundColor(int iconBackgroundColor) {
|
||||
mIconBackgroundColor = iconBackgroundColor;
|
||||
}
|
||||
|
||||
public void setMaxAvailableWidth(int maxAvailableWidth) {
|
||||
mMaxAvailableWidth = maxAvailableWidth;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the UI state. This state will be reflected in the background color drawn for the chip.
|
||||
*
|
||||
* @param stateSet one of the state constants in {@link android.view.View}
|
||||
* @see #setBackgroundColor(ColorStateList)
|
||||
*/
|
||||
@Override
|
||||
public void setState(int[] stateSet) {
|
||||
this.mStateSet = stateSet != null ? stateSet : new int[]{};
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
// If we haven't actually calculated a chip width yet just return -1, otherwise return the chip width + margins
|
||||
return mChipWidth != -1 ? (mLeftMarginPx + mChipWidth + mRightMarginPx) : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
|
||||
boolean usingFontMetrics = (fm != null);
|
||||
|
||||
// Adjust the font metrics regardless of whether or not there is a cached size so that the text view can maintain its height
|
||||
if (usingFontMetrics) {
|
||||
adjustFontMetrics(paint, fm);
|
||||
}
|
||||
|
||||
if (mCachedSize == -1 && usingFontMetrics) {
|
||||
mIconWidth = (mIcon != null) ? calculateChipHeight(fm.top, fm.bottom) : 0;
|
||||
|
||||
int actualWidth = calculateActualWidth(paint);
|
||||
mCachedSize = actualWidth;
|
||||
|
||||
if (mMaxAvailableWidth != -1) {
|
||||
int maxAvailableWidthMinusMargins = mMaxAvailableWidth - mLeftMarginPx - mRightMarginPx;
|
||||
if (actualWidth > maxAvailableWidthMinusMargins) {
|
||||
mTextToDraw = mText + mEllipsis;
|
||||
|
||||
while ((calculateActualWidth(paint) > maxAvailableWidthMinusMargins) && mTextToDraw.length() > 0) {
|
||||
int lastCharacterIndex = mTextToDraw.length() - mEllipsis.length() - 1;
|
||||
if (lastCharacterIndex < 0) {
|
||||
break;
|
||||
}
|
||||
mTextToDraw = mTextToDraw.substring(0, lastCharacterIndex) + mEllipsis;
|
||||
}
|
||||
|
||||
// Avoid a negative width
|
||||
mChipWidth = Math.max(0, maxAvailableWidthMinusMargins);
|
||||
mCachedSize = mMaxAvailableWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mCachedSize;
|
||||
}
|
||||
|
||||
private int calculateActualWidth(Paint paint) {
|
||||
// Only change the text size if a text size was set
|
||||
if (mTextSize != -1) {
|
||||
paint.setTextSize(mTextSize);
|
||||
}
|
||||
|
||||
int totalPadding = mPaddingEdgePx;
|
||||
|
||||
// Find text width
|
||||
Rect bounds = new Rect();
|
||||
paint.getTextBounds(mTextToDraw, 0, mTextToDraw.length(), bounds);
|
||||
int textWidth = bounds.width();
|
||||
|
||||
if (mIcon != null) {
|
||||
totalPadding += mPaddingBetweenImagePx;
|
||||
} else {
|
||||
totalPadding += mPaddingEdgePx;
|
||||
}
|
||||
|
||||
mChipWidth = totalPadding + textWidth + mIconWidth;
|
||||
return getWidth();
|
||||
}
|
||||
|
||||
public void invalidateCachedSize() {
|
||||
mCachedSize = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the provided font metrics to make it seem like the font takes up {@code mChipHeight + mChipVerticalSpacing} pixels in height.
|
||||
* This effectively ensures that the TextView will have a height equal to {@code mChipHeight + mChipVerticalSpacing} + whatever padding it has set.
|
||||
* In {@link #draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)} the chip itself is drawn to that it is vertically centered with
|
||||
* {@code mChipVerticalSpacing / 2} pixels of space above and below it
|
||||
*
|
||||
* @param paint the paint whose font metrics should be adjusted
|
||||
* @param fm the font metrics object to populate through {@link Paint#getFontMetricsInt(Paint.FontMetricsInt)}
|
||||
*/
|
||||
private void adjustFontMetrics(Paint paint, Paint.FontMetricsInt fm) {
|
||||
// Only actually adjust font metrics if we have a chip height set
|
||||
if (mChipHeight != -1) {
|
||||
paint.getFontMetricsInt(fm);
|
||||
int textHeight = fm.descent - fm.ascent;
|
||||
// Break up the vertical spacing in half because half will go above the chip, half will go below the chip
|
||||
int halfSpacing = mChipVerticalSpacing / 2;
|
||||
|
||||
// Given that the text is centered vertically within the chip, the amount of space above or below the text (inbetween the text and chip)
|
||||
// is half their difference in height:
|
||||
int spaceBetweenChipAndText = (mChipHeight - textHeight) / 2;
|
||||
|
||||
int textTop = fm.top;
|
||||
int chipTop = fm.top - spaceBetweenChipAndText;
|
||||
|
||||
int textBottom = fm.bottom;
|
||||
int chipBottom = fm.bottom + spaceBetweenChipAndText;
|
||||
|
||||
// The text may have been taller to begin with so we take the most negative coordinate (highest up) to be the top of the content
|
||||
int topOfContent = Math.min(textTop, chipTop);
|
||||
// Same as above but we want the largest positive coordinate (lowest down) to be the bottom of the content
|
||||
int bottomOfContent = Math.max(textBottom, chipBottom);
|
||||
|
||||
// Shift the top up by halfSpacing and the bottom down by halfSpacing
|
||||
int topOfContentWithSpacing = topOfContent - halfSpacing;
|
||||
int bottomOfContentWithSpacing = bottomOfContent + halfSpacing;
|
||||
|
||||
// Change the font metrics so that the TextView thinks the font takes up the vertical space of a chip + spacing
|
||||
fm.ascent = topOfContentWithSpacing;
|
||||
fm.descent = bottomOfContentWithSpacing;
|
||||
fm.top = topOfContentWithSpacing;
|
||||
fm.bottom = bottomOfContentWithSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateChipHeight(int top, int bottom) {
|
||||
// If a chip height was set we can return that, otherwise calculate it from top and bottom
|
||||
return mChipHeight != -1 ? mChipHeight : bottom - top;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
|
||||
// Shift everything mLeftMarginPx to the left to create an empty space on the left (creating the margin)
|
||||
x += mLeftMarginPx;
|
||||
if (mChipHeight != -1) {
|
||||
// If we set a chip height, adjust to vertically center chip in the line
|
||||
// Adding (bottom - top) / 2 shifts the chip down so the top of it will be centered vertically
|
||||
// Subtracting (mChipHeight / 2) shifts the chip back up so that the center of it will be centered vertically (as desired)
|
||||
top += ((bottom - top) / 2) - (mChipHeight / 2);
|
||||
bottom = top + mChipHeight;
|
||||
}
|
||||
|
||||
// Perform actual drawing
|
||||
drawBackground(canvas, x, top, bottom, paint);
|
||||
drawText(canvas, x, top, bottom, paint, mTextToDraw);
|
||||
if (mIcon != null) {
|
||||
drawIcon(canvas, x, top, bottom, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBackground(Canvas canvas, float x, int top, int bottom, Paint paint) {
|
||||
int backgroundColor = mBackgroundColor.getColorForState(mStateSet, mBackgroundColor.getDefaultColor());
|
||||
paint.setColor(backgroundColor);
|
||||
int height = calculateChipHeight(top, bottom);
|
||||
RectF rect = new RectF(x, top, x + mChipWidth, bottom);
|
||||
int cornerRadius = (mCornerRadius != -1) ? mCornerRadius : height / 2;
|
||||
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint);
|
||||
paint.setColor(mTextColor);
|
||||
}
|
||||
|
||||
private void drawText(Canvas canvas, float x, int top, int bottom, Paint paint, CharSequence text) {
|
||||
if (mTextSize != -1) {
|
||||
paint.setTextSize(mTextSize);
|
||||
}
|
||||
int height = calculateChipHeight(top, bottom);
|
||||
Paint.FontMetrics fm = paint.getFontMetrics();
|
||||
|
||||
// The top value provided here is the y coordinate for the very top of the chip
|
||||
// The y coordinate we are calculating is where the baseline of the text will be drawn
|
||||
// Our objective is to have the midpoint between the top and baseline of the text be in line with the vertical center of the chip
|
||||
// First we add height / 2 which will put the baseline at the vertical center of the chip
|
||||
// Then we add half the height of the text which will lower baseline so that the midpoint is at the vertical center of the chip as desired
|
||||
float adjustedY = top + ((height / 2) + ((-fm.top - fm.bottom) / 2));
|
||||
|
||||
// The x coordinate provided here is the left-most edge of the chip
|
||||
// If there is no icon or the icon is on the right, then the text will start at the left-most edge, but indented with the edge padding, so we
|
||||
// add mPaddingEdgePx
|
||||
// If there is an icon and it's on the left, the text will start at the left-most edge, but indented by the combined width of the icon and
|
||||
// the padding between the icon and text, so we add (mIconWidth + mPaddingBetweenImagePx)
|
||||
float adjustedX = x + ((mIcon == null || !mShowIconOnLeft) ? mPaddingEdgePx : (mIconWidth + mPaddingBetweenImagePx));
|
||||
|
||||
canvas.drawText(text, 0, text.length(), adjustedX, adjustedY, paint);
|
||||
}
|
||||
|
||||
private void drawIcon(Canvas canvas, float x, int top, int bottom, Paint paint) {
|
||||
drawIconBackground(canvas, x, top, bottom, paint);
|
||||
drawIconBitmap(canvas, x, top, bottom, paint);
|
||||
}
|
||||
|
||||
private void drawIconBackground(Canvas canvas, float x, int top, int bottom, Paint paint) {
|
||||
int height = calculateChipHeight(top, bottom);
|
||||
|
||||
paint.setColor(mIconBackgroundColor);
|
||||
|
||||
// Since it's a circle the diameter is equal to the height, so the radius == diameter / 2 == height / 2
|
||||
int radius = height / 2;
|
||||
// The coordinates that get passed to drawCircle are for the center of the circle
|
||||
// x is the left edge of the chip, (x + mChipWidth) is the right edge of the chip
|
||||
// So the center of the circle is one radius distance from either the left or right edge (depending on which side the icon is being drawn on)
|
||||
float circleX = mShowIconOnLeft ? (x + radius) : (x + mChipWidth - radius);
|
||||
// The y coordinate is always just one radius distance from the top
|
||||
canvas.drawCircle(circleX, top + radius, radius, paint);
|
||||
|
||||
paint.setColor(mTextColor);
|
||||
}
|
||||
|
||||
private void drawIconBitmap(Canvas canvas, float x, int top, int bottom, Paint paint) {
|
||||
int height = calculateChipHeight(top, bottom);
|
||||
|
||||
// Create a scaled down version of the bitmap to fit within the circle (whose diameter == height)
|
||||
Bitmap iconBitmap = Bitmap.createBitmap(mIcon.getIntrinsicWidth(), mIcon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
Bitmap scaledIconBitMap = scaleDown(iconBitmap, (float) height * SCALE_PERCENT_OF_CHIP_HEIGHT, true);
|
||||
iconBitmap.recycle();
|
||||
Canvas bitmapCanvas = new Canvas(scaledIconBitMap);
|
||||
mIcon.setBounds(0, 0, bitmapCanvas.getWidth(), bitmapCanvas.getHeight());
|
||||
mIcon.draw(bitmapCanvas);
|
||||
|
||||
// We are drawing a square icon inside of a circle
|
||||
// The coordinates we pass to canvas.drawBitmap have to be for the top-left corner of the bitmap
|
||||
// The bitmap should be inset by half of (circle width - bitmap width)
|
||||
// Since it's a circle, the circle's width is equal to it's height which is equal to the chip height
|
||||
float xInsetWithinCircle = (height - bitmapCanvas.getWidth()) / 2;
|
||||
|
||||
// The icon x coordinate is going to be insetWithinCircle pixels away from the left edge of the circle
|
||||
// If the icon is on the left, the left edge of the circle is just x
|
||||
// If the icon is on the right, the left edge of the circle is x + mChipWidth - height
|
||||
float iconX = mShowIconOnLeft ? (x + xInsetWithinCircle) : (x + mChipWidth - height + xInsetWithinCircle);
|
||||
|
||||
// The y coordinate works the same way (only it's always from the top edge)
|
||||
float yInsetWithinCircle = (height - bitmapCanvas.getHeight()) / 2;
|
||||
float iconY = top + yInsetWithinCircle;
|
||||
|
||||
canvas.drawBitmap(scaledIconBitMap, iconX, iconY, paint);
|
||||
}
|
||||
|
||||
private Bitmap scaleDown(Bitmap realImage, float maxImageSize, boolean filter) {
|
||||
float ratio = Math.min(maxImageSize / realImage.getWidth(), maxImageSize / realImage.getHeight());
|
||||
int width = Math.round(ratio * realImage.getWidth());
|
||||
int height = Math.round(ratio * realImage.getHeight());
|
||||
return Bitmap.createScaledBitmap(realImage, width, height, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mText.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
|
||||
public class ChipSpanChipCreator implements ChipCreator<ChipSpan> {
|
||||
|
||||
@Override
|
||||
public ChipSpan createChip(@NonNull Context context, @NonNull CharSequence text, Object data) {
|
||||
return new ChipSpan(context, text, null, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChipSpan createChip(@NonNull Context context, @NonNull ChipSpan existingChip) {
|
||||
return new ChipSpan(context, existingChip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureChip(@NonNull ChipSpan chip, @NonNull ChipConfiguration chipConfiguration) {
|
||||
int chipHorizontalSpacing = chipConfiguration.getChipHorizontalSpacing();
|
||||
ColorStateList chipBackground = chipConfiguration.getChipBackground();
|
||||
int chipCornerRadius = chipConfiguration.getChipCornerRadius();
|
||||
int chipTextColor = chipConfiguration.getChipTextColor();
|
||||
int chipTextSize = chipConfiguration.getChipTextSize();
|
||||
int chipHeight = chipConfiguration.getChipHeight();
|
||||
int chipVerticalSpacing = chipConfiguration.getChipVerticalSpacing();
|
||||
int maxAvailableWidth = chipConfiguration.getMaxAvailableWidth();
|
||||
|
||||
if (chipHorizontalSpacing != -1) {
|
||||
chip.setLeftMargin(chipHorizontalSpacing / 2);
|
||||
chip.setRightMargin(chipHorizontalSpacing / 2);
|
||||
}
|
||||
if (chipBackground != null) {
|
||||
chip.setBackgroundColor(chipBackground);
|
||||
}
|
||||
if (chipCornerRadius != -1) {
|
||||
chip.setCornerRadius(chipCornerRadius);
|
||||
}
|
||||
if (chipTextColor != Color.TRANSPARENT) {
|
||||
chip.setTextColor(chipTextColor);
|
||||
}
|
||||
if (chipTextSize != -1) {
|
||||
chip.setTextSize(chipTextSize);
|
||||
}
|
||||
if (chipHeight != -1) {
|
||||
chip.setChipHeight(chipHeight);
|
||||
}
|
||||
if (chipVerticalSpacing != -1) {
|
||||
chip.setChipVerticalSpacing(chipVerticalSpacing);
|
||||
}
|
||||
if (maxAvailableWidth != -1) {
|
||||
chip.setMaxAvailableWidth(maxAvailableWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.hootsuite.nachos.terminator;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This interface is used to handle the management of characters that should trigger the creation of chips in a text view.
|
||||
*
|
||||
* @see ChipTokenizer
|
||||
*/
|
||||
public interface ChipTerminatorHandler {
|
||||
|
||||
/**
|
||||
* When a chip terminator character is encountered in newly inserted text, all tokens in the whole text view will be chipified
|
||||
*/
|
||||
int BEHAVIOR_CHIPIFY_ALL = 0;
|
||||
|
||||
/**
|
||||
* When a chip terminator character is encountered in newly inserted text, only the current token (that in which the chip terminator character
|
||||
* was found) will be chipified. This token may extend beyond where the chip terminator character was located.
|
||||
*/
|
||||
int BEHAVIOR_CHIPIFY_CURRENT_TOKEN = 1;
|
||||
|
||||
/**
|
||||
* When a chip terminator character is encountered in newly inserted text, only the text from the previous chip up until the chip terminator
|
||||
* character will be chipified. This may not be an entire token.
|
||||
*/
|
||||
int BEHAVIOR_CHIPIFY_TO_TERMINATOR = 2;
|
||||
|
||||
/**
|
||||
* Constant for use with {@link #setPasteBehavior(int)}. Use this if a paste should behave the same as a standard text input (the chip temrinators
|
||||
* will all behave according to their pre-determined behavior set through {@link #addChipTerminator(char, int)} or {@link #setChipTerminators(Map)}).
|
||||
*/
|
||||
int PASTE_BEHAVIOR_USE_DEFAULT = -1;
|
||||
|
||||
/**
|
||||
* Sets all the characters that will be marked as chip terminators. This will replace any previously set chip terminators.
|
||||
*
|
||||
* @param chipTerminators a map of characters to be marked as chip terminators to behaviors that describe how to respond to the characters, or null
|
||||
* to remove all chip terminators
|
||||
*/
|
||||
void setChipTerminators(@Nullable Map<Character, Integer> chipTerminators);
|
||||
|
||||
/**
|
||||
* Adds a character as a chip terminator. When the provided character is encountered in entered text, the nearby text will be chipified according
|
||||
* to the behavior provided here.
|
||||
* {@code behavior} Must be one of:
|
||||
* <ul>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_ALL}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_CURRENT_TOKEN}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_TO_TERMINATOR}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param character the character to mark as a chip terminator
|
||||
* @param behavior the behavior describing how to respond to the chip terminator
|
||||
*/
|
||||
void addChipTerminator(char character, int behavior);
|
||||
|
||||
/**
|
||||
* Customizes the way paste events are handled.
|
||||
* If one of:
|
||||
* <ul>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_ALL}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_CURRENT_TOKEN}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_TO_TERMINATOR}</li>
|
||||
* </ul>
|
||||
* is passed, all chip terminators will be handled with that behavior when a paste event occurs.
|
||||
* If {@link #PASTE_BEHAVIOR_USE_DEFAULT} is passed, whatever behavior is configured for a particular chip terminator
|
||||
* (through {@link #setChipTerminators(Map)} or {@link #addChipTerminator(char, int)} will be used for that chip terminator
|
||||
*
|
||||
* @param pasteBehavior the behavior to use on a paste event
|
||||
*/
|
||||
void setPasteBehavior(int pasteBehavior);
|
||||
|
||||
/**
|
||||
* Parses the provided text looking for characters marked as chip terminators through {@link #addChipTerminator(char, int)} and {@link #setChipTerminators(Map)}.
|
||||
* The provided {@link Editable} will be modified if chip terminators are encountered.
|
||||
*
|
||||
* @param tokenizer the {@link ChipTokenizer} to use to identify and chipify tokens in the text
|
||||
* @param text the text in which to search for chip terminators tokens to be chipped
|
||||
* @param start the index at which to begin looking for chip terminators (inclusive)
|
||||
* @param end the index at which to end looking for chip terminators (exclusive)
|
||||
* @param isPasteEvent true if this handling is for a paste event in which case the behavior set in {@link #setPasteBehavior(int)} will be used,
|
||||
* otherwise false
|
||||
* @return an non-negative integer indicating the index where the cursor (selection) should be placed once the handling is complete,
|
||||
* or a negative integer indicating that the cursor should not be moved.
|
||||
*/
|
||||
int findAndHandleChipTerminators(@NonNull ChipTokenizer tokenizer, @NonNull Editable text, int start, int end, boolean isPasteEvent);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.hootsuite.nachos.terminator;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class DefaultChipTerminatorHandler implements ChipTerminatorHandler {
|
||||
|
||||
@Nullable
|
||||
private Map<Character, Integer> mChipTerminators;
|
||||
private int mPasteBehavior = BEHAVIOR_CHIPIFY_TO_TERMINATOR;
|
||||
|
||||
@Override
|
||||
public void setChipTerminators(@Nullable Map<Character, Integer> chipTerminators) {
|
||||
mChipTerminators = chipTerminators;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChipTerminator(char character, int behavior) {
|
||||
if (mChipTerminators == null) {
|
||||
mChipTerminators = new HashMap<>();
|
||||
}
|
||||
|
||||
mChipTerminators.put(character, behavior);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPasteBehavior(int pasteBehavior) {
|
||||
mPasteBehavior = pasteBehavior;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findAndHandleChipTerminators(@NonNull ChipTokenizer tokenizer, @NonNull Editable text, int start, int end, boolean isPasteEvent) {
|
||||
// If we don't have a tokenizer or any chip terminators, there's nothing to look for
|
||||
if (mChipTerminators == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
TextIterator textIterator = new TextIterator(text, start, end);
|
||||
int selectionIndex = -1;
|
||||
|
||||
characterLoop:
|
||||
while (textIterator.hasNextCharacter()) {
|
||||
char theChar = textIterator.nextCharacter();
|
||||
if (isChipTerminator(theChar)) {
|
||||
int behavior = (isPasteEvent && mPasteBehavior != PASTE_BEHAVIOR_USE_DEFAULT) ? mPasteBehavior : mChipTerminators.get(theChar);
|
||||
int newSelection = -1;
|
||||
switch (behavior) {
|
||||
case BEHAVIOR_CHIPIFY_ALL:
|
||||
selectionIndex = handleChipifyAll(textIterator, tokenizer);
|
||||
break characterLoop;
|
||||
case BEHAVIOR_CHIPIFY_CURRENT_TOKEN:
|
||||
newSelection = handleChipifyCurrentToken(textIterator, tokenizer);
|
||||
break;
|
||||
case BEHAVIOR_CHIPIFY_TO_TERMINATOR:
|
||||
newSelection = handleChipifyToTerminator(textIterator, tokenizer);
|
||||
break;
|
||||
}
|
||||
|
||||
if (newSelection != -1) {
|
||||
selectionIndex = newSelection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectionIndex;
|
||||
}
|
||||
|
||||
private int handleChipifyAll(TextIterator textIterator, ChipTokenizer tokenizer) {
|
||||
textIterator.deleteCharacter(true);
|
||||
tokenizer.terminateAllTokens(textIterator.getText());
|
||||
return textIterator.totalLength();
|
||||
}
|
||||
|
||||
private int handleChipifyCurrentToken(TextIterator textIterator, ChipTokenizer tokenizer) {
|
||||
textIterator.deleteCharacter(true);
|
||||
Editable text = textIterator.getText();
|
||||
int index = textIterator.getIndex();
|
||||
int tokenStart = tokenizer.findTokenStart(text, index);
|
||||
int tokenEnd = tokenizer.findTokenEnd(text, index);
|
||||
if (tokenStart < tokenEnd) {
|
||||
CharSequence chippedText = tokenizer.terminateToken(text.subSequence(tokenStart, tokenEnd), null);
|
||||
textIterator.replace(tokenStart, tokenEnd, chippedText);
|
||||
return tokenStart + chippedText.length();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int handleChipifyToTerminator(TextIterator textIterator, ChipTokenizer tokenizer) {
|
||||
Editable text = textIterator.getText();
|
||||
int index = textIterator.getIndex();
|
||||
if (index > 0) {
|
||||
int tokenStart = tokenizer.findTokenStart(text, index);
|
||||
if (tokenStart < index) {
|
||||
CharSequence chippedText = tokenizer.terminateToken(text.subSequence(tokenStart, index), null);
|
||||
textIterator.replace(tokenStart, index + 1, chippedText);
|
||||
} else {
|
||||
textIterator.deleteCharacter(false);
|
||||
}
|
||||
} else {
|
||||
textIterator.deleteCharacter(false);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean isChipTerminator(char character) {
|
||||
return mChipTerminators != null && mChipTerminators.keySet().contains(character);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.hootsuite.nachos.terminator;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
public class TextIterator {
|
||||
|
||||
private Editable mText;
|
||||
private int mStart;
|
||||
private int mEnd;
|
||||
|
||||
private int mIndex;
|
||||
|
||||
public TextIterator(Editable text, int start, int end) {
|
||||
mText = text;
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
|
||||
mIndex = mStart - 1; // Subtract 1 so that the first call to nextCharacter() will return the first character
|
||||
}
|
||||
|
||||
public int totalLength() {
|
||||
return mText.length();
|
||||
}
|
||||
|
||||
public int windowLength() {
|
||||
return mEnd - mStart;
|
||||
}
|
||||
|
||||
public Editable getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
public boolean hasNextCharacter() {
|
||||
return (mIndex + 1) < mEnd;
|
||||
}
|
||||
|
||||
public char nextCharacter() {
|
||||
mIndex++;
|
||||
return mText.charAt(mIndex);
|
||||
}
|
||||
|
||||
public void deleteCharacter(boolean maintainIndex) {
|
||||
mText.replace(mIndex, mIndex + 1, "");
|
||||
if (!maintainIndex) {
|
||||
mIndex--;
|
||||
}
|
||||
mEnd--;
|
||||
}
|
||||
|
||||
public void replace(int replaceStart, int replaceEnd, CharSequence chippedText) {
|
||||
mText.replace(replaceStart, replaceEnd, chippedText);
|
||||
|
||||
// Update indexes
|
||||
int newLength = chippedText.length();
|
||||
int oldLength = replaceEnd - replaceStart;
|
||||
mIndex = replaceStart + newLength - 1;
|
||||
mEnd += newLength - oldLength;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.hootsuite.nachos.tokenizer;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
import com.hootsuite.nachos.chip.Chip;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base implementation of the {@link ChipTokenizer} interface that performs no actions and returns default values.
|
||||
* This class allows for the easy creation of a ChipTokenizer that only implements some of the methods of the interface.
|
||||
*/
|
||||
public abstract class BaseChipTokenizer implements ChipTokenizer {
|
||||
|
||||
@Override
|
||||
public void applyConfiguration(Editable text, ChipConfiguration chipConfiguration) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenStart(CharSequence charSequence, int i) {
|
||||
// Do nothing
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenEnd(CharSequence charSequence, int i) {
|
||||
// Do nothing
|
||||
return 0;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Pair<Integer, Integer>> findAllTokens(CharSequence text) {
|
||||
// Do nothing
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence terminateToken(CharSequence charSequence, @Nullable Object data) {
|
||||
// Do nothing
|
||||
return charSequence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAllTokens(Editable text) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipStart(Chip chip, Spanned text) {
|
||||
// Do nothing
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipEnd(Chip chip, Spanned text) {
|
||||
// Do nothing
|
||||
return 0;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Chip[] findAllChips(int start, int end, Spanned text) {
|
||||
return new Chip[]{};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revertChipToToken(Chip chip, Editable text) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChip(Chip chip, Editable text) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChipAndPadding(Chip chip, Editable text) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.hootsuite.nachos.tokenizer;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
import com.hootsuite.nachos.chip.Chip;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An extension of {@link android.widget.MultiAutoCompleteTextView.Tokenizer Tokenizer} that provides extra support
|
||||
* for chipification.
|
||||
* <p>
|
||||
* In the context of this interface, a token is considered to be plain (non-chipped) text. Once a token is terminated it becomes or contains a chip.
|
||||
* </p>
|
||||
* <p>
|
||||
* The CharSequences passed to the ChipTokenizer methods may contain both chipped text
|
||||
* and plain text so the tokenizer must have some method of distinguishing between the two (e.g. using a delimeter character.
|
||||
* The {@link #terminateToken(CharSequence, Object)} method is where a chip can be formed and returned to replace the plain text.
|
||||
* Whatever class the implementation deems to represent a chip, must implement the {@link Chip} interface.
|
||||
* </p>
|
||||
*
|
||||
* @see SpanChipTokenizer
|
||||
*/
|
||||
public interface ChipTokenizer {
|
||||
|
||||
/**
|
||||
* Configures this ChipTokenizer to produce chips with the provided attributes. For each of these attributes, {@code -1} or {@code null} may be
|
||||
* passed to indicate that the attribute may be ignored.
|
||||
* <p>
|
||||
* This will also apply the provided {@link ChipConfiguration} to any existing chips in the provided text.
|
||||
* </p>
|
||||
*
|
||||
* @param text the text in which to search for existing chips to apply the configuration to
|
||||
* @param chipConfiguration a {@link ChipConfiguration} containing customizations for the chips produced by this class
|
||||
*/
|
||||
void applyConfiguration(Editable text, ChipConfiguration chipConfiguration);
|
||||
|
||||
/**
|
||||
* Returns the start of the token that ends at offset
|
||||
* <code>cursor</code> within <code>text</code>.
|
||||
*/
|
||||
int findTokenStart(CharSequence text, int cursor);
|
||||
|
||||
/**
|
||||
* Returns the end of the token (minus trailing punctuation)
|
||||
* that begins at offset <code>cursor</code> within <code>text</code>.
|
||||
*/
|
||||
int findTokenEnd(CharSequence text, int cursor);
|
||||
|
||||
/**
|
||||
* Searches through {@code text} for any tokens.
|
||||
*
|
||||
* @param text the text in which to search for un-terminated tokens
|
||||
* @return a list of {@link Pair}s of the form (startIndex, endIndex) containing the locations of all
|
||||
* unterminated tokens
|
||||
*/
|
||||
@NonNull
|
||||
List<Pair<Integer, Integer>> findAllTokens(CharSequence text);
|
||||
|
||||
/**
|
||||
* Returns <code>text</code>, modified, if necessary, to ensure that
|
||||
* it ends with a token terminator (for example a space or comma).
|
||||
*/
|
||||
CharSequence terminateToken(CharSequence text, @Nullable Object data);
|
||||
|
||||
/**
|
||||
* Terminates (converts from token into chip) all unterminated tokens in the provided text.
|
||||
* This method CAN alter the provided text.
|
||||
*
|
||||
* @param text the text in which to terminate all tokens
|
||||
*/
|
||||
void terminateAllTokens(Editable text);
|
||||
|
||||
/**
|
||||
* Finds the index of the first character in {@code text} that is a part of {@code chip}
|
||||
*
|
||||
* @param chip the chip whose start should be found
|
||||
* @param text the text in which to search for the start of {@code chip}
|
||||
* @return the start index of the chip
|
||||
*/
|
||||
int findChipStart(Chip chip, Spanned text);
|
||||
|
||||
/**
|
||||
* Finds the index of the character after the last character in {@code text} that is a part of {@code chip}
|
||||
*
|
||||
* @param chip the chip whose end should be found
|
||||
* @param text the text in which to search for the end of {@code chip}
|
||||
* @return the end index of the chip
|
||||
*/
|
||||
int findChipEnd(Chip chip, Spanned text);
|
||||
|
||||
/**
|
||||
* Searches through {@code text} for any chips
|
||||
*
|
||||
* @param start index to start looking for terminated tokens (inclusive)
|
||||
* @param end index to end looking for terminated tokens (exclusive)
|
||||
* @param text the text in which to search for terminated tokens
|
||||
* @return a list of objects implementing the {@link Chip} interface to represent the terminated tokens
|
||||
*/
|
||||
@NonNull
|
||||
Chip[] findAllChips(int start, int end, Spanned text);
|
||||
|
||||
/**
|
||||
* Effectively does the opposite of {@link #terminateToken(CharSequence, Object)} by reverting the provided chip back into a token.
|
||||
* This method CAN alter the provided text.
|
||||
*
|
||||
* @param chip the chip to revert into a token
|
||||
* @param text the text in which the chip resides
|
||||
*/
|
||||
void revertChipToToken(Chip chip, Editable text);
|
||||
|
||||
/**
|
||||
* Removes a chip and any text it encompasses from {@code text}. This method CAN alter the provided text.
|
||||
*
|
||||
* @param chip the chip to remove
|
||||
* @param text the text to remove the chip from
|
||||
*/
|
||||
void deleteChip(Chip chip, Editable text);
|
||||
|
||||
/**
|
||||
* Removes a chip, any text it encompasses AND any padding text (such as spaces) that may have been inserted when the chip was created in
|
||||
* {@link #terminateToken(CharSequence, Object)} or after. This method CAN alter the provided text.
|
||||
*
|
||||
* @param chip the chip to remove
|
||||
* @param text the text to remove the chip and padding from
|
||||
*/
|
||||
void deleteChipAndPadding(Chip chip, Editable text);
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.hootsuite.nachos.tokenizer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
import com.hootsuite.nachos.chip.Chip;
|
||||
import com.hootsuite.nachos.chip.ChipCreator;
|
||||
import com.hootsuite.nachos.chip.ChipSpan;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ChipTokenizer}.
|
||||
* This implementation does the following:
|
||||
* <ul>
|
||||
* <li>Surrounds each token with a space and the Unit Separator ASCII control character (31) - See the diagram below
|
||||
* <ul>
|
||||
* <li>The spaces are included so that android keyboards can distinguish the chips as different words and provide accurate
|
||||
* autocorrect suggestions</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Replaces each token with a {@link ChipSpan} containing the same text, once the token terminates</li>
|
||||
* <li>Uses the values passed to {@link #applyConfiguration(Editable, ChipConfiguration)} to configure any ChipSpans that get created</li>
|
||||
* </ul>
|
||||
* Each terminated token will therefore look like the following (this is what will be returned from {@link #terminateToken(CharSequence, Object)}):
|
||||
* <pre>
|
||||
* -----------------------------------------------------------
|
||||
* | SpannableString |
|
||||
* | ---------------------------------------------------- |
|
||||
* | | ChipSpan | |
|
||||
* | | | |
|
||||
* | | space separator text separator space | |
|
||||
* | | | |
|
||||
* | ---------------------------------------------------- |
|
||||
* -----------------------------------------------------------
|
||||
* </pre>
|
||||
*
|
||||
* @see ChipSpan
|
||||
*/
|
||||
public class SpanChipTokenizer<C extends Chip> implements ChipTokenizer {
|
||||
|
||||
/**
|
||||
* The character used to separate chips internally is the US (Unit Separator) ASCII control character.
|
||||
* This character is used because it's untypable so we have complete control over when chips are created.
|
||||
*/
|
||||
public static final char CHIP_SPAN_SEPARATOR = 31;
|
||||
public static final char AUTOCORRECT_SEPARATOR = ' ';
|
||||
|
||||
private Context mContext;
|
||||
|
||||
@Nullable
|
||||
private ChipConfiguration mChipConfiguration;
|
||||
@NonNull
|
||||
private ChipCreator<C> mChipCreator;
|
||||
@NonNull
|
||||
private Class<C> mChipClass;
|
||||
|
||||
private Comparator<Pair<Integer, Integer>> mReverseTokenIndexesSorter = new Comparator<Pair<Integer, Integer>>() {
|
||||
@Override
|
||||
public int compare(Pair<Integer, Integer> lhs, Pair<Integer, Integer> rhs) {
|
||||
return rhs.first - lhs.first;
|
||||
}
|
||||
};
|
||||
|
||||
public SpanChipTokenizer(Context context, @NonNull ChipCreator<C> chipCreator, @NonNull Class<C> chipClass) {
|
||||
mContext = context;
|
||||
mChipCreator = chipCreator;
|
||||
mChipClass = chipClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyConfiguration(Editable text, ChipConfiguration chipConfiguration) {
|
||||
mChipConfiguration = chipConfiguration;
|
||||
|
||||
for (C chip : findAllChips(0, text.length(), text)) {
|
||||
// Recreate the chips with the new configuration
|
||||
int chipStart = findChipStart(chip, text);
|
||||
deleteChip(chip, text);
|
||||
text.insert(chipStart, terminateToken(mChipCreator.createChip(mContext, chip)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenStart(CharSequence text, int cursor) {
|
||||
int i = cursor;
|
||||
|
||||
// Work backwards until we find a CHIP_SPAN_SEPARATOR
|
||||
while (i > 0 && text.charAt(i - 1) != CHIP_SPAN_SEPARATOR) {
|
||||
i--;
|
||||
}
|
||||
// Work forwards to skip over any extra whitespace at the beginning of the token
|
||||
while (i > 0 && i < text.length() && Character.isWhitespace(text.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenEnd(CharSequence text, int cursor) {
|
||||
int i = cursor;
|
||||
int len = text.length();
|
||||
|
||||
// Work forwards till we find a CHIP_SPAN_SEPARATOR
|
||||
while (i < len) {
|
||||
if (text.charAt(i) == CHIP_SPAN_SEPARATOR) {
|
||||
return (i - 1); // subtract one because the CHIP_SPAN_SEPARATOR will be preceded by a space
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Pair<Integer, Integer>> findAllTokens(CharSequence text) {
|
||||
List<Pair<Integer, Integer>> unterminatedTokens = new ArrayList<>();
|
||||
|
||||
boolean insideChip = false;
|
||||
// Iterate backwards through the text (to avoid messing up indexes)
|
||||
for (int index = text.length() - 1; index >= 0; index--) {
|
||||
char theCharacter = text.charAt(index);
|
||||
|
||||
// Every time we hit a CHIP_SPAN_SEPARATOR character we switch from being inside to outside
|
||||
// or outside to inside a chip
|
||||
// This check must happen before the whitespace check because CHIP_SPAN_SEPARATOR is considered a whitespace character
|
||||
if (theCharacter == CHIP_SPAN_SEPARATOR) {
|
||||
insideChip = !insideChip;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Completely skip over whitespace
|
||||
if (Character.isWhitespace(theCharacter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're ever outside a chip, see if the text we're in is a viable token for chipification
|
||||
if (!insideChip) {
|
||||
int tokenStart = findTokenStart(text, index);
|
||||
int tokenEnd = findTokenEnd(text, index);
|
||||
|
||||
// Can only actually be chipified if there's at least one character between them
|
||||
if (tokenEnd - tokenStart >= 1) {
|
||||
unterminatedTokens.add(new Pair<>(tokenStart, tokenEnd));
|
||||
index = tokenStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
return unterminatedTokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence terminateToken(CharSequence text, @Nullable Object data) {
|
||||
// Remove leading/trailing whitespace
|
||||
CharSequence trimmedText = text.toString().trim();
|
||||
return terminateToken(mChipCreator.createChip(mContext, trimmedText, data));
|
||||
}
|
||||
|
||||
private CharSequence terminateToken(C chip) {
|
||||
// Surround the text with CHIP_SPAN_SEPARATOR and spaces
|
||||
// The spaces allow autocorrect to correctly identify words
|
||||
String chipSeparator = Character.toString(CHIP_SPAN_SEPARATOR);
|
||||
String autoCorrectSeparator = Character.toString(AUTOCORRECT_SEPARATOR);
|
||||
CharSequence textWithSeparator = autoCorrectSeparator + chipSeparator + chip.getText() + chipSeparator + autoCorrectSeparator;
|
||||
|
||||
// Build the container object to house the ChipSpan and space
|
||||
SpannableString spannableString = new SpannableString(textWithSeparator);
|
||||
|
||||
// Attach the ChipSpan
|
||||
if (mChipConfiguration != null) {
|
||||
mChipCreator.configureChip(chip, mChipConfiguration);
|
||||
}
|
||||
spannableString.setSpan(chip, 0, textWithSeparator.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
return spannableString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAllTokens(Editable text) {
|
||||
List<Pair<Integer, Integer>> unterminatedTokens = findAllTokens(text);
|
||||
// Sort in reverse order (so index changes don't affect anything)
|
||||
Collections.sort(unterminatedTokens, mReverseTokenIndexesSorter);
|
||||
for (Pair<Integer, Integer> indexes : unterminatedTokens) {
|
||||
int start = indexes.first;
|
||||
int end = indexes.second;
|
||||
CharSequence textToChip = text.subSequence(start, end);
|
||||
CharSequence chippedText = terminateToken(textToChip, null);
|
||||
text.replace(start, end, chippedText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipStart(Chip chip, Spanned text) {
|
||||
return text.getSpanStart(chip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipEnd(Chip chip, Spanned text) {
|
||||
return text.getSpanEnd(chip);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NonNull
|
||||
@Override
|
||||
public C[] findAllChips(int start, int end, Spanned text) {
|
||||
C[] spansArray = text.getSpans(start, end, mChipClass);
|
||||
return (spansArray != null) ? spansArray : (C[]) Array.newInstance(mChipClass, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revertChipToToken(Chip chip, Editable text) {
|
||||
int chipStart = findChipStart(chip, text);
|
||||
int chipEnd = findChipEnd(chip, text);
|
||||
text.removeSpan(chip);
|
||||
text.replace(chipStart, chipEnd, chip.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChip(Chip chip, Editable text) {
|
||||
int chipStart = findChipStart(chip, text);
|
||||
int chipEnd = findChipEnd(chip, text);
|
||||
text.removeSpan(chip);
|
||||
// On the emulator for some reason the text automatically gets deleted and chipStart and chipEnd end up both being -1, so in that case we
|
||||
// don't need to call text.delete(...)
|
||||
if (chipStart != chipEnd) {
|
||||
text.delete(chipStart, chipEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChipAndPadding(Chip chip, Editable text) {
|
||||
// This implementation does not add any extra padding outside of the span so we can just delete the chip normally
|
||||
deleteChip(chip, text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.hootsuite.nachos.validator;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link NachoValidator} that deems text to be invalid if it contains
|
||||
* unterminated tokens and fixes the text by chipifying all the unterminated tokens.
|
||||
*/
|
||||
public class ChipifyingNachoValidator implements NachoValidator {
|
||||
|
||||
@Override
|
||||
public boolean isValid(@NonNull ChipTokenizer chipTokenizer, CharSequence text) {
|
||||
|
||||
// The text is considered valid if there are no unterminated tokens (everything is a chip)
|
||||
List<Pair<Integer, Integer>> unterminatedTokens = chipTokenizer.findAllTokens(text);
|
||||
return unterminatedTokens.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence fixText(@NonNull ChipTokenizer chipTokenizer, CharSequence invalidText) {
|
||||
SpannableStringBuilder newText = new SpannableStringBuilder(invalidText);
|
||||
chipTokenizer.terminateAllTokens(newText);
|
||||
return newText;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.hootsuite.nachos.validator;
|
||||
|
||||
public interface IllegalCharacterIdentifier {
|
||||
boolean isCharacterIllegal(Character c);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.hootsuite.nachos.validator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
/**
|
||||
* Interface used to ensure that a given CharSequence complies to a particular format.
|
||||
*/
|
||||
public interface NachoValidator {
|
||||
|
||||
/**
|
||||
* Validates the specified text.
|
||||
*
|
||||
* @return true If the text currently in the text editor is valid.
|
||||
* @see #fixText(ChipTokenizer, CharSequence)
|
||||
*/
|
||||
boolean isValid(@NonNull ChipTokenizer chipTokenizer, CharSequence text);
|
||||
|
||||
/**
|
||||
* Corrects the specified text to make it valid.
|
||||
*
|
||||
* @param invalidText A string that doesn't pass validation: isValid(invalidText)
|
||||
* returns false
|
||||
* @return A string based on invalidText such as invoking isValid() on it returns true.
|
||||
* @see #isValid(ChipTokenizer, CharSequence)
|
||||
*/
|
||||
CharSequence fixText(@NonNull ChipTokenizer chipTokenizer, CharSequence invalidText);
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -57,6 +56,7 @@ public class AudioPlayerService extends Service{
|
||||
private static HashSet<Callback> callbacks=new HashSet<>();
|
||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged;
|
||||
private boolean resumeAfterAudioFocusGain;
|
||||
private boolean isBuffering=true;
|
||||
|
||||
private BroadcastReceiver receiver=new BroadcastReceiver(){
|
||||
@Override
|
||||
@@ -176,6 +176,7 @@ public class AudioPlayerService extends Service{
|
||||
player.setOnErrorListener(this::onPlayerError);
|
||||
player.setOnCompletionListener(this::onPlayerCompletion);
|
||||
player.setOnSeekCompleteListener(this::onPlayerSeekCompleted);
|
||||
player.setOnInfoListener(this::onPlayerInfo);
|
||||
try{
|
||||
player.setDataSource(this, Uri.parse(attachment.url));
|
||||
player.prepareAsync();
|
||||
@@ -187,7 +188,9 @@ public class AudioPlayerService extends Service{
|
||||
}
|
||||
|
||||
private void onPlayerPrepared(MediaPlayer mp){
|
||||
Log.i(TAG, "onPlayerPrepared");
|
||||
playerReady=true;
|
||||
isBuffering=false;
|
||||
player.start();
|
||||
updateSessionState(false);
|
||||
}
|
||||
@@ -205,6 +208,21 @@ public class AudioPlayerService extends Service{
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
private boolean onPlayerInfo(MediaPlayer mp, int what, int extra){
|
||||
switch(what){
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
|
||||
isBuffering=true;
|
||||
updateSessionState(false);
|
||||
}
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
|
||||
isBuffering=false;
|
||||
updateSessionState(false);
|
||||
}
|
||||
default -> Log.i(TAG, "onPlayerInfo() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onAudioFocusChanged(int change){
|
||||
switch(change){
|
||||
case AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
@@ -212,7 +230,7 @@ public class AudioPlayerService extends Service{
|
||||
pause(false);
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
resumeAfterAudioFocusGain=true;
|
||||
resumeAfterAudioFocusGain=isPlaying();
|
||||
pause(false);
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
@@ -232,12 +250,16 @@ public class AudioPlayerService extends Service{
|
||||
|
||||
private void updateSessionState(boolean removeNotification){
|
||||
session.setPlaybackState(new PlaybackState.Builder()
|
||||
.setState(player.isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, player.getCurrentPosition(), 1f)
|
||||
.setState(switch(getPlayState()){
|
||||
case PLAYING -> PlaybackState.STATE_PLAYING;
|
||||
case PAUSED -> PlaybackState.STATE_PAUSED;
|
||||
case BUFFERING -> PlaybackState.STATE_BUFFERING;
|
||||
}, player.getCurrentPosition(), 1f)
|
||||
.setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO)
|
||||
.build());
|
||||
updateNotification(!player.isPlaying(), removeNotification);
|
||||
for(Callback cb:callbacks)
|
||||
cb.onPlayStateChanged(attachment.id, player.isPlaying(), player.getCurrentPosition());
|
||||
cb.onPlayStateChanged(attachment.id, getPlayState(), player.getCurrentPosition());
|
||||
}
|
||||
|
||||
private void updateNotification(boolean dismissable, boolean removeNotification){
|
||||
@@ -310,6 +332,12 @@ public class AudioPlayerService extends Service{
|
||||
return attachment.id;
|
||||
}
|
||||
|
||||
public PlayState getPlayState(){
|
||||
if(isBuffering)
|
||||
return PlayState.BUFFERING;
|
||||
return player.isPlaying() ? PlayState.PLAYING : PlayState.PAUSED;
|
||||
}
|
||||
|
||||
public static void registerCallback(Callback cb){
|
||||
callbacks.add(cb);
|
||||
}
|
||||
@@ -333,7 +361,13 @@ public class AudioPlayerService extends Service{
|
||||
}
|
||||
|
||||
public interface Callback{
|
||||
void onPlayStateChanged(String attachmentID, boolean playing, int position);
|
||||
void onPlayStateChanged(String attachmentID, PlayState state, int position);
|
||||
void onPlaybackStopped(String attachmentID);
|
||||
}
|
||||
|
||||
public enum PlayState{
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
BUFFERING
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,136 +4,119 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class GlobalUserPreferences{
|
||||
private static final String TAG="GlobalUserPreferences";
|
||||
|
||||
public static boolean playGifs;
|
||||
public static boolean useCustomTabs;
|
||||
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
|
||||
public static ThemePreference theme;
|
||||
|
||||
// MEGALODON
|
||||
public static boolean trueBlackTheme;
|
||||
public static boolean showReplies;
|
||||
public static boolean showBoosts;
|
||||
public static boolean loadNewPosts;
|
||||
public static boolean showNewPostsButton;
|
||||
public static boolean showInteractionCounts;
|
||||
public static boolean alwaysExpandContentWarnings;
|
||||
public static boolean disableMarquee;
|
||||
public static boolean toolbarMarquee;
|
||||
public static boolean disableSwipe;
|
||||
public static boolean voteButtonForSingleChoice;
|
||||
public static boolean enableDeleteNotifications;
|
||||
public static boolean translateButtonOpenedOnly;
|
||||
public static boolean uniformNotificationIcon;
|
||||
public static boolean reduceMotion;
|
||||
public static boolean keepOnlyLatestNotification;
|
||||
public static boolean disableAltTextReminder;
|
||||
public static boolean showAltIndicator;
|
||||
public static boolean showNoAltIndicator;
|
||||
public static boolean enablePreReleases;
|
||||
public static PrefixRepliesMode prefixReplies;
|
||||
public static boolean bottomEncoding;
|
||||
public static boolean collapseLongPosts;
|
||||
public static boolean spectatorMode;
|
||||
public static boolean autoHideFab;
|
||||
public static boolean replyLineAboveHeader;
|
||||
public static boolean compactReblogReplyLine;
|
||||
public static boolean confirmBeforeReblog;
|
||||
public static boolean allowRemoteLoading;
|
||||
public static boolean forwardReportDefault;
|
||||
public static AutoRevealMode autoRevealEqualSpoilers;
|
||||
public static String publishButtonText;
|
||||
public static ThemePreference theme;
|
||||
public static ColorPreference color;
|
||||
|
||||
public static Map<String, List<String>> recentLanguages;
|
||||
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
|
||||
public static Set<String> accountsWithLocalOnlySupport;
|
||||
public static Set<String> accountsInGlitchMode;
|
||||
public static Set<String> accountsWithContentTypesEnabled;
|
||||
public static Map<String, ContentType> accountsDefaultContentTypes;
|
||||
|
||||
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
|
||||
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
|
||||
private final static Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
|
||||
|
||||
/**
|
||||
* Pleroma
|
||||
*/
|
||||
public static String replyVisibility;
|
||||
public static boolean disableM3PillActiveIndicator;
|
||||
public static boolean showNavigationLabels;
|
||||
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private static <T> T fromJson(String json, Type type, T orElse) {
|
||||
if (json == null) return orElse;
|
||||
try { return gson.fromJson(json, type); }
|
||||
catch (JsonSyntaxException ignored) { return orElse; }
|
||||
public static <T> T fromJson(String json, Type type, T orElse){
|
||||
if(json==null) return orElse;
|
||||
try{
|
||||
T value=gson.fromJson(json, type);
|
||||
return value==null ? orElse : value;
|
||||
}catch(JsonSyntaxException ignored){
|
||||
return orElse;
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeAccount(String accountId) {
|
||||
recentLanguages.remove(accountId);
|
||||
pinnedTimelines.remove(accountId);
|
||||
accountsInGlitchMode.remove(accountId);
|
||||
accountsWithLocalOnlySupport.remove(accountId);
|
||||
accountsWithContentTypesEnabled.remove(accountId);
|
||||
accountsDefaultContentTypes.remove(accountId);
|
||||
save();
|
||||
public static <T extends Enum<T>> T enumValue(Class<T> enumType, String name) {
|
||||
try { return Enum.valueOf(enumType, name); }
|
||||
catch (NullPointerException npe) { return null; }
|
||||
}
|
||||
|
||||
public static void load(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
altTextReminders=prefs.getBoolean("altTextReminders", true);
|
||||
confirmUnfollow=prefs.getBoolean("confirmUnfollow", true);
|
||||
confirmBoost=prefs.getBoolean("confirmBoost", false);
|
||||
confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
|
||||
|
||||
// MEGALODON
|
||||
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
|
||||
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true);
|
||||
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
|
||||
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
||||
disableMarquee=prefs.getBoolean("disableMarquee", false);
|
||||
toolbarMarquee=prefs.getBoolean("toolbarMarquee", true);
|
||||
disableSwipe=prefs.getBoolean("disableSwipe", false);
|
||||
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
|
||||
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
|
||||
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
|
||||
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
||||
reduceMotion=prefs.getBoolean("reduceMotion", false);
|
||||
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||
disableAltTextReminder=prefs.getBoolean("disableAltTextReminder", false);
|
||||
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
|
||||
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
|
||||
enablePreReleases=prefs.getBoolean("enablePreReleases", false);
|
||||
prefixReplies=PrefixRepliesMode.valueOf(prefs.getString("prefixReplies", PrefixRepliesMode.NEVER.name()));
|
||||
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
|
||||
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
|
||||
spectatorMode=prefs.getBoolean("spectatorMode", false);
|
||||
autoHideFab=prefs.getBoolean("autoHideFab", true);
|
||||
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
|
||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
||||
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
|
||||
publishButtonText=prefs.getString("publishButtonText", "");
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
|
||||
pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>());
|
||||
accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
|
||||
accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
|
||||
replyVisibility=prefs.getString("replyVisibility", null);
|
||||
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
|
||||
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
|
||||
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
||||
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
|
||||
disableM3PillActiveIndicator=prefs.getBoolean("disableM3PillActiveIndicator", false);
|
||||
showNavigationLabels=prefs.getBoolean("showNavigationLabels", true);
|
||||
displayPronounsInTimelines=prefs.getBoolean("displayPronounsInTimelines", true);
|
||||
displayPronounsInThreads=prefs.getBoolean("displayPronounsInThreads", true);
|
||||
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
|
||||
|
||||
if (prefs.contains("prefixRepliesWithRe")) {
|
||||
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
|
||||
@@ -150,27 +133,30 @@ public class GlobalUserPreferences{
|
||||
// invalid color name or color was previously saved as integer
|
||||
color=ColorPreference.PINK;
|
||||
}
|
||||
|
||||
if(prefs.getInt("migrationLevel", 0) < 61) migrateToUpstreamVersion61();
|
||||
}
|
||||
|
||||
public static void save(){
|
||||
getPrefs().edit()
|
||||
.putBoolean("playGifs", playGifs)
|
||||
.putBoolean("useCustomTabs", useCustomTabs)
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.putBoolean("altTextReminders", altTextReminders)
|
||||
.putBoolean("confirmUnfollow", confirmUnfollow)
|
||||
.putBoolean("confirmBoost", confirmBoost)
|
||||
.putBoolean("confirmDeletePost", confirmDeletePost)
|
||||
|
||||
// MEGALODON
|
||||
.putBoolean("loadNewPosts", loadNewPosts)
|
||||
.putBoolean("showNewPostsButton", showNewPostsButton)
|
||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||
.putBoolean("showInteractionCounts", showInteractionCounts)
|
||||
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
|
||||
.putBoolean("disableMarquee", disableMarquee)
|
||||
.putBoolean("toolbarMarquee", toolbarMarquee)
|
||||
.putBoolean("disableSwipe", disableSwipe)
|
||||
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
|
||||
.putBoolean("translateButtonOpenedOnly", translateButtonOpenedOnly)
|
||||
.putBoolean("uniformNotificationIcon", uniformNotificationIcon)
|
||||
.putBoolean("reduceMotion", reduceMotion)
|
||||
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
||||
.putBoolean("disableAltTextReminder", disableAltTextReminder)
|
||||
.putBoolean("showAltIndicator", showAltIndicator)
|
||||
.putBoolean("showNoAltIndicator", showNoAltIndicator)
|
||||
.putBoolean("enablePreReleases", enablePreReleases)
|
||||
@@ -179,25 +165,68 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("spectatorMode", spectatorMode)
|
||||
.putBoolean("autoHideFab", autoHideFab)
|
||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
||||
.putString("publishButtonText", publishButtonText)
|
||||
.putBoolean("bottomEncoding", bottomEncoding)
|
||||
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
|
||||
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.putString("color", color.name())
|
||||
.putString("recentLanguages", gson.toJson(recentLanguages))
|
||||
.putString("pinnedTimelines", gson.toJson(pinnedTimelines))
|
||||
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
|
||||
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
|
||||
.putString("replyVisibility", replyVisibility)
|
||||
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
|
||||
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
|
||||
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
||||
.putBoolean("forwardReportDefault", forwardReportDefault)
|
||||
.putBoolean("disableM3PillActiveIndicator", disableM3PillActiveIndicator)
|
||||
.putBoolean("showNavigationLabels", showNavigationLabels)
|
||||
.putBoolean("displayPronounsInTimelines", displayPronounsInTimelines)
|
||||
.putBoolean("displayPronounsInThreads", displayPronounsInThreads)
|
||||
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static void migrateToUpstreamVersion61(){
|
||||
Log.d(TAG, "Migrating preferences to upstream version 61!!");
|
||||
|
||||
Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
|
||||
Type pinnedTimelinesType = new TypeToken<Map<String, ArrayList<TimelineDefinition>>>() {}.getType();
|
||||
Type recentLanguagesType = new TypeToken<Map<String, ArrayList<String>>>() {}.getType();
|
||||
|
||||
// migrate global preferences
|
||||
SharedPreferences prefs=getPrefs();
|
||||
altTextReminders=!prefs.getBoolean("disableAltTextReminder", false);
|
||||
confirmBoost=prefs.getBoolean("confirmBeforeReblog", false);
|
||||
toolbarMarquee=!prefs.getBoolean("disableMarquee", false);
|
||||
|
||||
save();
|
||||
|
||||
// migrate local preferences
|
||||
AccountSessionManager asm=AccountSessionManager.getInstance();
|
||||
// reset: Set<String> accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
|
||||
Map<String, ContentType> accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
|
||||
Map<String, ArrayList<TimelineDefinition>> pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>());
|
||||
Set<String> accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
|
||||
Set<String> accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
|
||||
Map<String, ArrayList<String>> recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
|
||||
|
||||
for(AccountSession session : asm.getLoggedInAccounts()){
|
||||
String accountID=session.getID();
|
||||
AccountLocalPreferences localPrefs=session.getLocalPreferences();
|
||||
localPrefs.revealCWs=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
||||
localPrefs.recentLanguages=recentLanguages.get(accountID);
|
||||
// reset: localPrefs.contentTypesEnabled=accountsWithContentTypesEnabled.contains(accountID);
|
||||
localPrefs.defaultContentType=accountsDefaultContentTypes.getOrDefault(accountID, ContentType.PLAIN);
|
||||
localPrefs.showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
|
||||
localPrefs.timelines=pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
|
||||
localPrefs.localOnlySupported=accountsWithLocalOnlySupport.contains(accountID);
|
||||
localPrefs.glitchInstance=accountsInGlitchMode.contains(accountID);
|
||||
localPrefs.publishButtonText=prefs.getString("publishButtonText", null);
|
||||
localPrefs.keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||
localPrefs.showReplies=prefs.getBoolean("showReplies", true);
|
||||
localPrefs.showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
|
||||
if(session.getInstance().map(Instance::isAkkoma).orElse(false)){
|
||||
localPrefs.timelineReplyVisibility=prefs.getString("replyVisibility", null);
|
||||
}
|
||||
|
||||
localPrefs.save();
|
||||
}
|
||||
|
||||
prefs.edit().putInt("migrationLevel", 61).apply();
|
||||
}
|
||||
|
||||
public enum ColorPreference{
|
||||
MATERIAL3,
|
||||
PINK,
|
||||
@@ -206,7 +235,20 @@ public class GlobalUserPreferences{
|
||||
BLUE,
|
||||
BROWN,
|
||||
RED,
|
||||
YELLOW
|
||||
YELLOW;
|
||||
|
||||
public @StringRes int getName() {
|
||||
return switch(this){
|
||||
case MATERIAL3 -> R.string.sk_color_palette_material3;
|
||||
case PINK -> R.string.sk_color_palette_pink;
|
||||
case PURPLE -> R.string.sk_color_palette_purple;
|
||||
case GREEN -> R.string.sk_color_palette_green;
|
||||
case BLUE -> R.string.sk_color_palette_blue;
|
||||
case BROWN -> R.string.sk_color_palette_brown;
|
||||
case RED -> R.string.sk_color_palette_red;
|
||||
case YELLOW -> R.string.sk_color_palette_yellow;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum ThemePreference{
|
||||
|
||||
@@ -5,13 +5,16 @@ import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
@@ -21,6 +24,7 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
@@ -28,6 +32,9 @@ import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
||||
@Override
|
||||
@@ -73,6 +80,8 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
||||
showFragmentForNotification(notification, session.getID());
|
||||
} else if (intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
} else {
|
||||
showFragmentClearingBackStack(fragment);
|
||||
maybeRequestNotificationsPermission();
|
||||
@@ -111,11 +120,55 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
||||
}
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
|
||||
}*/
|
||||
}
|
||||
|
||||
public void handleURL(Uri uri, String accountID){
|
||||
if(uri==null)
|
||||
return;
|
||||
if(!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme()))
|
||||
return;
|
||||
AccountSession session;
|
||||
if(accountID==null)
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
else
|
||||
session=AccountSessionManager.get(accountID);
|
||||
if(session==null || !session.activated)
|
||||
return;
|
||||
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
|
||||
}
|
||||
|
||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
|
||||
new GetSearchResults(q, null, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
if(result.statuses!=null && !result.statuses.isEmpty()){
|
||||
args.putParcelable("status", Parcels.wrap(result.statuses.get(0)));
|
||||
Nav.go(MainActivity.this, ThreadFragment.class, args);
|
||||
}else if(result.accounts!=null && !result.accounts.isEmpty()){
|
||||
args.putParcelable("profileAccount", Parcels.wrap(result.accounts.get(0)));
|
||||
Nav.go(MainActivity.this, ProfileFragment.class, args);
|
||||
}else{
|
||||
Toast.makeText(MainActivity.this, fromSearch ? R.string.no_search_results : R.string.link_not_supported, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(MainActivity.this);
|
||||
}
|
||||
})
|
||||
.wrapProgress(this, progressText, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void showFragmentForNotification(Notification notification, String accountID){
|
||||
try{
|
||||
notification.postprocess();
|
||||
@@ -123,7 +176,9 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
||||
Log.w("MainActivity", x);
|
||||
return;
|
||||
}
|
||||
UiUtils.showFragmentForNotification(this, notification, accountID, null);
|
||||
Bundle args = new Bundle();
|
||||
args.putBoolean("noTransition", true);
|
||||
UiUtils.showFragmentForNotification(this, notification, accountID, args);
|
||||
}
|
||||
|
||||
private void showFragmentForExternalShare(Bundle args) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
|
||||
@@ -28,5 +29,8 @@ public class MastodonApp extends Application{
|
||||
|
||||
PushSubscriptionManager.tryRegisterFCM();
|
||||
GlobalUserPreferences.load();
|
||||
if(BuildConfig.DEBUG){
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.NotificationReceivedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.NotificationAction;
|
||||
@@ -38,7 +37,9 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -57,6 +58,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
|
||||
private static final int SUMMARY_ID = 791;
|
||||
private static int notificationId = 0;
|
||||
private static Map<String, Integer> notificationIdsForAccounts = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
@@ -88,9 +90,12 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
|
||||
return;
|
||||
}
|
||||
if(account.getLocalPreferences().getNotificationsPauseEndTime()>System.currentTimeMillis()){
|
||||
Log.i(TAG, "onReceive: dropping notification because user has paused notifications for this account");
|
||||
return;
|
||||
}
|
||||
String accountID=account.getID();
|
||||
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
||||
E.post(new NotificationReceivedEvent(accountID, pn.notificationId+""));
|
||||
new GetNotificationByID(pn.notificationId+"")
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -145,7 +150,8 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
|
||||
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
|
||||
NotificationManager nm=context.getSystemService(NotificationManager.class);
|
||||
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||
AccountSession session=AccountSessionManager.get(accountID);
|
||||
Account self=session.self;
|
||||
String accountName="@"+self.username+"@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
Notification.Builder builder;
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
||||
@@ -215,7 +221,21 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
builder.setSubText(accountName);
|
||||
}
|
||||
|
||||
int id = GlobalUserPreferences.keepOnlyLatestNotification ? NOTIFICATION_ID : notificationId++;
|
||||
int id;
|
||||
if(session.getLocalPreferences().keepOnlyLatestNotification){
|
||||
if(notificationIdsForAccounts.containsKey(accountID)){
|
||||
// we overwrite the existing notification
|
||||
id=notificationIdsForAccounts.get(accountID);
|
||||
}else{
|
||||
// there's no existing notification, so we increment
|
||||
id=notificationId++;
|
||||
// and store the notification id for this account
|
||||
notificationIdsForAccounts.put(accountID, id);
|
||||
}
|
||||
}else{
|
||||
// we don't want to overwrite anything, therefore incrementing
|
||||
id=notificationId++;
|
||||
}
|
||||
|
||||
if (notification != null){
|
||||
switch (pn.notificationType){
|
||||
|
||||
@@ -13,22 +13,20 @@ import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -43,6 +41,8 @@ public class CacheController{
|
||||
private final String accountID;
|
||||
private DatabaseHelper db;
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private boolean loadingNotifications;
|
||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
|
||||
@@ -58,7 +58,6 @@ public class CacheController{
|
||||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
@@ -66,18 +65,16 @@ public class CacheController{
|
||||
ArrayList<Status> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
String newMaxID;
|
||||
outer:
|
||||
do{
|
||||
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
||||
status.postprocess();
|
||||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
if (!new StatusFilterPredicate(filters, Filter.FilterContext.HOME).test(status))
|
||||
continue outer;
|
||||
result.add(status);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||
return;
|
||||
}
|
||||
@@ -85,11 +82,13 @@ public class CacheController{
|
||||
Log.w(TAG, "getHomeTimeline: corrupted status object in database", x);
|
||||
}
|
||||
}
|
||||
new GetHomeTimeline(maxID, null, count, null)
|
||||
new GetHomeTimeline(maxID, null, count, null, AccountSessionManager.get(accountID).getLocalPreferences().timelineReplyVisibility)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters, Filter.FilterContext.HOME)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
ArrayList<Status> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
putHomeTimeline(result, maxID==null);
|
||||
}
|
||||
|
||||
@@ -126,12 +125,39 @@ public class CacheController{
|
||||
});
|
||||
}
|
||||
|
||||
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<CacheablePaginatedResponse<List<Notification>>> callback){
|
||||
public void updateStatus(Status status) {
|
||||
runOnDbThread((db)->{
|
||||
ContentValues statusUpdate=new ContentValues(1);
|
||||
statusUpdate.put("json", MastodonAPIController.gson.toJson(status));
|
||||
db.update("home_timeline", statusUpdate, "id = ?", new String[] { status.id });
|
||||
});
|
||||
}
|
||||
|
||||
public void updateNotification(Notification notification) {
|
||||
runOnDbThread((db)->{
|
||||
ContentValues notificationUpdate=new ContentValues(1);
|
||||
notificationUpdate.put("json", MastodonAPIController.gson.toJson(notification));
|
||||
String[] notificationArgs = new String[] { notification.id };
|
||||
db.update("notifications_all", notificationUpdate, "id = ?", notificationArgs);
|
||||
db.update("notifications_mentions", notificationUpdate, "id = ?", notificationArgs);
|
||||
db.update("notifications_posts", notificationUpdate, "id = ?", notificationArgs);
|
||||
|
||||
ContentValues statusUpdate=new ContentValues(1);
|
||||
statusUpdate.put("json", MastodonAPIController.gson.toJson(notification.status));
|
||||
db.update("home_timeline", statusUpdate, "id = ?", new String[] { notification.status.id });
|
||||
});
|
||||
}
|
||||
|
||||
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
|
||||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
AccountSession accountSession=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
List<Filter> filters=accountSession.wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
||||
if(!onlyMentions && !onlyPosts && loadingNotifications){
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
pendingNotificationsCallbacks.add(callback);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
@@ -140,42 +166,56 @@ public class CacheController{
|
||||
ArrayList<Notification> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
String newMaxID;
|
||||
outer:
|
||||
do{
|
||||
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
|
||||
ntf.postprocess();
|
||||
newMaxID=ntf.id;
|
||||
if(ntf.status!=null){
|
||||
if (!new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
result.add(ntf);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
|
||||
return;
|
||||
}
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
||||
}
|
||||
}
|
||||
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
|
||||
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma())
|
||||
if(!onlyMentions && !onlyPosts)
|
||||
loadingNotifications=true;
|
||||
boolean isAkkoma = AccountSessionManager.get(accountID).getInstance().map(Instance::isAkkoma).orElse(false);
|
||||
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), isAkkoma)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{
|
||||
if(ntf.status!=null){
|
||||
return new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status);
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
ArrayList<Notification> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
|
||||
callback.onSuccess(res);
|
||||
putNotifications(result, onlyMentions, onlyPosts, maxID==null);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onSuccess(res);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
callback.onError(error);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onError(error);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -327,7 +367,7 @@ public class CacheController{
|
||||
createRecentSearchesTable(db);
|
||||
}
|
||||
if(oldVersion<3){
|
||||
// MEGALODON-SPECIFIC
|
||||
// MEGALODON
|
||||
createPostsNotificationsTable(db);
|
||||
}
|
||||
if(oldVersion<4){
|
||||
|
||||
@@ -117,6 +117,9 @@ public class MastodonAPIController{
|
||||
synchronized(req){
|
||||
req.okhttpCall=call;
|
||||
}
|
||||
if(req.timeout>0){
|
||||
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
|
||||
@@ -153,13 +156,17 @@ public class MastodonAPIController{
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||
else
|
||||
else if(req.respClass!=null)
|
||||
respObj=gson.fromJson(respJson, req.respClass);
|
||||
else
|
||||
respObj=null;
|
||||
}else{
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(reader, req.respTypeToken.getType());
|
||||
else
|
||||
else if(req.respClass!=null)
|
||||
respObj=gson.fromJson(reader, req.respClass);
|
||||
else
|
||||
respObj=null;
|
||||
}
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if (req.context != null && response.body().contentType().subtype().equals("html")) {
|
||||
|
||||
@@ -49,6 +49,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
Token token;
|
||||
boolean canceled, isRemote;
|
||||
Map<String, String> headers;
|
||||
long timeout;
|
||||
private ProgressDialog progressDialog;
|
||||
protected boolean removeUnsupportedItems;
|
||||
@Nullable Context context;
|
||||
@@ -117,7 +118,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
.findAny())
|
||||
.map(AccountSession::getID)
|
||||
.map(this::exec)
|
||||
.orElse(this.execNoAuth(domain));
|
||||
.orElseGet(() -> this.execNoAuth(domain));
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
|
||||
@@ -152,6 +153,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
headers.put(key, value);
|
||||
}
|
||||
|
||||
protected void setTimeout(long timeout){
|
||||
this.timeout=timeout;
|
||||
}
|
||||
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v1";
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ public class PushSubscriptionManager{
|
||||
private String accountID;
|
||||
private PrivateKey privateKey;
|
||||
private PublicKey publicKey;
|
||||
private PublicKey serverKey;
|
||||
private byte[] authKey;
|
||||
|
||||
public PushSubscriptionManager(String accountID){
|
||||
@@ -162,10 +161,6 @@ public class PushSubscriptionManager{
|
||||
@Override
|
||||
public void onSuccess(PushSubscription result){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
result.serverKey=result.serverKey.replace('/','_');
|
||||
result.serverKey=result.serverKey.replace('+','-');
|
||||
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
|
||||
|
||||
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
|
||||
if(session==null)
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
public abstract class ResultlessMastodonAPIRequest extends MastodonAPIRequest<Void>{
|
||||
public ResultlessMastodonAPIRequest(HttpMethod method, String path){
|
||||
super(method, path, (Class<Void>)null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetAccountFeaturedHashtags extends MastodonAPIRequest<List<Hashtag>>{
|
||||
public GetAccountFeaturedHashtags(String id){
|
||||
super(HttpMethod.GET, "/accounts/"+id+"/featured_tags", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -21,22 +21,22 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
switch(filter){
|
||||
case DEFAULT -> addQueryParameter("exclude_replies", "true");
|
||||
case INCLUDE_REPLIES -> {}
|
||||
case PINNED -> addQueryParameter("pinned", "true");
|
||||
case MEDIA -> addQueryParameter("only_media", "true");
|
||||
case NO_REBLOGS -> {
|
||||
addQueryParameter("exclude_replies", "true");
|
||||
addQueryParameter("exclude_reblogs", "true");
|
||||
}
|
||||
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
|
||||
case PINNED -> addQueryParameter("pinned", "true");
|
||||
}
|
||||
}
|
||||
|
||||
public enum Filter{
|
||||
DEFAULT,
|
||||
INCLUDE_REPLIES,
|
||||
PINNED,
|
||||
MEDIA,
|
||||
NO_REBLOGS,
|
||||
OWN_POSTS_AND_REPLIES
|
||||
OWN_POSTS_AND_REPLIES,
|
||||
PINNED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,22 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
||||
public RegisterAccount(String username, String email, String password, String locale, String reason){
|
||||
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
|
||||
super(HttpMethod.POST, "/accounts", Token.class);
|
||||
setRequestBody(new Body(username, email, password, locale, reason));
|
||||
setRequestBody(new Body(username, email, password, locale, reason, timezone));
|
||||
}
|
||||
|
||||
private static class Body{
|
||||
public String username, email, password, locale, reason;
|
||||
public String username, email, password, locale, reason, timeZone;
|
||||
public boolean agreement=true;
|
||||
|
||||
public Body(String username, String email, String password, String locale, String reason){
|
||||
public Body(String username, String email, String password, String locale, String reason, String timeZone){
|
||||
this.username=username;
|
||||
this.email=email;
|
||||
this.password=password;
|
||||
this.locale=locale;
|
||||
this.reason=reason;
|
||||
this.timeZone=timeZone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
|
||||
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
|
||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public Boolean locked, discoverable;
|
||||
public RequestSource source;
|
||||
|
||||
public Request(Boolean locked, Boolean discoverable, RequestSource source){
|
||||
this.locked=locked;
|
||||
this.discoverable=discoverable;
|
||||
this.source=source;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestSource{
|
||||
public StatusPrivacy privacy;
|
||||
public String language;
|
||||
|
||||
public RequestSource(StatusPrivacy privacy, String language){
|
||||
this.privacy=privacy;
|
||||
this.language=language;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.api.requests.catalog;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetCatalogDefaultInstances extends MastodonAPIRequest<List<CatalogDefaultInstance>>{
|
||||
public GetCatalogDefaultInstances(){
|
||||
super(HttpMethod.GET, null, new TypeToken<>(){});
|
||||
setTimeout(500);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getURL(){
|
||||
return Uri.parse("https://api.joinmastodon.org/default-servers");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CreateFilter extends MastodonAPIRequest<Filter>{
|
||||
public CreateFilter(String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words){
|
||||
super(HttpMethod.POST, "/filters", Filter.class);
|
||||
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, words.stream().map(w->new KeywordAttribute(null, null, w.keyword, w.wholeWord)).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class DeleteFilter extends ResultlessMastodonAPIRequest{
|
||||
public DeleteFilter(String id){
|
||||
super(HttpMethod.DELETE, "/filters/"+id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
class FilterRequest{
|
||||
public String title;
|
||||
public EnumSet<FilterContext> context;
|
||||
public FilterAction filterAction;
|
||||
public Integer expiresIn;
|
||||
public List<KeywordAttribute> keywordsAttributes;
|
||||
|
||||
public FilterRequest(String title, EnumSet<FilterContext> context, FilterAction filterAction, Integer expiresIn, List<KeywordAttribute> keywordsAttributes){
|
||||
this.title=title;
|
||||
this.context=context;
|
||||
this.filterAction=filterAction;
|
||||
this.expiresIn=expiresIn;
|
||||
this.keywordsAttributes=keywordsAttributes;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
@@ -7,8 +7,13 @@ import org.joinmastodon.android.model.Filter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetWordFilters extends MastodonAPIRequest<List<Filter>>{
|
||||
public GetWordFilters(){
|
||||
public class GetFilters extends MastodonAPIRequest<List<Filter>>{
|
||||
public GetFilters(){
|
||||
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLegacyFilters extends MastodonAPIRequest<List<LegacyFilter>>{
|
||||
public GetLegacyFilters(){
|
||||
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
class KeywordAttribute{
|
||||
public String id;
|
||||
@SerializedName("_destroy")
|
||||
public Boolean delete;
|
||||
public String keyword;
|
||||
public Boolean wholeWord;
|
||||
|
||||
public KeywordAttribute(String id, Boolean delete, String keyword, Boolean wholeWord){
|
||||
this.id=id;
|
||||
this.delete=delete;
|
||||
this.keyword=keyword;
|
||||
this.wholeWord=wholeWord;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class UpdateFilter extends MastodonAPIRequest<Filter>{
|
||||
public UpdateFilter(String id, String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words, List<String> deletedWords){
|
||||
super(HttpMethod.PUT, "/filters/"+id, Filter.class);
|
||||
|
||||
List<KeywordAttribute> attrs=Stream.of(
|
||||
words.stream().map(w->new KeywordAttribute(w.id, null, w.keyword, w.wholeWord)),
|
||||
deletedWords.stream().map(wid->new KeywordAttribute(wid, true, null, null))
|
||||
).flatMap(Function.identity()).collect(Collectors.toList());
|
||||
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, attrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class GetInstanceExtendedDescription extends MastodonAPIRequest<GetInstanceExtendedDescription.Response>{
|
||||
public GetInstanceExtendedDescription(){
|
||||
super(HttpMethod.GET, "/instance/extended_description", Response.class);
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public Instant updatedAt;
|
||||
public String content;
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,18 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class CreateList extends MastodonAPIRequest<ListTimeline> {
|
||||
public CreateList(String title, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
public CreateList(String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
super(HttpMethod.POST, "/lists", ListTimeline.class);
|
||||
Request req = new Request();
|
||||
req.title = title;
|
||||
req.exclusive = exclusive;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
public static class Request {
|
||||
public String title;
|
||||
public boolean exclusive;
|
||||
public ListTimeline.RepliesPolicy repliesPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class UpdateList extends MastodonAPIRequest<ListTimeline> {
|
||||
public UpdateList(String id, String title, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
public UpdateList(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
|
||||
CreateList.Request req = new CreateList.Request();
|
||||
req.title = title;
|
||||
req.exclusive = exclusive;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package org.joinmastodon.android.api.requests.markers;
|
||||
|
||||
import org.joinmastodon.android.api.ApiUtils;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Marker;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class GetMarkers extends MastodonAPIRequest<Markers> {
|
||||
public GetMarkers(EnumSet<Marker.Type> timelines) {
|
||||
super(HttpMethod.GET, "/markers", Markers.class);
|
||||
for (String type : ApiUtils.enumSetToStrings(timelines, Marker.Type.class)){
|
||||
addQueryParameter("timeline[]", type);
|
||||
}
|
||||
public class GetMarkers extends MastodonAPIRequest<TimelineMarkers>{
|
||||
public GetMarkers(){
|
||||
super(HttpMethod.GET, "/markers", TimelineMarkers.class);
|
||||
addQueryParameter("timeline[]", "home");
|
||||
addQueryParameter("timeline[]", "notifications");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package org.joinmastodon.android.api.requests.markers;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
|
||||
import org.joinmastodon.android.model.Marker;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
|
||||
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
||||
public class SaveMarkers extends MastodonAPIRequest<TimelineMarkers>{
|
||||
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
|
||||
super(HttpMethod.POST, "/markers", Response.class);
|
||||
super(HttpMethod.POST, "/markers", TimelineMarkers.class);
|
||||
JsonObjectBuilder builder=new JsonObjectBuilder();
|
||||
if(lastSeenHomePostID!=null)
|
||||
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
|
||||
@@ -14,8 +14,4 @@ public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
||||
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public Marker home, notifications;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
|
||||
addQueryParameter("resolve", "true");
|
||||
}
|
||||
|
||||
public GetSearchResults limit(int limit){
|
||||
addQueryParameter("limit", String.valueOf(limit));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
|
||||
@@ -4,20 +4,19 @@ import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetBubbleTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
public GetBubbleTimeline(String maxID, int limit) {
|
||||
public GetBubbleTimeline(String maxID, int limit, String replyVisibility) {
|
||||
super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){});
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,32 @@ package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit){
|
||||
public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit, List<String> containsAny, List<String> containsAll, List<String> containsNone, boolean localOnly, String replyVisibility){
|
||||
super(HttpMethod.GET, "/timelines/tag/"+hashtag, new TypeToken<>(){});
|
||||
if (localOnly)
|
||||
addQueryParameter("local", "true");
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
if(containsAny!=null)
|
||||
for (String tag : containsAny)
|
||||
addQueryParameter("any[]", tag);
|
||||
if(containsAll!=null)
|
||||
for (String tag : containsAll)
|
||||
addQueryParameter("all[]", tag);
|
||||
if(containsNone!=null)
|
||||
for (String tag : containsNone)
|
||||
addQueryParameter("none[]", tag);
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetHomeTimeline(String maxID, String minID, int limit, String sinceID){
|
||||
public GetHomeTimeline(String maxID, String minID, int limit, String sinceID, String replyVisibility){
|
||||
super(HttpMethod.GET, "/timelines/home", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
@@ -19,7 +18,7 @@ public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID) {
|
||||
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID, String replyVisibility) {
|
||||
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
@@ -19,7 +18,7 @@ public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,13 @@ import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit){
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit, String replyVisibility){
|
||||
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
||||
if(local)
|
||||
addQueryParameter("local", "true");
|
||||
@@ -21,7 +20,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.fromJson;
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.enumValue;
|
||||
import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AccountLocalPreferences{
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public boolean showInteractionCounts;
|
||||
public boolean customEmojiInNames;
|
||||
public boolean revealCWs;
|
||||
public boolean hideSensitiveMedia;
|
||||
public boolean serverSideFiltersSupported;
|
||||
|
||||
// MEGALODON
|
||||
public boolean showReplies;
|
||||
public boolean showBoosts;
|
||||
public ArrayList<String> recentLanguages;
|
||||
public boolean bottomEncoding;
|
||||
public ContentType defaultContentType;
|
||||
public boolean contentTypesEnabled;
|
||||
public ArrayList<TimelineDefinition> timelines;
|
||||
public boolean localOnlySupported;
|
||||
public boolean glitchInstance;
|
||||
public String publishButtonText;
|
||||
public String timelineReplyVisibility; // akkoma-only
|
||||
public boolean keepOnlyLatestNotification;
|
||||
|
||||
private final static Type recentLanguagesType = new TypeToken<ArrayList<String>>() {}.getType();
|
||||
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
|
||||
|
||||
public AccountLocalPreferences(SharedPreferences prefs, AccountSession session){
|
||||
this.prefs=prefs;
|
||||
showInteractionCounts=prefs.getBoolean("interactionCounts", false);
|
||||
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
|
||||
revealCWs=prefs.getBoolean("revealCWs", false);
|
||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
||||
|
||||
// MEGALODON
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new ArrayList<>());
|
||||
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
|
||||
defaultContentType=enumValue(ContentType.class, prefs.getString("defaultContentType", ContentType.PLAIN.name()));
|
||||
contentTypesEnabled=prefs.getBoolean("contentTypesEnabled", true);
|
||||
timelines=fromJson(prefs.getString("timelines", null), timelinesType, TimelineDefinition.getDefaultTimelines(session.getID()));
|
||||
localOnlySupported=prefs.getBoolean("localOnlySupported", false);
|
||||
glitchInstance=prefs.getBoolean("glitchInstance", false);
|
||||
publishButtonText=prefs.getString("publishButtonText", null);
|
||||
timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null);
|
||||
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||
}
|
||||
|
||||
public long getNotificationsPauseEndTime(){
|
||||
return prefs.getLong("notificationsPauseTime", 0L);
|
||||
}
|
||||
|
||||
public void setNotificationsPauseEndTime(long time){
|
||||
prefs.edit().putLong("notificationsPauseTime", time).apply();
|
||||
}
|
||||
|
||||
public void save(){
|
||||
prefs.edit()
|
||||
.putBoolean("interactionCounts", showInteractionCounts)
|
||||
.putBoolean("emojiInNames", customEmojiInNames)
|
||||
.putBoolean("revealCWs", revealCWs)
|
||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
||||
|
||||
// MEGALODON
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
.putString("recentLanguages", gson.toJson(recentLanguages))
|
||||
.putBoolean("bottomEncoding", bottomEncoding)
|
||||
.putString("defaultContentType", defaultContentType==null ? null : defaultContentType.name())
|
||||
.putBoolean("contentTypesEnabled", contentTypesEnabled)
|
||||
.putString("timelines", gson.toJson(timelines))
|
||||
.putBoolean("localOnlySupported", localOnlySupported)
|
||||
.putBoolean("glitchInstance", glitchInstance)
|
||||
.putString("publishButtonText", publishButtonText)
|
||||
.putString("timelineReplyVisibility", timelineReplyVisibility)
|
||||
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,53 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.StatusInteractionController;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
|
||||
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class AccountSession{
|
||||
private static final String TAG="AccountSession";
|
||||
|
||||
public Token token;
|
||||
public Account self;
|
||||
public String domain;
|
||||
@@ -32,15 +60,17 @@ public class AccountSession{
|
||||
public PushSubscription pushSubscription;
|
||||
public boolean needUpdatePushSettings;
|
||||
public long filtersLastUpdated;
|
||||
public List<Filter> wordFilters=new ArrayList<>();
|
||||
public List<LegacyFilter> wordFilters=new ArrayList<>();
|
||||
public String pushAccountID;
|
||||
public Preferences preferences;
|
||||
public AccountActivationInfo activationInfo;
|
||||
public Markers markers;
|
||||
public Preferences preferences;
|
||||
private transient MastodonAPIController apiController;
|
||||
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
|
||||
private transient CacheController cacheController;
|
||||
private transient PushSubscriptionManager pushSubscriptionManager;
|
||||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
@@ -58,10 +88,6 @@ public class AccountSession{
|
||||
return domain+"_"+self.id;
|
||||
}
|
||||
|
||||
public String getFullUsername() {
|
||||
return "@"+self.username+"@"+domain;
|
||||
}
|
||||
|
||||
public MastodonAPIController getApiController(){
|
||||
if(apiController==null)
|
||||
apiController=new MastodonAPIController(this);
|
||||
@@ -92,6 +118,188 @@ public class AccountSession{
|
||||
return pushSubscriptionManager;
|
||||
}
|
||||
|
||||
public String getFullUsername(){
|
||||
return '@'+self.username+'@'+domain;
|
||||
}
|
||||
|
||||
public void preferencesFromAccountSource(Account account) {
|
||||
if (account != null && account.source != null && preferences != null) {
|
||||
if (account.source.privacy != null)
|
||||
preferences.postingDefaultVisibility = account.source.privacy;
|
||||
if (account.source.language != null)
|
||||
preferences.postingDefaultLanguage = account.source.language;
|
||||
}
|
||||
}
|
||||
|
||||
public void reloadPreferences(Consumer<Preferences> callback){
|
||||
new GetPreferences()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Preferences result){
|
||||
preferences=result;
|
||||
preferencesFromAccountSource(self);
|
||||
if(callback!=null)
|
||||
callback.accept(result);
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
|
||||
}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public SharedPreferences getRawLocalPreferences(){
|
||||
if(prefs==null)
|
||||
prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE);
|
||||
return prefs;
|
||||
}
|
||||
|
||||
public void reloadNotificationsMarker(Consumer<String> callback){
|
||||
new GetMarkers()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(TimelineMarkers result){
|
||||
if(result.notifications!=null && !TextUtils.isEmpty(result.notifications.lastReadId)){
|
||||
String id=result.notifications.lastReadId;
|
||||
String lastKnown=getLastKnownNotificationsMarker();
|
||||
if(ObjectIdComparator.INSTANCE.compare(id, lastKnown)<0){
|
||||
// Marker moved back -- previous marker update must have failed.
|
||||
// Pretend it didn't happen and repeat the request.
|
||||
id=lastKnown;
|
||||
new SaveMarkers(null, id).exec(getID());
|
||||
}
|
||||
callback.accept(id);
|
||||
setNotificationsMarker(id, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public String getLastKnownNotificationsMarker(){
|
||||
return getRawLocalPreferences().getString("notificationsMarker", null);
|
||||
}
|
||||
|
||||
public void setNotificationsMarker(String id, boolean clearUnread){
|
||||
getRawLocalPreferences().edit().putString("notificationsMarker", id).apply();
|
||||
E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread));
|
||||
}
|
||||
|
||||
public void logOut(Activity activity, Runnable onDone){
|
||||
new RevokeOauthToken(app.clientId, app.clientSecret, token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
AccountSessionManager.getInstance().removeAccount(getID());
|
||||
onDone.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
AccountSessionManager.getInstance().removeAccount(getID());
|
||||
onDone.run();
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.loading, false)
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public void savePreferencesLater(){
|
||||
preferencesNeedSaving=true;
|
||||
}
|
||||
|
||||
public void savePreferencesIfPending(){
|
||||
if(preferencesNeedSaving){
|
||||
new UpdateAccountCredentialsPreferences(preferences, null, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
preferencesNeedSaving=false;
|
||||
self=result;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.e(TAG, "failed to save preferences: "+error);
|
||||
}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
}
|
||||
|
||||
public AccountLocalPreferences getLocalPreferences(){
|
||||
if(localPreferences==null)
|
||||
localPreferences=new AccountLocalPreferences(getRawLocalPreferences(), this);
|
||||
return localPreferences;
|
||||
}
|
||||
|
||||
public void filterStatuses(List<Status> statuses, FilterContext context){
|
||||
filterStatuses(statuses, context, null);
|
||||
}
|
||||
|
||||
public void filterStatuses(List<Status> statuses, FilterContext context, Account profile){
|
||||
filterStatusContainingObjects(statuses, Function.identity(), context, profile);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context){
|
||||
filterStatusContainingObjects(objects, extractor, context, null);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
|
||||
Predicate<Status> statusIsOnOwnProfile = (s) -> self != null && profile != null && s.account != null
|
||||
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
|
||||
|
||||
if(getLocalPreferences().serverSideFiltersSupported){
|
||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
if(s.filtered==null)
|
||||
return false;
|
||||
// don't hide own posts in own profile
|
||||
if (statusIsOnOwnProfile.test(s))
|
||||
return false;
|
||||
for(FilterResult filter:s.filtered){
|
||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(wordFilters==null)
|
||||
return;
|
||||
for(T obj:objects){
|
||||
Status s=extractor.apply(obj);
|
||||
if(s!=null && s.filtered!=null){
|
||||
getLocalPreferences().serverSideFiltersSupported=true;
|
||||
getLocalPreferences().save();
|
||||
return;
|
||||
}
|
||||
}
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
// don't hide own posts in own profile
|
||||
if (statusIsOnOwnProfile.test(s))
|
||||
return false;
|
||||
for(LegacyFilter filter:wordFilters){
|
||||
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Instance> getInstance() {
|
||||
return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain));
|
||||
}
|
||||
|
||||
@@ -21,23 +21,18 @@ import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
|
||||
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
|
||||
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||
import org.joinmastodon.android.events.EmojiUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Marker;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
import java.io.File;
|
||||
@@ -50,10 +45,10 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -155,11 +150,19 @@ public class AccountSessionManager{
|
||||
return session;
|
||||
}
|
||||
|
||||
public static AccountSession get(String id){
|
||||
return getInstance().getAccount(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession tryGetAccount(String id){
|
||||
return sessions.get(id);
|
||||
}
|
||||
|
||||
public static Optional<AccountSession> getOptional(String id) {
|
||||
return Optional.ofNullable(getInstance().tryGetAccount(id));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession tryGetAccount(Account account) {
|
||||
return sessions.get(account.getDomainFromURL() + "_" + account.id);
|
||||
@@ -192,13 +195,19 @@ public class AccountSessionManager{
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
GlobalUserPreferences.removeAccount(id);
|
||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
MastodonApp.context.deleteSharedPreferences(id);
|
||||
}else{
|
||||
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
|
||||
}
|
||||
sessions.remove(id);
|
||||
if(lastActiveAccountID.equals(id)){
|
||||
if(sessions.isEmpty())
|
||||
lastActiveAccountID=null;
|
||||
else
|
||||
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
|
||||
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
|
||||
}
|
||||
writeAccountsFile();
|
||||
String domain=session.domain.toLowerCase();
|
||||
@@ -271,14 +280,13 @@ public class AccountSessionManager{
|
||||
HashSet<String> domains=new HashSet<>();
|
||||
for(AccountSession session:sessions.values()){
|
||||
domains.add(session.domain.toLowerCase());
|
||||
if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){
|
||||
updateSessionPreferences(session);
|
||||
if(session == activeSession || now-session.infoLastUpdated>24L*3600_000L){
|
||||
session.reloadPreferences(null);
|
||||
updateSessionLocalInfo(session);
|
||||
}
|
||||
if(now-session.filtersLastUpdated>3600_000L || session == activeSession){
|
||||
if(session == activeSession || (session.getLocalPreferences().serverSideFiltersSupported && now-session.filtersLastUpdated>3600_000L)){
|
||||
updateSessionWordFilters(session);
|
||||
}
|
||||
updateSessionMarkers(session);
|
||||
}
|
||||
if(loadedInstances){
|
||||
maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null);
|
||||
@@ -289,20 +297,12 @@ public class AccountSessionManager{
|
||||
long now=System.currentTimeMillis();
|
||||
for(String domain:domains){
|
||||
Long lastUpdated=instancesLastUpdated.get(domain);
|
||||
if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){
|
||||
if(domain.equals(activeDomain) || lastUpdated==null || now-lastUpdated>24L*3600_000L){
|
||||
updateInstanceInfo(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void preferencesFromSource(AccountSession session, Account account) {
|
||||
if (account != null && account.source != null && session.preferences != null) {
|
||||
if (account.source.privacy != null)
|
||||
session.preferences.postingDefaultVisibility = account.source.privacy;
|
||||
if (account.source.language != null)
|
||||
session.preferences.postingDefaultLanguage = account.source.language;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSessionLocalInfo(AccountSession session){
|
||||
new GetOwnAccount()
|
||||
@@ -311,39 +311,7 @@ public class AccountSessionManager{
|
||||
public void onSuccess(Account result){
|
||||
session.self=result;
|
||||
session.infoLastUpdated=System.currentTimeMillis();
|
||||
preferencesFromSource(session, result);
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
})
|
||||
.exec(session.getID());
|
||||
}
|
||||
|
||||
private void updateSessionPreferences(AccountSession session){
|
||||
new GetPreferences().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Preferences preferences) {
|
||||
session.preferences=preferences;
|
||||
preferencesFromSource(session, session.self);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
session.preferences = new Preferences();
|
||||
preferencesFromSource(session, session.self);
|
||||
}
|
||||
}).exec(session.getID());
|
||||
}
|
||||
|
||||
private void updateSessionWordFilters(AccountSession session){
|
||||
new GetWordFilters()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Filter> result){
|
||||
session.wordFilters=result;
|
||||
session.filtersLastUpdated=System.currentTimeMillis();
|
||||
session.preferencesFromAccountSource(result);
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
@@ -355,19 +323,22 @@ public class AccountSessionManager{
|
||||
.exec(session.getID());
|
||||
}
|
||||
|
||||
private void updateSessionMarkers(AccountSession session) {
|
||||
new GetMarkers(EnumSet.allOf(Marker.Type.class)).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Markers markers) {
|
||||
session.markers = markers;
|
||||
writeAccountsFile();
|
||||
}
|
||||
private void updateSessionWordFilters(AccountSession session){
|
||||
new GetLegacyFilters()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<LegacyFilter> result){
|
||||
session.wordFilters=result;
|
||||
session.filtersLastUpdated=System.currentTimeMillis();
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
}
|
||||
}).exec(session.getID());
|
||||
}
|
||||
})
|
||||
.exec(session.getID());
|
||||
}
|
||||
|
||||
public void updateInstanceInfo(String domain){
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class AllNotificationsSeenEvent {
|
||||
}
|
||||
@@ -6,10 +6,12 @@ public class ListUpdatedCreatedEvent {
|
||||
public final String id;
|
||||
public final String title;
|
||||
public final ListTimeline.RepliesPolicy repliesPolicy;
|
||||
public final boolean exclusive;
|
||||
|
||||
public ListUpdatedCreatedEvent(String id, String title, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
public ListUpdatedCreatedEvent(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.exclusive = exclusive;
|
||||
this.repliesPolicy = repliesPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class NotificationReceivedEvent {
|
||||
public String account, id;
|
||||
public NotificationReceivedEvent(String account, String id) {
|
||||
this.account = account;
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class NotificationsMarkerUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final String marker;
|
||||
public final boolean clearUnread;
|
||||
|
||||
public NotificationsMarkerUpdatedEvent(String accountID, String marker, boolean clearUnread){
|
||||
this.accountID=accountID;
|
||||
this.marker=marker;
|
||||
this.clearUnread=clearUnread;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
|
||||
public class SettingsFilterCreatedOrUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final Filter filter;
|
||||
|
||||
public SettingsFilterCreatedOrUpdatedEvent(String accountID, Filter filter){
|
||||
this.accountID=accountID;
|
||||
this.filter=filter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class SettingsFilterDeletedEvent{
|
||||
public final String accountID;
|
||||
public final String filterID;
|
||||
|
||||
public SettingsFilterDeletedEvent(String accountID, String filterID){
|
||||
this.accountID=accountID;
|
||||
this.filterID=filterID;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class StatusCountersUpdatedEvent{
|
||||
public String id;
|
||||
public long favorites, reblogs, replies;
|
||||
public boolean favorited, reblogged, bookmarked, pinned;
|
||||
public Status status;
|
||||
|
||||
public StatusCountersUpdatedEvent(Status s){
|
||||
id=s.id;
|
||||
status=s;
|
||||
favorites=s.favouritesCount;
|
||||
reblogs=s.reblogsCount;
|
||||
replies=s.repliesCount;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class StatusDisplaySettingsChangedEvent{
|
||||
public final String accountID;
|
||||
|
||||
public StatusDisplaySettingsChangedEvent(String accountID){
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
@@ -48,27 +48,22 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
user=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||
filter=GetAccountStatuses.Filter.valueOf(getArguments().getString("filter"));
|
||||
super.onAttach(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(user==null) // TODO figure out why this happens
|
||||
return;
|
||||
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
result=result.stream().filter(status -> {
|
||||
// don't hide own posts in own profile
|
||||
if (asm.isSelf(accountID, user) && asm.isSelf(accountID, status.account)) return true;
|
||||
else return new StatusFilterPredicate(accountID, getFilterContext()).test(status);
|
||||
}).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
boolean empty=result.isEmpty();
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext(), user);
|
||||
onDataLoaded(result, !empty);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -77,6 +72,7 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
view.setBackground(null); // prevents unnecessary overdraw
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,20 +82,20 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
loadData();
|
||||
}
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||
protected void onStatusCreated(Status status){
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
if(!asm.isSelf(accountID, ev.status.account) || !asm.isSelf(accountID, user))
|
||||
if(!asm.isSelf(accountID, status.account) || !asm.isSelf(accountID, user))
|
||||
return;
|
||||
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
||||
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
||||
// Keep replies to self, discard all other replies
|
||||
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||
if(status.inReplyToAccountId!=null && !status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||
return;
|
||||
}else if(filter==GetAccountStatuses.Filter.MEDIA){
|
||||
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true))
|
||||
if(Optional.ofNullable(status.mediaAttachments).map(List::isEmpty).orElse(true))
|
||||
return;
|
||||
}
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
prependItems(Collections.singletonList(status), true);
|
||||
if (isOnTop()) scrollToTop();
|
||||
}
|
||||
|
||||
@@ -130,8 +126,8 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
protected FilterContext getFilterContext() {
|
||||
return FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,14 +6,10 @@ import android.content.res.Configuration;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Layout;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
@@ -26,6 +22,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
@@ -33,13 +30,16 @@ import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||
@@ -54,6 +54,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -68,10 +69,11 @@ import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
|
||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends MastodonRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
|
||||
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
||||
protected DisplayItemsAdapter adapter;
|
||||
protected String accountID;
|
||||
@@ -96,7 +98,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(GlobalUserPreferences.disableMarquee){
|
||||
if(GlobalUserPreferences.toolbarMarquee){
|
||||
setTitleMarqueeEnabled(false);
|
||||
setSubtitleMarqueeEnabled(false);
|
||||
}
|
||||
@@ -350,7 +352,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
public void getSelectorBounds(View view, Rect outRect){
|
||||
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
|
||||
int lastIndex = -1, firstIndex = -1;
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
}else{
|
||||
outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
|
||||
}
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder){
|
||||
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
|
||||
@@ -427,6 +433,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
|
||||
protected int getMainAdapterOffset(){
|
||||
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
|
||||
return mergeAdapter.getPositionForAdapter(adapter);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -438,6 +447,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
c.drawLine(0, y, parent.getWidth(), y, paint);
|
||||
}
|
||||
|
||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract void onItemClick(String id);
|
||||
|
||||
protected void updatePoll(String itemID, Status status, Poll poll){
|
||||
@@ -525,38 +538,57 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
public void onRevealSpoilerClick(TextStatusDisplayItem.Holder holder){
|
||||
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
revealSpoiler(status, holder.getItemID());
|
||||
toggleSpoiler(status, holder.getItemID());
|
||||
}
|
||||
|
||||
public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
revealSpoiler(status, holder.getItemID());
|
||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
|
||||
Status status = holder.getItem().status;
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
||||
if (mediaGrid != null) {
|
||||
if (!status.sensitiveRevealed) mediaGrid.revealSensitive();
|
||||
else mediaGrid.hideSensitive();
|
||||
} else {
|
||||
// media grid's methods normally change the status' state - we still want to be able
|
||||
// to do this if the media grid is not bound, tho - so, doing it ourselves here
|
||||
status.sensitiveRevealed = !status.sensitiveRevealed;
|
||||
}
|
||||
holder.rebind();
|
||||
}
|
||||
|
||||
protected void revealSpoiler(Status status, String itemID){
|
||||
status.spoilerRevealed=true;
|
||||
public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) {
|
||||
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if(header != null) header.rebind();
|
||||
}
|
||||
|
||||
protected void toggleSpoiler(Status status, String itemID){
|
||||
status.spoilerRevealed=!status.spoilerRevealed;
|
||||
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
|
||||
status.sensitiveRevealed = false;
|
||||
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
if(spoiler!=null)
|
||||
spoiler.rebind();
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
|
||||
|
||||
int index=displayItems.indexOf(spoilerItem);
|
||||
if(status.spoilerRevealed){
|
||||
displayItems.addAll(index+1, spoilerItem.contentItems);
|
||||
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
|
||||
}else{
|
||||
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
|
||||
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
||||
}
|
||||
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null)
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null)
|
||||
header.rebind();
|
||||
updateImagesSpoilerState(status, itemID);
|
||||
}
|
||||
|
||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
status.spoilerRevealed=!status.spoilerRevealed;
|
||||
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||
TextStatusDisplayItem.Holder text = findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null){
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
|
||||
}
|
||||
}
|
||||
holder.rebind();
|
||||
updateImagesSpoilerState(status, holder.getItemID());
|
||||
list.invalidateItemDecorations();
|
||||
}
|
||||
|
||||
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
|
||||
@@ -575,30 +607,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
if (header != null) header.rebind();
|
||||
}
|
||||
|
||||
protected void updateImagesSpoilerState(Status status, String itemID){
|
||||
ArrayList<Integer> updatedPositions=new ArrayList<>();
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
|
||||
if(mediaGrid!=null){
|
||||
mediaGrid.setRevealed(status.spoilerRevealed);
|
||||
updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
}
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(itemID.equals(item.parentID) && item instanceof MediaGridStatusDisplayItem && !updatedPositions.contains(i)){
|
||||
adapter.notifyItemChanged(i);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public void onImageUpdated(MediaGridStatusDisplayItem.Holder holder, int index) {
|
||||
holder.rebind();
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
||||
if(mediaGrid!=null){
|
||||
adapter.notifyItemChanged(mediaGrid.getAbsoluteAdapterPosition());
|
||||
}
|
||||
}
|
||||
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
||||
|
||||
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
||||
@@ -754,11 +762,24 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
||||
}
|
||||
|
||||
public void rebuildAllDisplayItems(){
|
||||
displayItems.clear();
|
||||
for(T item:data){
|
||||
displayItems.addAll(buildDisplayItems(item));
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||
|
||||
@Override
|
||||
protected void onDataLoaded(List<T> d, boolean more) {
|
||||
if(getContext()==null) return;
|
||||
super.onDataLoaded(d, more);
|
||||
// more available, but the page isn't even full yet? seems wrong, let's load some more
|
||||
if (more && d.size() < itemsPerPage) preloader.onScrolledToLastItem();
|
||||
if(more && d.size() < itemsPerPage){
|
||||
preloader.onScrolledToLastItem();
|
||||
}
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
@@ -770,7 +791,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@NonNull
|
||||
@Override
|
||||
public BindableViewHolder<StatusDisplayItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return (BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent);
|
||||
BindableViewHolder<StatusDisplayItem> holder=(BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent, BaseStatusListFragment.this);
|
||||
onModifyItemViewHolder(holder);
|
||||
return holder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -801,15 +824,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
|
||||
private class StatusListItemDecoration extends RecyclerView.ItemDecoration{
|
||||
private Paint dividerPaint=new Paint(), hiddenMediaPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private Typeface mediumTypeface=Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||
private Layout mediaHiddenTitleLayout, mediaHiddenTextLayout, tapToRevealTextLayout;
|
||||
private int currentMediaHiddenLayoutsWidth=0;
|
||||
private Paint dividerPaint=new Paint();
|
||||
|
||||
{
|
||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
|
||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
|
||||
dividerPaint.setStyle(Paint.Style.STROKE);
|
||||
dividerPaint.setStrokeWidth(V.dp(1));
|
||||
dividerPaint.setStrokeWidth(V.dp(0.5f));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -819,80 +839,23 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
View bottomSibling=parent.getChildAt(i+1);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
|
||||
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
|
||||
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
|
||||
if(needDrawDivider(holder, siblingHolder)){
|
||||
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
|
||||
if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
|
||||
hiddenMediaPaint.setColor(0x80000000);
|
||||
c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint);
|
||||
}
|
||||
}
|
||||
private boolean needDrawDivider(RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
if(needDividerForExtraItem(holder.itemView, siblingHolder.itemView, holder, siblingHolder))
|
||||
return true;
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh){
|
||||
// Do not draw dividers between hashtag and/or account rows
|
||||
if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder))
|
||||
return false;
|
||||
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) return false;
|
||||
return (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
|
||||
}
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
|
||||
if(!imgHolder.getItem().status.spoilerRevealed){
|
||||
if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
|
||||
int listWidth=getListWidthForMediaLayout();
|
||||
int width=Math.min(listWidth, UiUtils.MAX_WIDTH);
|
||||
if(currentMediaHiddenLayoutsWidth!=width)
|
||||
rebuildMediaHiddenLayouts(width-V.dp(32));
|
||||
c.save();
|
||||
float totalHeight;
|
||||
boolean hiddenByAuthor=imgHolder.getItem().status.sensitive;
|
||||
if(hiddenByAuthor)
|
||||
totalHeight=mediaHiddenTitleLayout.getHeight()+mediaHiddenTextLayout.getHeight()+V.dp(8);
|
||||
else
|
||||
totalHeight=tapToRevealTextLayout.getHeight();
|
||||
c.translate(child.getX()+V.dp(16), child.getY()+child.getHeight()/2f-totalHeight/2f);
|
||||
if(hiddenByAuthor){
|
||||
mediaHiddenTitleLayout.draw(c);
|
||||
c.translate(0, mediaHiddenTitleLayout.getHeight()+V.dp(8));
|
||||
mediaHiddenTextLayout.draw(c);
|
||||
}else{
|
||||
tapToRevealTextLayout.draw(c);
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rebuildMediaHiddenLayouts(int width){
|
||||
currentMediaHiddenLayoutsWidth=width;
|
||||
String title=getString(R.string.sensitive_content);
|
||||
TextPaint titlePaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
titlePaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray50));
|
||||
titlePaint.setTextSize(V.dp(22));
|
||||
titlePaint.setTypeface(mediumTypeface);
|
||||
mediaHiddenTitleLayout=StaticLayout.Builder.obtain(title, 0, title.length(), titlePaint, width)
|
||||
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||
.build();
|
||||
String tapToReveal=getString(R.string.tap_to_reveal);
|
||||
tapToRevealTextLayout=StaticLayout.Builder.obtain(tapToReveal, 0, tapToReveal.length(), titlePaint, width)
|
||||
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||
.build();
|
||||
TextPaint textPaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
textPaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray200));
|
||||
textPaint.setTextSize(V.dp(16));
|
||||
String text=getString(R.string.sensitive_content_explain);
|
||||
mediaHiddenTextLayout=StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, width)
|
||||
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||
.setLineSpacing(V.dp(5), 1f)
|
||||
.build();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -39,8 +40,13 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FilterContext getFilterContext() {
|
||||
return FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,18 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -12,28 +20,36 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.UpdateAttachment;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.parceler.Parcels;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.utils.ColorPalette;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import java.util.Collections;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
|
||||
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{
|
||||
private static final String TAG="ComposeImageDescription";
|
||||
|
||||
private String accountID, attachmentID;
|
||||
private EditText edit;
|
||||
private Button saveButton;
|
||||
private ImageView image;
|
||||
private ContextThemeWrapper themeWrapper;
|
||||
private PhotoViewer photoViewer;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -46,7 +62,14 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.edit_image);
|
||||
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
|
||||
ColorPalette.palettes.get(GlobalUserPreferences.color).apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK);
|
||||
setTitle(R.string.add_alt_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
return super.onCreateView(themeWrapper.getSystemService(LayoutInflater.class), container, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,14 +77,48 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
|
||||
View view=inflater.inflate(R.layout.fragment_image_description, container, false);
|
||||
|
||||
edit=view.findViewById(R.id.edit);
|
||||
ImageView image=view.findViewById(R.id.photo);
|
||||
image=view.findViewById(R.id.photo);
|
||||
int width=getArguments().getInt("width", 0);
|
||||
int height=getArguments().getInt("height", 0);
|
||||
if(width>0 && height>0){
|
||||
// image.setAspectRatio(Math.max(1f, (float)width/height));
|
||||
}
|
||||
image.setOnClickListener(v->openPhotoViewer());
|
||||
Uri uri=getArguments().getParcelable("uri");
|
||||
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
|
||||
Attachment.Type type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
|
||||
if(type==Attachment.Type.IMAGE)
|
||||
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
|
||||
else
|
||||
loadVideoThumbIntoView(image, uri);
|
||||
edit.setText(getArguments().getString("existingDescription"));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void loadVideoThumbIntoView(ImageView target, Uri uri){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Context context=getActivity();
|
||||
if(context==null)
|
||||
return;
|
||||
try{
|
||||
MediaMetadataRetriever mmr=new MediaMetadataRetriever();
|
||||
mmr.setDataSource(context, uri);
|
||||
Bitmap frame=mmr.getFrameAtTime(3_000_000);
|
||||
mmr.release();
|
||||
int size=Math.max(frame.getWidth(), frame.getHeight());
|
||||
int maxSize=V.dp(250);
|
||||
if(size>maxSize){
|
||||
float factor=maxSize/(float)size;
|
||||
frame=Bitmap.createScaledBitmap(frame, Math.round(frame.getWidth()*factor), Math.round(frame.getHeight()*factor), true);
|
||||
}
|
||||
Bitmap finalFrame=frame;
|
||||
target.post(()->target.setImageBitmap(finalFrame));
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "loadVideoThumbIntoView: error getting video frame", x);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@@ -71,43 +128,114 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
TypedArray ta=getActivity().obtainStyledAttributes(new int[]{R.attr.secondaryButtonStyle});
|
||||
int buttonStyle=ta.getResourceId(0, 0);
|
||||
ta.recycle();
|
||||
saveButton=new Button(getActivity(), null, 0, buttonStyle);
|
||||
saveButton.setText(R.string.save);
|
||||
saveButton.setOnClickListener(this::onSaveClick);
|
||||
FrameLayout wrap=new FrameLayout(getActivity());
|
||||
wrap.addView(saveButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT));
|
||||
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
||||
wrap.setClipToPadding(false);
|
||||
MenuItem item=menu.add(R.string.publish);
|
||||
item.setActionView(wrap);
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
inflater.inflate(R.menu.compose_image_description, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.help){
|
||||
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
|
||||
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
|
||||
for(BulletSpan span:spans){
|
||||
BulletSpan betterSpan;
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
|
||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
|
||||
else
|
||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
|
||||
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
|
||||
msg.removeSpan(span);
|
||||
}
|
||||
new M3AlertDialogBuilder(themeWrapper)
|
||||
.setTitle(R.string.what_is_alt_text)
|
||||
.setMessage(msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onSaveClick(View v){
|
||||
new UpdateAttachment(attachmentID, edit.getText().toString().trim())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Attachment result){
|
||||
Bundle r=new Bundle();
|
||||
r.putParcelable("attachment", Parcels.wrap(result));
|
||||
setResult(true, r);
|
||||
Nav.finish(ComposeImageDescriptionFragment.this);
|
||||
}
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
deliverResult();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.saving, false)
|
||||
.exec(accountID);
|
||||
@Override
|
||||
protected LayoutInflater getToolbarLayoutInflater(){
|
||||
return LayoutInflater.from(themeWrapper);
|
||||
}
|
||||
|
||||
private void deliverResult(){
|
||||
Bundle r=new Bundle();
|
||||
r.putString("text", edit.getText().toString().trim());
|
||||
r.putString("attachment", attachmentID);
|
||||
setResult(true, r);
|
||||
}
|
||||
|
||||
private void openPhotoViewer(){
|
||||
Attachment fakeAttachment=new Attachment();
|
||||
fakeAttachment.id="local";
|
||||
fakeAttachment.type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
|
||||
int width=getArguments().getInt("width", 0);
|
||||
int height=getArguments().getInt("height", 0);
|
||||
Uri uri=getArguments().getParcelable("uri");
|
||||
fakeAttachment.url=uri.toString();
|
||||
fakeAttachment.meta=new Attachment.Metadata();
|
||||
fakeAttachment.meta.width=width;
|
||||
fakeAttachment.meta.height=height;
|
||||
|
||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
|
||||
@Override
|
||||
public void setPhotoViewVisibility(int index, boolean visible){
|
||||
image.setAlpha(visible ? 1f : 0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
|
||||
int[] pos={0, 0};
|
||||
image.getLocationOnScreen(pos);
|
||||
outRect.set(pos[0], pos[1], pos[0]+image.getWidth(), pos[1]+image.getHeight());
|
||||
image.setElevation(1f);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransitioningViewTransform(float translateX, float translateY, float scale){
|
||||
image.setTranslationX(translateX);
|
||||
image.setTranslationY(translateY);
|
||||
image.setScaleX(scale);
|
||||
image.setScaleY(scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endPhotoViewTransition(){
|
||||
Drawable d=image.getDrawable();
|
||||
image.setImageDrawable(null);
|
||||
image.setImageDrawable(d);
|
||||
|
||||
image.setTranslationX(0f);
|
||||
image.setTranslationY(0f);
|
||||
image.setScaleX(1f);
|
||||
image.setScaleY(1f);
|
||||
image.setElevation(0f);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Drawable getPhotoViewCurrentDrawable(int index){
|
||||
return image.getDrawable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void photoViewerDismissed(){
|
||||
photoViewer=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissions(String[] permissions){
|
||||
|
||||
}
|
||||
});
|
||||
photoViewer.removeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import static android.view.Menu.NONE;
|
||||
|
||||
import static com.hootsuite.nachos.terminator.ChipTerminatorHandler.BEHAVIOR_CHIPIFY_ALL;
|
||||
import static org.joinmastodon.android.ui.utils.UiUtils.makeBackItem;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -14,46 +16,51 @@ import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import com.hootsuite.nachos.NachoTextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition> implements ScrollableToTop {
|
||||
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
|
||||
private String accountID;
|
||||
private TimelinesAdapter adapter;
|
||||
private final ItemTouchHelper itemTouchHelper;
|
||||
@@ -62,6 +69,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>();
|
||||
private final List<ListTimeline> listTimelines = new ArrayList<>();
|
||||
private final List<Hashtag> hashtags = new ArrayList<>();
|
||||
private MenuItem addHashtagItem;
|
||||
|
||||
public EditTimelinesFragment() {
|
||||
super(10);
|
||||
@@ -114,7 +122,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
itemTouchHelper.attachToRecyclerView(list);
|
||||
refreshLayout.setEnabled(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,21 +140,34 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
}
|
||||
TimelineDefinition tl = timelineByMenuItem.get(item);
|
||||
if (tl != null) {
|
||||
data.add(tl.copy());
|
||||
adapter.notifyItemInserted(data.size());
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
};
|
||||
addTimeline(tl);
|
||||
} else if (item == addHashtagItem) {
|
||||
makeTimelineEditor(null, (hashtag) -> {
|
||||
if (hashtag != null) addTimeline(hashtag);
|
||||
}, null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addTimeline(TimelineDefinition tl) {
|
||||
data.add(tl.copy());
|
||||
adapter.notifyItemInserted(data.size());
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
private void addTimelineToOptions(TimelineDefinition tl, Menu menu) {
|
||||
if (data.contains(tl)) return;
|
||||
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, tl.getTitle(getContext()));
|
||||
item.setIcon(tl.getIcon().iconRes);
|
||||
MenuItem item = addOptionsItem(menu, tl.getTitle(getContext()), tl.getIcon().iconRes);
|
||||
timelineByMenuItem.put(item, tl);
|
||||
}
|
||||
|
||||
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon) {
|
||||
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, name);
|
||||
item.setIcon(icon);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void updateOptionsMenu() {
|
||||
if (getActivity() == null) return;
|
||||
optionsMenu.clear();
|
||||
@@ -167,8 +188,9 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
makeBackItem(listsMenu);
|
||||
makeBackItem(hashtagsMenu);
|
||||
|
||||
TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
|
||||
addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
||||
|
||||
timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0);
|
||||
@@ -179,10 +201,12 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
}
|
||||
|
||||
private void saveTimelines() {
|
||||
updated = true;
|
||||
GlobalUserPreferences.pinnedTimelines.put(accountID, data.size() > 0 ? data : List.of(TimelineDefinition.HOME_TIMELINE));
|
||||
GlobalUserPreferences.save();
|
||||
}
|
||||
updated=true;
|
||||
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
|
||||
if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
|
||||
prefs.timelines=data;
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
private void removeTimeline(int position) {
|
||||
data.remove(position);
|
||||
@@ -193,7 +217,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false);
|
||||
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@@ -213,6 +237,140 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
if (updated) UiUtils.restartApp();
|
||||
}
|
||||
|
||||
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) {
|
||||
if (tags == null || tags.isEmpty()) return false;
|
||||
editText.setText(String.join(",", tags));
|
||||
editText.chipifyAllUnterminatedTokens();
|
||||
return true;
|
||||
}
|
||||
|
||||
private NachoTextView prepareChipTextView(NachoTextView nacho) {
|
||||
nacho.addChipTerminator(',', BEHAVIOR_CHIPIFY_ALL);
|
||||
nacho.addChipTerminator('\n', BEHAVIOR_CHIPIFY_ALL);
|
||||
nacho.addChipTerminator(' ', BEHAVIOR_CHIPIFY_ALL);
|
||||
nacho.addChipTerminator(';', BEHAVIOR_CHIPIFY_ALL);
|
||||
nacho.enableEditChipOnTouch(true, true);
|
||||
nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens());
|
||||
return nacho;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove) {
|
||||
Context ctx = getContext();
|
||||
View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
|
||||
|
||||
View divider = view.findViewById(R.id.divider);
|
||||
Button advancedBtn = view.findViewById(R.id.advanced);
|
||||
EditText editText = view.findViewById(R.id.input);
|
||||
if (item != null) editText.setText(item.getCustomTitle());
|
||||
editText.setHint(item != null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
|
||||
|
||||
LinearLayout tagWrap = view.findViewById(R.id.tag_wrap);
|
||||
boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG;
|
||||
advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
|
||||
advancedBtn.setOnClickListener(l -> {
|
||||
advancedBtn.setSelected(!advancedBtn.isSelected());
|
||||
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
|
||||
divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
UiUtils.beginLayoutTransition((ViewGroup) view);
|
||||
});
|
||||
|
||||
Switch localOnlySwitch = view.findViewById(R.id.local_only_switch);
|
||||
view.findViewById(R.id.local_only)
|
||||
.setOnClickListener(l -> localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
|
||||
|
||||
EditText tagMain = view.findViewById(R.id.tag_main);
|
||||
NachoTextView tagsAny = prepareChipTextView(view.findViewById(R.id.tags_any));
|
||||
NachoTextView tagsAll = prepareChipTextView(view.findViewById(R.id.tags_all));
|
||||
NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none));
|
||||
if (item != null) {
|
||||
tagMain.setText(item.getHashtagName());
|
||||
boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
|
||||
hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
|
||||
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
|
||||
hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
|
||||
if (item.isHashtagLocalOnly()) {
|
||||
localOnlySwitch.setChecked(true);
|
||||
hasAdvanced = true;
|
||||
}
|
||||
if (hasAdvanced) {
|
||||
advancedBtn.setSelected(true);
|
||||
advancedBtn.setText(R.string.sk_advanced_options_hide);
|
||||
tagWrap.setVisibility(View.VISIBLE);
|
||||
divider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
ImageButton btn = view.findViewById(R.id.button);
|
||||
PopupMenu popup = new PopupMenu(ctx, btn);
|
||||
TimelineDefinition.Icon currentIcon = item != null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
btn.setImageResource(currentIcon.iconRes);
|
||||
btn.setTag(currentIcon.ordinal());
|
||||
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
||||
btn.setOnTouchListener(popup.getDragToOpenListener());
|
||||
btn.setOnClickListener(l -> popup.show());
|
||||
|
||||
Menu menu = popup.getMenu();
|
||||
TimelineDefinition.Icon defaultIcon = item != null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
||||
if (!currentIcon.equals(defaultIcon)) {
|
||||
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
||||
}
|
||||
for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) {
|
||||
if (icon.hidden || icon.ordinal() == (int) btn.getTag()) continue;
|
||||
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(ctx, popup);
|
||||
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
||||
btn.setImageResource(icon.iconRes);
|
||||
btn.setTag(menuItem.getItemId());
|
||||
btn.setContentDescription(ctx.getString(icon.nameRes));
|
||||
return true;
|
||||
});
|
||||
|
||||
AlertDialog.Builder builder = new M3AlertDialogBuilder(ctx)
|
||||
.setTitle(item == null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
tagsAny.chipifyAllUnterminatedTokens();
|
||||
tagsAll.chipifyAllUnterminatedTokens();
|
||||
tagsNone.chipifyAllUnterminatedTokens();
|
||||
String name = editText.getText().toString().trim();
|
||||
String mainHashtag = tagMain.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(mainHashtag)) {
|
||||
mainHashtag = name;
|
||||
name = null;
|
||||
}
|
||||
if (TextUtils.isEmpty(mainHashtag)) {
|
||||
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
|
||||
onSave.accept(null);
|
||||
return;
|
||||
}
|
||||
|
||||
TimelineDefinition tl = item != null ? item : TimelineDefinition.ofHashtag(name);
|
||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[(int) btn.getTag()];
|
||||
tl.setIcon(icon);
|
||||
tl.setTitle(name);
|
||||
tl.setTagOptions(
|
||||
mainHashtag,
|
||||
tagsAny.getChipValues(),
|
||||
tagsAll.getChipValues(),
|
||||
tagsNone.getChipValues(),
|
||||
localOnlySwitch.isChecked()
|
||||
);
|
||||
onSave.accept(tl);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {});
|
||||
|
||||
if (onRemove != null) builder.setNeutralButton(R.string.sk_remove, (d, which) -> onRemove.run());
|
||||
|
||||
builder.show();
|
||||
btn.requestFocus();
|
||||
}
|
||||
|
||||
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
@@ -256,60 +414,19 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
});
|
||||
}
|
||||
|
||||
private void onSave(TimelineDefinition tl) {
|
||||
saveTimelines();
|
||||
rebind();
|
||||
}
|
||||
|
||||
private void onRemove() {
|
||||
removeTimeline(getAbsoluteAdapterPosition());
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onClick() {
|
||||
Context ctx = getContext();
|
||||
LinearLayout view = (LinearLayout) getActivity().getLayoutInflater()
|
||||
.inflate(R.layout.edit_timeline, (ViewGroup) itemView, false);
|
||||
|
||||
TextInputFrameLayout inputLayout = view.findViewById(R.id.input);
|
||||
EditText editText = inputLayout.getEditText();
|
||||
editText.setText(item.getCustomTitle());
|
||||
editText.setHint(item.getDefaultTitle(ctx));
|
||||
|
||||
ImageButton btn = view.findViewById(R.id.button);
|
||||
PopupMenu popup = new PopupMenu(ctx, btn);
|
||||
TimelineDefinition.Icon currentIcon = item.getIcon();
|
||||
btn.setImageResource(currentIcon.iconRes);
|
||||
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
||||
btn.setOnTouchListener(popup.getDragToOpenListener());
|
||||
btn.setOnClickListener(l -> popup.show());
|
||||
|
||||
Menu menu = popup.getMenu();
|
||||
TimelineDefinition.Icon defaultIcon = item.getDefaultIcon();
|
||||
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
||||
if (!currentIcon.equals(defaultIcon)) {
|
||||
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
||||
}
|
||||
for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) {
|
||||
if (icon.hidden || icon.equals(item.getIcon())) continue;
|
||||
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(ctx, popup);
|
||||
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
||||
btn.setImageResource(icon.iconRes);
|
||||
btn.setContentDescription(ctx.getString(icon.nameRes));
|
||||
item.setIcon(icon);
|
||||
return true;
|
||||
});
|
||||
|
||||
new M3AlertDialogBuilder(ctx)
|
||||
.setTitle(R.string.sk_edit_timeline)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
item.setTitle(editText.getText().toString().trim());
|
||||
rebind();
|
||||
saveTimelines();
|
||||
})
|
||||
.setNeutralButton(R.string.sk_remove, (d, which) ->
|
||||
removeTimeline(getAbsoluteAdapterPosition()))
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
|
||||
btn.requestFocus();
|
||||
makeTimelineEditor(item, this::onSave, this::onRemove);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -39,8 +39,8 @@ public class FavoritedStatusListFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
protected FilterContext getFilterContext() {
|
||||
return FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag>{
|
||||
private Account account;
|
||||
private String accountID;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||
onDataLoaded(getArguments().getParcelableArrayList("hashtags").stream().map(p->(Hashtag)Parcels.unwrap(p)).collect(Collectors.toList()), false);
|
||||
setTitle(R.string.hashtags);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Hashtag s){
|
||||
return Collections.singletonList(new HashtagStatusDisplayItem(s.name, this, s));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(Hashtag s){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, id, data.stream().filter(h -> Objects.equals(h.name, id)).findAny().map(h -> h.following).orElse(null));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base){
|
||||
return null; // TODO
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
@@ -254,7 +254,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
|
||||
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
|
||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
||||
postsLabel.setText(getResources().getQuantityString(R.plurals.x_posts, (int)(item.account.statusesCount%1000), item.account.statusesCount));
|
||||
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||
@@ -278,7 +278,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
|
||||
actionWrap.setVisibility(View.VISIBLE);
|
||||
acceptWrap.setVisibility(View.GONE);
|
||||
rejectWrap.setVisibility(View.GONE);
|
||||
UiUtils.setRelationshipToActionButton(relationship, actionButton);
|
||||
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +313,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
|
||||
private void onFollowRequestButtonClick(View v) {
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, rel -> {
|
||||
if(getContext()==null) return;
|
||||
itemView.setHasTransientState(false);
|
||||
relationships.put(item.account.id, rel);
|
||||
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();
|
||||
@@ -328,6 +329,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
|
||||
private void onActionButtonClick(View v){
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
|
||||
if(getContext()==null) return;
|
||||
itemView.setHasTransientState(false);
|
||||
relationships.put(item.account.id, rel);
|
||||
rebind();
|
||||
|
||||
@@ -21,7 +21,7 @@ import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
public class FollowedHashtagsFragment extends MastodonRecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String nextMaxID;
|
||||
private String accountID;
|
||||
|
||||
@@ -47,7 +47,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
@@ -24,4 +25,8 @@ public interface HasAccountID {
|
||||
default Optional<Instance> getInstance() {
|
||||
return getSession().getInstance();
|
||||
}
|
||||
|
||||
default AccountLocalPreferences getLocalPrefs() {
|
||||
return AccountSessionManager.get(getAccountID()).getLocalPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
public interface HasElevationOnScrollListener {
|
||||
ElevationOnScrollListener getElevationOnScrollListener();
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import org.joinmastodon.android.api.requests.tags.GetHashtag;
|
||||
import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
|
||||
import org.joinmastodon.android.events.HashtagUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
@@ -35,7 +35,11 @@ import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
private String hashtag;
|
||||
private List<String> any;
|
||||
private List<String> all;
|
||||
private List<String> none;
|
||||
private boolean following;
|
||||
private boolean localOnly;
|
||||
private MenuItem followButton;
|
||||
|
||||
@Override
|
||||
@@ -48,6 +52,10 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
super.onAttach(activity);
|
||||
updateTitle(getArguments().getString("hashtag"));
|
||||
following=getArguments().getBoolean("following", false);
|
||||
localOnly=getArguments().getBoolean("localOnly", false);
|
||||
any=getArguments().getStringArrayList("any");
|
||||
all=getArguments().getStringArrayList("all");
|
||||
none=getArguments().getStringArrayList("none");
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@@ -118,7 +126,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count)
|
||||
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
@@ -152,12 +160,12 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
|
||||
@Override
|
||||
protected void onSetFabBottomInset(int inset){
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.PUBLIC;
|
||||
protected FilterContext getFilterContext() {
|
||||
return FilterContext.PUBLIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -23,26 +24,27 @@ import androidx.annotation.Nullable;
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
||||
import org.joinmastodon.android.events.NotificationReceivedEvent;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -59,42 +61,38 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
private FragmentRootLinearLayout content;
|
||||
private HomeTabFragment homeTabFragment;
|
||||
private NotificationsFragment notificationsFragment;
|
||||
private DiscoverFragment searchFragment;
|
||||
private DiscoverFragment discoverFragment;
|
||||
private ProfileFragment profileFragment;
|
||||
private TabBar tabBar;
|
||||
private View tabBarWrap;
|
||||
private ImageView tabBarAvatar;
|
||||
private ImageView notificationTabIcon;
|
||||
@IdRes
|
||||
private int currentTab=R.id.tab_home;
|
||||
private TextView notificationsBadge;
|
||||
|
||||
private String accountID;
|
||||
private boolean isPleroma;
|
||||
private boolean isAkkoma;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
E.register(this);
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(R.string.sk_app_name);
|
||||
isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
|
||||
.map(Instance::isAkkoma)
|
||||
.orElse(false);
|
||||
isAkkoma = getInstance().map(Instance::isAkkoma).orElse(false);
|
||||
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
|
||||
// TODO: clean up
|
||||
if(savedInstanceState==null){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
homeTabFragment=new HomeTabFragment();
|
||||
homeTabFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("disableDiscover", isPleroma);
|
||||
args.putBoolean("disableDiscover", isAkkoma);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
searchFragment=new DiscoverFragment();
|
||||
searchFragment.setArguments(args);
|
||||
discoverFragment=new DiscoverFragment();
|
||||
discoverFragment.setArguments(args);
|
||||
notificationsFragment=new NotificationsFragment();
|
||||
notificationsFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
@@ -104,6 +102,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
profileFragment.setArguments(args);
|
||||
}
|
||||
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -121,24 +126,47 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
tabBar.setListeners(this::onTabSelected, this::onTabLongClick);
|
||||
tabBarWrap=content.findViewById(R.id.tabbar_wrap);
|
||||
|
||||
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava);
|
||||
tabBarAvatar.setOutlineProvider(new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setOval(0, 0, view.getWidth(), view.getHeight());
|
||||
// this one's for the pill haters (https://m3.material.io/components/navigation-bar/overview)
|
||||
if(GlobalUserPreferences.disableM3PillActiveIndicator){
|
||||
tabBar.findViewById(R.id.tab_home_pill).setBackground(null);
|
||||
tabBar.findViewById(R.id.tab_search_pill).setBackground(null);
|
||||
tabBar.findViewById(R.id.tab_notifications_pill).setBackground(null);
|
||||
tabBar.findViewById(R.id.tab_profile_pill).setBackgroundResource(R.drawable.bg_tab_profile);
|
||||
|
||||
View[] tabs={
|
||||
tabBar.findViewById(R.id.tab_home),
|
||||
tabBar.findViewById(R.id.tab_search),
|
||||
tabBar.findViewById(R.id.tab_notifications),
|
||||
tabBar.findViewById(R.id.tab_profile)
|
||||
};
|
||||
|
||||
for(View tab : tabs){
|
||||
tab.setBackgroundResource(R.drawable.bg_tabbar_tab_ripple);
|
||||
((RippleDrawable) tab.getBackground())
|
||||
.setRadius(V.dp(GlobalUserPreferences.showNavigationLabels ? 56 : 42));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(!GlobalUserPreferences.showNavigationLabels){
|
||||
tabBar.findViewById(R.id.tab_home_label).setVisibility(View.GONE);
|
||||
tabBar.findViewById(R.id.tab_search_label).setVisibility(View.GONE);
|
||||
tabBar.findViewById(R.id.tab_notifications_label).setVisibility(View.GONE);
|
||||
tabBar.findViewById(R.id.tab_profile_label).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava);
|
||||
tabBarAvatar.setOutlineProvider(OutlineProviders.OVAL);
|
||||
tabBarAvatar.setClipToOutline(true);
|
||||
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||
ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28)));
|
||||
ViewImageLoader.loadWithoutAnimation(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
|
||||
|
||||
notificationTabIcon=content.findViewById(R.id.tab_notifications);
|
||||
updateNotificationBadge();
|
||||
notificationsBadge=tabBar.findViewById(R.id.notifications_badge);
|
||||
notificationsBadge.setVisibility(View.GONE);
|
||||
|
||||
if(savedInstanceState==null){
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment)
|
||||
.add(me.grishka.appkit.R.id.fragment_wrap, searchFragment).hide(searchFragment)
|
||||
.add(me.grishka.appkit.R.id.fragment_wrap, discoverFragment).hide(discoverFragment)
|
||||
.add(me.grishka.appkit.R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment)
|
||||
.add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment)
|
||||
.commit();
|
||||
@@ -165,7 +193,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
if(savedInstanceState==null) return;
|
||||
homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment");
|
||||
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
|
||||
discoverFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
|
||||
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
||||
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
||||
currentTab=savedInstanceState.getInt("selectedTab");
|
||||
@@ -173,7 +201,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
Fragment current=fragmentForTab(currentTab);
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.hide(homeTabFragment)
|
||||
.hide(searchFragment)
|
||||
.hide(discoverFragment)
|
||||
.hide(notificationsFragment)
|
||||
.hide(profileFragment)
|
||||
.show(current)
|
||||
@@ -189,7 +217,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
return currentTab!=R.id.tab_profile && !UiUtils.isDarkTheme();
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -201,14 +229,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(24)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0);
|
||||
homeTabFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
searchFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
discoverFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
notificationsFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
profileFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
}
|
||||
@@ -217,7 +245,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
if(tab==R.id.tab_home){
|
||||
return homeTabFragment;
|
||||
}else if(tab==R.id.tab_search){
|
||||
return searchFragment;
|
||||
return discoverFragment;
|
||||
}else if(tab==R.id.tab_notifications){
|
||||
return notificationsFragment;
|
||||
}else if(tab==R.id.tab_profile){
|
||||
@@ -235,11 +263,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
private void onTabSelected(@IdRes int tab){
|
||||
Fragment newFragment=fragmentForTab(tab);
|
||||
if(tab==currentTab){
|
||||
if (tab == R.id.tab_search)
|
||||
searchFragment.onSelect();
|
||||
else if(newFragment instanceof ScrollableToTop scrollable)
|
||||
scrollable.scrollToTop();
|
||||
if(tab==currentTab && newFragment instanceof ScrollableToTop scrollable) {
|
||||
scrollable.scrollToTop();
|
||||
return;
|
||||
}
|
||||
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
||||
@@ -247,7 +272,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab();
|
||||
currentTab=tab;
|
||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
|
||||
}
|
||||
|
||||
private void maybeTriggerLoading(Fragment newFragment){
|
||||
@@ -258,7 +282,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
((DiscoverFragment) newFragment).loadData();
|
||||
}else if(newFragment instanceof NotificationsFragment){
|
||||
((NotificationsFragment) newFragment).loadData();
|
||||
// TODO make an interface?
|
||||
NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
|
||||
for (StatusBarNotification notification : nm.getActiveNotifications()) {
|
||||
if (accountID.equals(notification.getTag())) {
|
||||
@@ -276,6 +299,11 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
}
|
||||
new AccountSwitcherSheet(getActivity(), this).show();
|
||||
return true;
|
||||
} else if(tab==R.id.tab_search){
|
||||
tabBar.selectTab(R.id.tab_search);
|
||||
onTabSelected(R.id.tab_search);
|
||||
discoverFragment.openSearch();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -285,7 +313,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
if(currentTab==R.id.tab_profile)
|
||||
if (profileFragment.onBackPressed()) return true;
|
||||
if(currentTab==R.id.tab_search)
|
||||
if (searchFragment.onBackPressed()) return true;
|
||||
if (discoverFragment.onBackPressed()) return true;
|
||||
if (currentTab!=R.id.tab_home) {
|
||||
tabBar.selectTab(R.id.tab_home);
|
||||
onTabSelected(R.id.tab_home);
|
||||
@@ -300,52 +328,79 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("selectedTab", currentTab);
|
||||
if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment);
|
||||
if (searchFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment);
|
||||
if (discoverFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", discoverFragment);
|
||||
if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
|
||||
if (profileFragment.isAdded()) getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
|
||||
}
|
||||
|
||||
public void updateNotificationBadge() {
|
||||
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||
Optional<Instance> instance = session.getInstance();
|
||||
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
|
||||
|
||||
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
|
||||
.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Notification> notifications) {
|
||||
if (notifications.size() > 0) {
|
||||
try {
|
||||
long newestId = Long.parseLong(notifications.get(0).id);
|
||||
long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId);
|
||||
setNotificationBadge(newestId > lastSeenId);
|
||||
} catch (Exception ignored) {
|
||||
setNotificationBadge(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
setNotificationBadge(false);
|
||||
}
|
||||
}).exec(accountID);
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
reloadNotificationsForUnreadCount();
|
||||
}
|
||||
|
||||
public void setNotificationBadge(boolean badge) {
|
||||
notificationTabIcon.setImageResource(badge
|
||||
? R.drawable.ic_fluent_alert_28_selector_badged
|
||||
: R.drawable.ic_fluent_alert_28_selector);
|
||||
public void reloadNotificationsForUnreadCount(){
|
||||
List<Notification>[] notifications=new List[]{null};
|
||||
String[] marker={null};
|
||||
|
||||
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||
marker[0]=m;
|
||||
if(notifications[0]!=null){
|
||||
updateUnreadCount(notifications[0], marker[0]);
|
||||
}
|
||||
});
|
||||
|
||||
AccountSessionManager.get(accountID).getCacheController().getNotifications(null, 40, false, false, true, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
notifications[0]=result.items;
|
||||
if(marker[0]!=null)
|
||||
updateUnreadCount(notifications[0], marker[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private void updateUnreadCount(List<Notification> notifications, String marker){
|
||||
if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){
|
||||
V.setVisibilityAnimated(notificationsBadge, View.GONE);
|
||||
}else{
|
||||
V.setVisibilityAnimated(notificationsBadge, View.VISIBLE);
|
||||
if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){
|
||||
notificationsBadge.setText(String.format("%d+", notifications.size()));
|
||||
}else{
|
||||
int count=0;
|
||||
for(Notification n:notifications){
|
||||
if(n.id.equals(marker))
|
||||
break;
|
||||
count++;
|
||||
}
|
||||
notificationsBadge.setText(String.format("%d", count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onNotificationReceived(NotificationReceivedEvent notificationReceivedEvent) {
|
||||
if (notificationReceivedEvent.account.equals(accountID)) setNotificationBadge(true);
|
||||
public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
if(ev.clearUnread)
|
||||
V.setVisibilityAnimated(notificationsBadge, View.GONE);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
|
||||
setNotificationBadge(false);
|
||||
public void onStatusDisplaySettingsChanged(StatusDisplaySettingsChangedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
if(homeTabFragment.getCurrentFragment() instanceof LoaderFragment lf && lf.loaded
|
||||
&& lf instanceof BaseStatusListFragment<?> homeTimelineFragment)
|
||||
homeTimelineFragment.rebuildAllDisplayItems();
|
||||
if(notificationsFragment.getCurrentFragment() instanceof LoaderFragment lf && lf.loaded
|
||||
&& lf instanceof BaseStatusListFragment<?> l)
|
||||
l.rebuildAllDisplayItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -43,10 +44,12 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.announcements.GetAnnouncements;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.HashtagUpdatedEvent;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.Announcement;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
@@ -55,6 +58,7 @@ import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -72,8 +76,9 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent {
|
||||
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent, HasElevationOnScrollListener {
|
||||
private static final int ANNOUNCEMENTS_RESULT = 654;
|
||||
|
||||
private String accountID;
|
||||
@@ -91,7 +96,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
private PopupMenu switcherPopup;
|
||||
private final Map<Integer, ListTimeline> listItems = new HashMap<>();
|
||||
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
|
||||
private List<TimelineDefinition> timelineDefinitions;
|
||||
private List<TimelineDefinition> timelinesList;
|
||||
private int count;
|
||||
private Fragment[] fragments;
|
||||
private FrameLayout[] tabViews;
|
||||
@@ -102,19 +107,20 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
private View overflowActionView = null;
|
||||
private boolean announcementsBadged, settingsBadged;
|
||||
private ImageButton fab;
|
||||
private ElevationOnScrollListener elevationOnScrollListener;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
E.register(this);
|
||||
accountID = getArguments().getString("account");
|
||||
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
|
||||
assert timelineDefinitions != null;
|
||||
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
|
||||
count = timelineDefinitions.size();
|
||||
fragments = new Fragment[count];
|
||||
tabViews = new FrameLayout[count];
|
||||
timelines = new TimelineDefinition[count];
|
||||
timelinesList=AccountSessionManager.get(accountID).getLocalPreferences().timelines;
|
||||
assert timelinesList!=null;
|
||||
if(timelinesList.isEmpty()) timelinesList=List.of(TimelineDefinition.HOME_TIMELINE);
|
||||
count=timelinesList.size();
|
||||
fragments=new Fragment[count];
|
||||
tabViews=new FrameLayout[count];
|
||||
timelines=new TimelineDefinition[count];
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,7 +131,11 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
FragmentRootLinearLayout rootView = new FragmentRootLinearLayout(getContext());
|
||||
rootView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
FrameLayout view = new FrameLayout(getContext());
|
||||
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
rootView.addView(view);
|
||||
inflater.inflate(R.layout.compose_fab, view);
|
||||
fab = view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
@@ -140,8 +150,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
args.putBoolean("__disable_fab", true);
|
||||
args.putBoolean("onlyPosts", true);
|
||||
|
||||
for (int i = 0; i < timelineDefinitions.size(); i++) {
|
||||
TimelineDefinition tl = timelineDefinitions.get(i);
|
||||
for (int i=0; i < timelinesList.size(); i++) {
|
||||
TimelineDefinition tl = timelinesList.get(i);
|
||||
fragments[i] = tl.getFragment();
|
||||
timelines[i] = tl;
|
||||
}
|
||||
@@ -168,7 +178,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
overflowActionView.setOnClickListener(l -> overflowPopup.show());
|
||||
overflowActionView.setOnTouchListener(overflowPopup.getDragToOpenListener());
|
||||
|
||||
return view;
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -243,6 +253,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
});
|
||||
}
|
||||
|
||||
elevationOnScrollListener = new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar());
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
updateUpdateState(GithubSelfUpdater.getInstance().getState());
|
||||
}
|
||||
@@ -289,6 +301,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
public ElevationOnScrollListener getElevationOnScrollListener() {
|
||||
return elevationOnScrollListener;
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) {
|
||||
l.onFabClick(v);
|
||||
@@ -466,10 +482,19 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
&& fabulous.isScrolling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (elevationOnScrollListener != null) elevationOnScrollListener.setViews(getToolbar());
|
||||
}
|
||||
|
||||
private void updateSwitcherIcon(int i) {
|
||||
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
||||
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
||||
showFab();
|
||||
if (elevationOnScrollListener != null && getCurrentFragment() instanceof IsOnTop f) {
|
||||
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -484,7 +509,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
getToolbar().post(() -> overflowPopup.show());
|
||||
return true;
|
||||
} else if (id == R.id.settings || id == R.id.settings_action) {
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
} else if (id == R.id.announcements || id == R.id.announcements_action) {
|
||||
Nav.goForResult(getActivity(), AnnouncementsFragment.class, args, ANNOUNCEMENTS_RESULT, this);
|
||||
} else if (id == R.id.edit_timelines) {
|
||||
@@ -492,6 +517,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
} else if ((list = listItems.get(id)) != null) {
|
||||
args.putString("listID", list.id);
|
||||
args.putString("listTitle", list.title);
|
||||
args.putBoolean("listIsExclusive", list.exclusive);
|
||||
if (list.repliesPolicy != null) args.putInt("repliesPolicy", list.repliesPolicy.ordinal());
|
||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||
} else if ((hashtag = hashtagsItems.get(id)) != null) {
|
||||
@@ -632,8 +658,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
@Override
|
||||
protected void onShown() {
|
||||
super.onShown();
|
||||
Object pinnedTimelines = GlobalUserPreferences.pinnedTimelines.get(accountID);
|
||||
if (pinnedTimelines != null && timelineDefinitions != pinnedTimelines) UiUtils.restartApp();
|
||||
Object timelines = AccountSessionManager.get(accountID).getLocalPreferences().timelines;
|
||||
if (timelines != null && timelinesList!= timelines) UiUtils.restartApp();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -697,6 +723,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
return hashtagsItems.values();
|
||||
}
|
||||
|
||||
public Fragment getCurrentFragment() {
|
||||
return fragments[pager.getCurrentItem()];
|
||||
}
|
||||
|
||||
public ImageButton getFab() {
|
||||
return fab;
|
||||
}
|
||||
|
||||
@@ -11,11 +11,13 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
@@ -49,8 +51,9 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
}
|
||||
|
||||
private boolean typeFilterPredicate(Status s) {
|
||||
return (GlobalUserPreferences.showReplies || s.inReplyToId == null) &&
|
||||
(GlobalUserPreferences.showBoosts || s.reblog == null);
|
||||
AccountLocalPreferences lp=getLocalPrefs();
|
||||
return (lp.showReplies || s.inReplyToId == null) &&
|
||||
(lp.showBoosts || s.reblog == null);
|
||||
}
|
||||
|
||||
private List<Status> filterPosts(List<Status> items) {
|
||||
@@ -66,8 +69,8 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if (getActivity() == null) return;
|
||||
List<Status> filteredItems = filterPosts(result.items);
|
||||
onDataLoaded(filteredItems, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
onDataLoaded(filteredItems, !result.items.isEmpty());
|
||||
if(result.isFromCache())
|
||||
loadNewPosts();
|
||||
}
|
||||
@@ -110,7 +113,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
new SaveMarkers(topPostID, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SaveMarkers.Response result){
|
||||
public void onSuccess(TimelineMarkers result){
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,8 +126,8 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
}
|
||||
}
|
||||
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
public void onStatusCreated(Status status){
|
||||
prependItems(Collections.singletonList(status), true);
|
||||
}
|
||||
|
||||
private void loadNewPosts(){
|
||||
@@ -134,7 +137,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
// we'll get the currently topmost post as last in the response. This way we know there's no gap
|
||||
// between the existing and newly loaded parts of the timeline.
|
||||
String sinceID=data.size()>1 ? data.get(1).id : "1";
|
||||
currentRequest=new GetHomeTimeline(null, null, 20, sinceID)
|
||||
currentRequest=new GetHomeTimeline(null, null, 20, sinceID, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
@@ -151,8 +154,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
|
||||
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, getFilterContext());
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
if (parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
|
||||
@@ -169,7 +171,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
.exec(accountID);
|
||||
|
||||
if (parent.getParentFragment() instanceof HomeFragment homeFragment) {
|
||||
homeFragment.updateNotificationBadge();
|
||||
homeFragment.reloadNotificationsForUnreadCount();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +184,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
V.setVisibilityAnimated(item.text, View.GONE);
|
||||
GapStatusDisplayItem gap=item.getItem();
|
||||
dataLoading=true;
|
||||
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null)
|
||||
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
@@ -239,6 +241,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
insertedPosts.add(s);
|
||||
}
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(insertedPosts, getFilterContext());
|
||||
if(targetList.isEmpty()){
|
||||
// oops. We didn't add new posts, but at least we know there are none.
|
||||
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||
@@ -285,8 +288,8 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.HOME;
|
||||
protected FilterContext getFilterContext() {
|
||||
return FilterContext.HOME;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,13 +18,13 @@ import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ListTimelineEditor;
|
||||
import org.joinmastodon.android.ui.views.ListEditor;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
@@ -36,12 +36,12 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
|
||||
public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
private String listID;
|
||||
private String listTitle;
|
||||
@Nullable
|
||||
private ListTimeline.RepliesPolicy repliesPolicy;
|
||||
private boolean exclusive;
|
||||
|
||||
@Override
|
||||
protected boolean wantsComposeButton() {
|
||||
@@ -54,6 +54,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
Bundle args = getArguments();
|
||||
listID = args.getString("listID");
|
||||
listTitle = args.getString("listTitle");
|
||||
exclusive = args.getBoolean("listIsExclusive");
|
||||
repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
||||
|
||||
setTitle(listTitle);
|
||||
@@ -88,8 +89,8 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (super.onOptionsItemSelected(item)) return true;
|
||||
if (item.getItemId() == R.id.edit) {
|
||||
ListTimelineEditor editor = new ListTimelineEditor(getContext());
|
||||
editor.applyList(listTitle, repliesPolicy);
|
||||
ListEditor editor = new ListEditor(getContext());
|
||||
editor.applyList(listTitle, exclusive, repliesPolicy);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_edit_list_title)
|
||||
.setIcon(R.drawable.ic_fluent_people_28_regular)
|
||||
@@ -97,14 +98,15 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
String newTitle = editor.getTitle().trim();
|
||||
setTitle(newTitle);
|
||||
new UpdateList(listID, newTitle, editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
new UpdateList(listID, newTitle, editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
if (getActivity() == null) return;
|
||||
setTitle(list.title);
|
||||
listTitle = list.title;
|
||||
repliesPolicy = list.repliesPolicy;
|
||||
E.post(new ListUpdatedCreatedEvent(listID, listTitle, repliesPolicy));
|
||||
exclusive = list.exclusive;
|
||||
E.post(new ListUpdatedCreatedEvent(listID, listTitle, exclusive, repliesPolicy));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -127,12 +129,12 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
|
||||
@Override
|
||||
protected TimelineDefinition makeTimelineDefinition() {
|
||||
return TimelineDefinition.ofList(listID, listTitle);
|
||||
return TimelineDefinition.ofList(listID, listTitle, exclusive);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count) {
|
||||
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null)
|
||||
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<Status> result) {
|
||||
@@ -165,8 +167,8 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.HOME;
|
||||
protected FilterContext getFilterContext() {
|
||||
return FilterContext.HOME;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.views.ListTimelineEditor;
|
||||
import org.joinmastodon.android.ui.views.ListEditor;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -42,7 +42,7 @@ import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ListsFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private String profileAccountId;
|
||||
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||
@@ -80,7 +80,7 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,18 +91,18 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.create) {
|
||||
ListTimelineEditor editor = new ListTimelineEditor(getContext());
|
||||
ListEditor editor = new ListEditor(getContext());
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_create_list_title)
|
||||
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
||||
.setView(editor)
|
||||
.setPositiveButton(R.string.sk_create, (d, which) ->
|
||||
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
new CreateList(editor.getTitle(), editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
data.add(0, list);
|
||||
adapter.notifyItemRangeInserted(0, 1);
|
||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
|
||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.exclusive, list.repliesPolicy));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -185,6 +185,7 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
|
||||
if (item.id.equals(event.id)) {
|
||||
item.title = event.title;
|
||||
item.repliesPolicy = event.repliesPolicy;
|
||||
item.exclusive = event.exclusive;
|
||||
adapter.notifyItemChanged(i);
|
||||
break;
|
||||
}
|
||||
@@ -242,7 +243,9 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
title.setText(item.title);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(
|
||||
item.exclusive ? R.drawable.ic_fluent_rss_24_regular : R.drawable.ic_fluent_people_24_regular
|
||||
), null, null, null);
|
||||
if (profileAccountId != null) {
|
||||
Boolean checked = userInList.get(item.id);
|
||||
listToggle.setVisibility(View.VISIBLE);
|
||||
@@ -263,6 +266,7 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
|
||||
args.putString("account", accountID);
|
||||
args.putString("listID", item.id);
|
||||
args.putString("listTitle", item.title);
|
||||
args.putBoolean("listIsExclusive", item.exclusive);
|
||||
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
|
||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T>{
|
||||
protected ElevationOnScrollListener elevationOnScrollListener;
|
||||
|
||||
public MastodonRecyclerFragment(int perPage){
|
||||
super(perPage);
|
||||
}
|
||||
|
||||
public MastodonRecyclerFragment(int layout, int perPage){
|
||||
super(layout, perPage);
|
||||
}
|
||||
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
Toolbar toolbar=getToolbar();
|
||||
return toolbar!=null ? Collections.singletonList(toolbar) : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if (getParentFragment() instanceof HasElevationOnScrollListener elevator)
|
||||
list.addOnScrollListener(elevator.getElevationOnScrollListener());
|
||||
else if(wantsElevationOnScrollEffect())
|
||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
|
||||
if(refreshLayout!=null)
|
||||
setRefreshLayoutColors(refreshLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
if(elevationOnScrollListener!=null){
|
||||
elevationOnScrollListener.setViews(getViewsForElevationEffect());
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean wantsElevationOnScrollEffect(){
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<T> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public static void setRefreshLayoutColors(SwipeRefreshLayout l) {
|
||||
List<Integer> colors = new ArrayList<>(Arrays.asList(
|
||||
UiUtils.isDarkTheme() ? R.color.primary_200 : R.color.primary_600,
|
||||
UiUtils.isDarkTheme() ? R.color.red_primary_200 : R.color.red_primary_600,
|
||||
UiUtils.isDarkTheme() ? R.color.green_primary_200 : R.color.green_primary_600,
|
||||
UiUtils.isDarkTheme() ? R.color.blue_primary_200 : R.color.blue_primary_600,
|
||||
UiUtils.isDarkTheme() ? R.color.purple_200 : R.color.purple_600
|
||||
));
|
||||
int primary = UiUtils.getThemeColorRes(l.getContext(),
|
||||
UiUtils.isDarkTheme() ? R.attr.colorPrimary200 : R.attr.colorPrimary600);
|
||||
if (!colors.contains(primary)) colors.add(0, primary);
|
||||
int offset = colors.indexOf(primary);
|
||||
int[] sorted = new int[colors.size()];
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
sorted[i] = colors.get((i + offset) % colors.size());
|
||||
}
|
||||
l.setColorSchemeResources(sorted);
|
||||
int colorBackground=UiUtils.getThemeColor(l.getContext(), R.attr.colorM3Background);
|
||||
int colorPrimary=UiUtils.getThemeColor(l.getContext(), R.attr.colorM3Primary);
|
||||
l.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,15 @@ import androidx.annotation.CallSuper;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
|
||||
public abstract class MastodonToolbarFragment extends ToolbarFragment{
|
||||
|
||||
public MastodonToolbarFragment(){
|
||||
super();
|
||||
}
|
||||
|
||||
protected MastodonToolbarFragment(int layout){
|
||||
super(layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -24,6 +25,9 @@ import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.FollowRequestHandledEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
@@ -31,6 +35,8 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
@@ -38,15 +44,19 @@ import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent {
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent, HasElevationOnScrollListener {
|
||||
|
||||
private TabLayout tabLayout;
|
||||
TabLayout tabLayout;
|
||||
private ViewPager2 pager;
|
||||
private FrameLayout[] tabViews;
|
||||
private View tabsDivider;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
|
||||
String unreadMarker, realUnreadMarker;
|
||||
private MenuItem markAllReadItem;
|
||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
||||
private ElevationOnScrollListener elevationOnScrollListener;
|
||||
|
||||
private String accountID;
|
||||
@Override
|
||||
@@ -72,11 +82,19 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
setTitle(R.string.notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShown() {
|
||||
super.onShown();
|
||||
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.notifications, menu);
|
||||
menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications);
|
||||
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests);
|
||||
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||
updateMarkAllReadButton();
|
||||
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests, R.id.mark_all_read);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,15 +111,40 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.mark_all_read) {
|
||||
markAsRead();
|
||||
if (getCurrentFragment() instanceof NotificationsListFragment nlf) {
|
||||
nlf.resetUnreadBackground();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void markAsRead(){
|
||||
if(allNotificationsFragment.getData().isEmpty()) return;
|
||||
String id=allNotificationsFragment.getData().get(0).id;
|
||||
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||
new SaveMarkers(null, id).exec(accountID);
|
||||
if (allNotificationsFragment.isInstanceAkkoma()) {
|
||||
new PleromaMarkNotificationsRead(id).exec(accountID);
|
||||
}
|
||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||
realUnreadMarker=id;
|
||||
updateMarkAllReadButton();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateMarkAllReadButton(){
|
||||
markAllReadItem.setVisible(!allNotificationsFragment.getData().isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(allNotificationsFragment.getData().get(0).id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false);
|
||||
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
tabsDivider=view.findViewById(R.id.tabs_divider);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
UiUtils.reduceSwipeSensitivity(pager);
|
||||
|
||||
@@ -119,7 +162,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
}
|
||||
|
||||
tabLayout.setTabTextSize(V.dp(16));
|
||||
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {}
|
||||
@@ -139,6 +182,8 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
||||
@Override
|
||||
public void onPageSelected(int position){
|
||||
if (elevationOnScrollListener != null && getCurrentFragment() instanceof IsOnTop f)
|
||||
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
|
||||
if(position==0)
|
||||
return;
|
||||
Fragment _page=getFragmentForPage(position);
|
||||
@@ -176,7 +221,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
case 1 -> R.string.mentions;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
tab.view.textView.setAllCaps(true);
|
||||
}
|
||||
});
|
||||
tabLayoutMediator.attach();
|
||||
@@ -184,6 +228,28 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
elevationOnScrollListener = new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar(), tabLayout);
|
||||
elevationOnScrollListener.setDivider(tabsDivider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (elevationOnScrollListener == null) return;
|
||||
elevationOnScrollListener.setViews(getToolbar(), tabLayout);
|
||||
if (getCurrentFragment() instanceof IsOnTop f) {
|
||||
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElevationOnScrollListener getElevationOnScrollListener() {
|
||||
return elevationOnScrollListener;
|
||||
}
|
||||
|
||||
public void refreshFollowRequestsBadge() {
|
||||
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
|
||||
@Override
|
||||
@@ -228,6 +294,10 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
};
|
||||
}
|
||||
|
||||
public Fragment getCurrentFragment() {
|
||||
return getFragmentForPage(pager.getCurrentItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@@ -10,50 +13,44 @@ import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification> {
|
||||
private boolean onlyMentions;
|
||||
private boolean onlyPosts;
|
||||
private String maxID;
|
||||
private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS);
|
||||
private boolean reloadingFromCache;
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
|
||||
@Override
|
||||
protected boolean wantsComposeButton() {
|
||||
@@ -64,6 +61,13 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
E.register(this);
|
||||
if(savedInstanceState!=null){
|
||||
onlyMentions=savedInstanceState.getBoolean("onlyMentions", false);
|
||||
onlyPosts=savedInstanceState.getBoolean("onlyPosts", false);
|
||||
}
|
||||
if (onlyPosts) {
|
||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS, accountID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,59 +81,35 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
super.onAttach(activity);
|
||||
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
||||
onlyPosts=getArguments().getBoolean("onlyPosts", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
super.onRefresh();
|
||||
if (getParentFragment() instanceof NotificationsFragment notificationsFragment) {
|
||||
notificationsFragment.refreshFollowRequestsBadge();
|
||||
}
|
||||
setTitle(R.string.notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
Account reportTarget = n.report == null ? null : n.report.targetAccount == null ? null :
|
||||
n.report.targetAccount;
|
||||
Emoji emoji = new Emoji();
|
||||
if(n.emojiUrl!=null){
|
||||
emoji.shortcode=n.emoji.substring(1,n.emoji.length()-1);
|
||||
emoji.url=n.emojiUrl;
|
||||
emoji.staticUrl=n.emojiUrl;
|
||||
emoji.visibleInPicker=false;
|
||||
NotificationHeaderStatusDisplayItem titleItem;
|
||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||
titleItem=null;
|
||||
}else{
|
||||
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
||||
}
|
||||
if (n.type == Notification.Type.FOLLOW_REQUEST) {
|
||||
ArrayList<StatusDisplayItem> items = new ArrayList<>();
|
||||
items.add(titleItem);
|
||||
items.add(new AccountCardStatusDisplayItem(n.id, this, n.account, n));
|
||||
return items;
|
||||
}
|
||||
String extraText=switch(n.type){
|
||||
case FOLLOW -> getString(R.string.user_followed_you);
|
||||
case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request);
|
||||
case MENTION, STATUS -> null;
|
||||
case REBLOG -> getString(R.string.notification_boosted);
|
||||
case FAVORITE -> getString(R.string.user_favorited);
|
||||
case POLL -> getString(R.string.poll_ended);
|
||||
case UPDATE -> getString(R.string.sk_post_edited);
|
||||
case SIGN_UP -> getString(R.string.sk_signed_up);
|
||||
case REPORT -> getString(R.string.sk_reported);
|
||||
case REACTION, PLEROMA_EMOJI_REACTION ->
|
||||
n.emoji != null ? getString(R.string.sk_reacted_with, n.emoji) : getString(R.string.sk_reacted);
|
||||
};
|
||||
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, n.emojiUrl!=null ? HtmlParser.parseCustomEmoji(extraText, Collections.singletonList(emoji)) : extraText, n, null) : null;
|
||||
if(n.status!=null){
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS);
|
||||
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET); // | StatusDisplayItem.FLAG_NO_HEADER);
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, null, flags);
|
||||
if(titleItem!=null)
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
}else if(titleItem!=null){
|
||||
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this,
|
||||
reportTarget != null ? reportTarget : n.account, n);
|
||||
TextStatusDisplayItem text = n.report != null && !TextUtils.isEmpty(n.report.comment) ?
|
||||
new TextStatusDisplayItem(n.id, n.report.comment, this,
|
||||
Status.ofFake(n.id, n.report.comment, n.createdAt), true) :
|
||||
null;
|
||||
return text == null ? Arrays.asList(titleItem, card) : Arrays.asList(titleItem, text, card);
|
||||
return Collections.singletonList(titleItem);
|
||||
}else{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(Notification s){
|
||||
if(!knownAccounts.containsKey(s.account.id))
|
||||
@@ -144,52 +124,38 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Notification>> result){
|
||||
if (getActivity() == null) return;
|
||||
if(refreshing)
|
||||
relationships.clear();
|
||||
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
||||
Set<String> needRelationships=result.items.stream()
|
||||
.filter(ntf->ntf.status==null && !relationships.containsKey(ntf.account.id))
|
||||
.map(ntf->ntf.account.id)
|
||||
.collect(Collectors.toSet());
|
||||
loadRelationships(needRelationships);
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
maxID=result.maxID;
|
||||
|
||||
Markers markers = AccountSessionManager.getInstance().getAccount(accountID).markers;
|
||||
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && markers.notifications != null){
|
||||
E.post(new AllNotificationsSeenEvent());
|
||||
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).markers
|
||||
.notifications.lastReadId = result.items.get(0).id;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
|
||||
if (isInstanceAkkoma()) {
|
||||
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
|
||||
}
|
||||
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
||||
reloadingFromCache=false;
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
nf.updateMarkAllReadButton();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRelationshipsLoaded(){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof AccountCardStatusDisplayItem.Holder accountHolder)
|
||||
accountHolder.rebind();
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!dataLoading){
|
||||
if(onlyMentions){
|
||||
refresh();
|
||||
}else{
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
// if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
// loadData();
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
resetUnreadBackground();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -205,7 +171,50 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
if (onlyPosts) bannerHelper.maybeAddBanner(contentWrap);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Paint paint=new Paint();
|
||||
private Rect tmpRect=new Rect();
|
||||
|
||||
{
|
||||
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3SurfaceVariant));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
if(TextUtils.isEmpty(nf.unreadMarker))
|
||||
return;
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
|
||||
String itemID=holder.getItemID();
|
||||
if(ObjectIdComparator.INSTANCE.compare(itemID, nf.unreadMarker)>0){
|
||||
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
c.drawRect(tmpRect, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
ArrayList<View> views=new ArrayList<>(super.getViewsForElevationEffect());
|
||||
views.add(nf.tabLayout);
|
||||
return views;
|
||||
} else {
|
||||
return super.getViewsForElevationEffect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean("onlyMentions", onlyMentions);
|
||||
outState.putBoolean("onlyPosts", onlyPosts);
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
@@ -238,6 +247,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
if (n.status == null) continue;
|
||||
if(n.status.getContentStatus().id.equals(ev.id)){
|
||||
n.status.getContentStatus().update(ev);
|
||||
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
||||
@@ -252,6 +262,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
if (n.status == null) continue;
|
||||
if(n.status.getContentStatus().id.equals(ev.id)){
|
||||
n.status.getContentStatus().update(ev);
|
||||
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,6 +300,40 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
|
||||
}
|
||||
|
||||
void resetUnreadBackground(){
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
nf.unreadMarker=nf.realUnreadMarker;
|
||||
list.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){
|
||||
super.onRefresh();
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
if (!onlyMentions && !onlyPosts) nf.markAsRead();
|
||||
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||
nf.unreadMarker=nf.realUnreadMarker=m;
|
||||
nf.updateMarkAllReadButton();
|
||||
});
|
||||
}
|
||||
resetUnreadBackground();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
if (bannerHelper == null) return super.getAdapter();
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
bannerHelper.maybeAddBanner(list, adapter);
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma()
|
||||
|
||||
@@ -7,20 +7,21 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class PinnableStatusListFragment extends StatusListFragment {
|
||||
protected List<TimelineDefinition> pinnedTimelines;
|
||||
protected List<TimelineDefinition> timelines;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)));
|
||||
timelines=new ArrayList<>(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,7 +31,7 @@ public abstract class PinnableStatusListFragment extends StatusListFragment {
|
||||
}
|
||||
|
||||
protected boolean isPinned() {
|
||||
return pinnedTimelines.contains(makeTimelineDefinition());
|
||||
return timelines.contains(makeTimelineDefinition());
|
||||
}
|
||||
|
||||
protected void updatePinButton(MenuItem pin) {
|
||||
@@ -57,11 +58,12 @@ public abstract class PinnableStatusListFragment extends StatusListFragment {
|
||||
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
|
||||
TimelineDefinition def = makeTimelineDefinition();
|
||||
boolean pinned = isPinned();
|
||||
if (pinned) pinnedTimelines.remove(def);
|
||||
else pinnedTimelines.add(def);
|
||||
if (pinned) timelines.remove(def);
|
||||
else timelines.add(def);
|
||||
Toast.makeText(getContext(), pinned ? R.string.sk_unpinned_timeline : R.string.sk_pinned_timeline, Toast.LENGTH_SHORT).show();
|
||||
GlobalUserPreferences.pinnedTimelines.put(accountID, pinnedTimelines);
|
||||
GlobalUserPreferences.save();
|
||||
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
|
||||
prefs.timelines=new ArrayList<>(timelines);
|
||||
prefs.save();
|
||||
updatePinButton(pin);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.os.Bundle;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -41,8 +41,8 @@ public class PinnedPostsListFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
protected FilterContext getFilterContext() {
|
||||
return FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -43,16 +44,15 @@ import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareFragment{
|
||||
private static final int MAX_FIELDS=4;
|
||||
static final int MAX_FIELDS=Integer.MAX_VALUE;
|
||||
|
||||
public UsableRecyclerView list;
|
||||
private List<AccountField> fields=Collections.emptyList();
|
||||
private AboutAdapter adapter;
|
||||
private Paint dividerPaint=new Paint();
|
||||
private boolean isInEditMode;
|
||||
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
|
||||
private RecyclerView.ViewHolder draggedViewHolder;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
private boolean editDirty;
|
||||
|
||||
public void setFields(List<AccountField> fields){
|
||||
this.fields=fields;
|
||||
@@ -74,27 +74,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
|
||||
list.setAdapter(adapter=new AboutAdapter());
|
||||
int pad=V.dp(16);
|
||||
list.setPadding(pad, pad, pad, pad);
|
||||
list.setPadding(0, V.dp(16), 0, 0);
|
||||
list.setClipToPadding(false);
|
||||
dividerPaint.setStyle(Paint.Style.STROKE);
|
||||
dividerPaint.setStrokeWidth(V.dp(1));
|
||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View item=parent.getChildAt(i);
|
||||
int pos=parent.getChildAdapterPosition(item);
|
||||
int draggedPos=draggedViewHolder==null ? -1 : draggedViewHolder.getAbsoluteAdapterPosition();
|
||||
if(pos<adapter.getItemCount()-1 && pos!=draggedPos && pos!=draggedPos-1){
|
||||
float y=item.getY()+item.getHeight();
|
||||
dividerPaint.setAlpha(Math.round(255*item.getAlpha()));
|
||||
c.drawLine(item.getLeft(), y, item.getRight(), y, dividerPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -103,12 +84,17 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
fields=editableFields;
|
||||
adapter.notifyDataSetChanged();
|
||||
dragHelper.attachToRecyclerView(list);
|
||||
editDirty=false;
|
||||
}
|
||||
|
||||
public List<AccountField> getFields(){
|
||||
return fields;
|
||||
}
|
||||
|
||||
public boolean isEditDirty(){
|
||||
return editDirty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
@@ -183,36 +169,25 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
}
|
||||
|
||||
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
|
||||
protected ShapeDrawable background=new ShapeDrawable();
|
||||
|
||||
public BaseViewHolder(int layout){
|
||||
super(getActivity(), layout, list);
|
||||
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||
itemView.setBackground(background);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountField item){
|
||||
boolean first=getAbsoluteAdapterPosition()==0, last=getAbsoluteAdapterPosition()==adapter.getItemCount()-1;
|
||||
float radius=V.dp(10);
|
||||
float[] rad=new float[8];
|
||||
if(first)
|
||||
rad[0]=rad[1]=rad[2]=rad[3]=radius;
|
||||
if(last)
|
||||
rad[4]=rad[5]=rad[6]=rad[7]=radius;
|
||||
background.setShape(new RoundRectShape(rad, null, null));
|
||||
itemView.invalidateOutline();
|
||||
}
|
||||
}
|
||||
|
||||
private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder{
|
||||
private TextView title;
|
||||
private LinkedTextView value;
|
||||
private final TextView title;
|
||||
private final LinkedTextView value;
|
||||
// private final ImageView verifiedIcon;
|
||||
|
||||
public AboutViewHolder(){
|
||||
super(R.layout.item_profile_about);
|
||||
title=findViewById(R.id.title);
|
||||
value=findViewById(R.id.value);
|
||||
// verifiedIcon=findViewById(R.id.verified_icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -220,20 +195,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
super.onBind(item);
|
||||
title.setText(item.parsedName);
|
||||
value.setText(item.parsedValue);
|
||||
if(item.verifiedAt!=null){
|
||||
background.getPaint().setColor(UiUtils.isDarkTheme() ? 0xFF49595a : 0xFFd7e3da);
|
||||
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
|
||||
value.setTextColor(textColor);
|
||||
value.setLinkTextColor(textColor);
|
||||
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
|
||||
check.setTint(textColor);
|
||||
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
|
||||
}else{
|
||||
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
|
||||
value.setCompoundDrawables(null, null, null, null);
|
||||
}
|
||||
// verifiedIcon.setVisibility(item.verifiedAt!=null ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -251,27 +213,38 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
}
|
||||
|
||||
private class EditableAboutViewHolder extends BaseViewHolder{
|
||||
private EditText title;
|
||||
private EditText value;
|
||||
private final EditText title;
|
||||
private final EditText value;
|
||||
private boolean ignoreTextChange;
|
||||
|
||||
public EditableAboutViewHolder(){
|
||||
super(R.layout.item_profile_about_editable);
|
||||
super(R.layout.onboarding_profile_field);
|
||||
title=findViewById(R.id.title);
|
||||
value=findViewById(R.id.value);
|
||||
value=findViewById(R.id.content);
|
||||
findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
|
||||
dragHelper.startDrag(this);
|
||||
return true;
|
||||
});
|
||||
title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString()));
|
||||
value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString()));
|
||||
findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick);
|
||||
title.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
item.name=e.toString();
|
||||
if(!ignoreTextChange)
|
||||
editDirty=true;
|
||||
}));
|
||||
value.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
item.value=e.toString();
|
||||
if(!ignoreTextChange)
|
||||
editDirty=true;
|
||||
}));
|
||||
findViewById(R.id.delete).setOnClickListener(this::onRemoveRowClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountField item){
|
||||
super.onBind(item);
|
||||
ignoreTextChange=true;
|
||||
title.setText(item.name);
|
||||
value.setText(item.value);
|
||||
ignoreTextChange=false;
|
||||
}
|
||||
|
||||
private void onRemoveRowClick(View v){
|
||||
@@ -323,8 +296,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
}
|
||||
}
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
((BindableViewHolder)viewHolder).rebind();
|
||||
((BindableViewHolder)target).rebind();
|
||||
((BindableViewHolder<?>)viewHolder).rebind();
|
||||
((BindableViewHolder<?>)target).rebind();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -339,7 +312,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
|
||||
viewHolder.itemView.setTag(me.grishka.appkit.R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
|
||||
viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
draggedViewHolder=viewHolder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +319,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
draggedViewHolder=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
|
||||
|
||||
public abstract class RecyclerFragment<T> extends BaseRecyclerFragment<T> {
|
||||
public RecyclerFragment(int perPage) {
|
||||
super(perPage);
|
||||
}
|
||||
|
||||
public RecyclerFragment(int layout, int perPage) {
|
||||
super(layout, perPage);
|
||||
}
|
||||
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if (refreshLayout != null) setRefreshLayoutColors(refreshLayout);
|
||||
}
|
||||
|
||||
public static void setRefreshLayoutColors(SwipeRefreshLayout l) {
|
||||
List<Integer> colors = new ArrayList<>(Arrays.asList(
|
||||
R.color.primary_600,
|
||||
R.color.red_primary_600,
|
||||
R.color.green_primary_600,
|
||||
R.color.blue_primary_600,
|
||||
R.color.purple_600
|
||||
));
|
||||
int primary = UiUtils.getThemeColorRes(l.getContext(), R.attr.colorPrimary600);
|
||||
if (!colors.contains(primary)) colors.add(0, primary);
|
||||
int offset = colors.indexOf(primary);
|
||||
int[] sorted = new int[colors.size()];
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
sorted[i] = colors.get((i + offset) % colors.size());
|
||||
}
|
||||
l.setColorSchemeResources(sorted);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
|
||||
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, null);
|
||||
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, true, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user