Compare commits
444 Commits
v1.1.5+for
...
v1.1.5+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec40488ed1 | ||
|
|
88851a085e | ||
|
|
6e718d6765 | ||
|
|
b26d491eda | ||
|
|
abdbab9d7b | ||
|
|
af1c7194e6 | ||
|
|
8e507e7970 | ||
|
|
3b542730b1 | ||
|
|
b038f81718 | ||
|
|
e1206703cf | ||
|
|
2110861f1b | ||
|
|
4f6476c807 | ||
|
|
eccfa27128 | ||
|
|
460bce6174 | ||
|
|
d40790a85a | ||
|
|
75dc6fd019 | ||
|
|
581e2056f7 | ||
|
|
70d5100419 | ||
|
|
0285d9620e | ||
|
|
f6411052dd | ||
|
|
ff21c0c103 | ||
|
|
443c18ce13 | ||
|
|
7380da88f9 | ||
|
|
a7551ce9d9 | ||
|
|
dbb2c62702 | ||
|
|
eb2385afe4 | ||
|
|
6c63e7b833 | ||
|
|
d8eb1f280b | ||
|
|
82a90d5486 | ||
|
|
78a3c43b06 | ||
|
|
54c23b2d05 | ||
|
|
3fb350abe4 | ||
|
|
2e4bb98bb0 | ||
|
|
0d2d4dd1b2 | ||
|
|
f8b1695c61 | ||
|
|
b456973c6a | ||
|
|
7c4616568b | ||
|
|
1e93360778 | ||
|
|
e7db1fcfc1 | ||
|
|
8be50e126b | ||
|
|
a75611e707 | ||
|
|
1df643fc9a | ||
|
|
1e730df767 | ||
|
|
f4c097704e | ||
|
|
a86035a4ed | ||
|
|
5b57b4ca79 | ||
|
|
5e9dda72b5 | ||
|
|
4121346794 | ||
|
|
3ce24f72d8 | ||
|
|
5ee81a6416 | ||
|
|
0fb7402094 | ||
|
|
4de4617cf5 | ||
|
|
bc3ace42f4 | ||
|
|
7c9437b5d2 | ||
|
|
7d4b82f4ca | ||
|
|
f7ea6fb0dd | ||
|
|
afff61fe8c | ||
|
|
d60c82b21c | ||
|
|
7c9107f229 | ||
|
|
40eb686418 | ||
|
|
bf9f859827 | ||
|
|
cd51bca670 | ||
|
|
2048a49f9b | ||
|
|
ea00117844 | ||
|
|
f8df86ae6b | ||
|
|
924f792f8b | ||
|
|
02c9928a1f | ||
|
|
0fec486ce0 | ||
|
|
c6c90d61b5 | ||
|
|
9147b3b495 | ||
|
|
a9cca7f8db | ||
|
|
e8e8eef42d | ||
|
|
c1f31f3983 | ||
|
|
3df7123599 | ||
|
|
6dd4b202d9 | ||
|
|
71432fb87d | ||
|
|
1a0a09ddae | ||
|
|
94b0c8be08 | ||
|
|
5cb5f426d8 | ||
|
|
2b0b612191 | ||
|
|
6c4424bca4 | ||
|
|
b9e46339cd | ||
|
|
ded00f84f1 | ||
|
|
79b74a1960 | ||
|
|
25e8febc44 | ||
|
|
6cc2885050 | ||
|
|
a68053f3a5 | ||
|
|
deca8df309 | ||
|
|
60edcfee1f | ||
|
|
eb1ab99262 | ||
|
|
e75d350b7a | ||
|
|
e9cfe3dee0 | ||
|
|
753914ca5a | ||
|
|
93e3097993 | ||
|
|
0916eddbb7 | ||
|
|
a4848f001b | ||
|
|
8952cd6f97 | ||
|
|
4a3e6888d6 | ||
|
|
b9bcb62cda | ||
|
|
d3491b5753 | ||
|
|
9e9f9357fd | ||
|
|
ec525bde6d | ||
|
|
ee19410cc6 | ||
|
|
0468ae246e | ||
|
|
9722cd9e12 | ||
|
|
85799a7d93 | ||
|
|
e222559bde | ||
|
|
2f3c7dc8f1 | ||
|
|
d86588bbe2 | ||
|
|
2bf787c8f2 | ||
|
|
82ab8bef56 | ||
|
|
cf024dc85f | ||
|
|
ddebe1b3c0 | ||
|
|
070e5637cc | ||
|
|
bdd3c849e7 | ||
|
|
21073b11d0 | ||
|
|
b4fa74b78f | ||
|
|
ab3a98fd60 | ||
|
|
6e6fdbccd5 | ||
|
|
1764e5f3d1 | ||
|
|
1cc6bf4971 | ||
|
|
54200991cb | ||
|
|
6bea10bdac | ||
|
|
6c615a4893 | ||
|
|
3dc338b3a5 | ||
|
|
37278ff52b | ||
|
|
17262ebdac | ||
|
|
836c493951 | ||
|
|
eb45874546 | ||
|
|
f75520a1d8 | ||
|
|
b5392f0c2b | ||
|
|
d667b8fa98 | ||
|
|
6a1032cd61 | ||
|
|
d72f8d3f9c | ||
|
|
cd95a75f8f | ||
|
|
e7bb393cee | ||
|
|
9b74373c22 | ||
|
|
de0afdfa16 | ||
|
|
480b3f1902 | ||
|
|
be73ca188d | ||
|
|
d0ad55611d | ||
|
|
d47797bf7a | ||
|
|
54c29fd787 | ||
|
|
294595513a | ||
|
|
b8f101ead7 | ||
|
|
2614118d7d | ||
|
|
cbcbaaa9fa | ||
|
|
7c6f6816b3 | ||
|
|
3e3ed050ba | ||
|
|
84179bc207 | ||
|
|
9dc795ded7 | ||
|
|
9c733d65b2 | ||
|
|
4d49890b1e | ||
|
|
f78c6978cf | ||
|
|
b58a3d89e8 | ||
|
|
cd0cfba7c0 | ||
|
|
713c95d597 | ||
|
|
15534ad42e | ||
|
|
32bb3fac69 | ||
|
|
e604da4ff4 | ||
|
|
557d535e5a | ||
|
|
60517b00f3 | ||
|
|
faf5e8e82b | ||
|
|
7264982761 | ||
|
|
fedf74258f | ||
|
|
def4960be6 | ||
|
|
525cc69c70 | ||
|
|
7ed1b164b5 | ||
|
|
0a9c31fb09 | ||
|
|
5ed6f97846 | ||
|
|
7dc195606c | ||
|
|
b2377a3353 | ||
|
|
0fdae0c775 | ||
|
|
113bbd960f | ||
|
|
a48e09e77b | ||
|
|
3c3f759d9a | ||
|
|
5f0986d03b | ||
|
|
579794d7e0 | ||
|
|
4956543eac | ||
|
|
87043f19bc | ||
|
|
375f8ceb27 | ||
|
|
496ad6a442 | ||
|
|
83a09c4af2 | ||
|
|
fac6985d01 | ||
|
|
43670ba62b | ||
|
|
ef511349f8 | ||
|
|
eea0199a21 | ||
|
|
7e0f02ecc7 | ||
|
|
7b26a65ea0 | ||
|
|
c5b92d7162 | ||
|
|
f8387f0a81 | ||
|
|
8486326b00 | ||
|
|
8f64747e75 | ||
|
|
f75efdaa03 | ||
|
|
ee8d9d0c07 | ||
|
|
6791adef46 | ||
|
|
80bcb84acf | ||
|
|
7d6b0f9ca8 | ||
|
|
594f6d4fc5 | ||
|
|
e40acb9bf6 | ||
|
|
e372871108 | ||
|
|
1c01469a3e | ||
|
|
90c2e45be2 | ||
|
|
1a783d4faf | ||
|
|
d2944983a4 | ||
|
|
26459eecb0 | ||
|
|
90dea16222 | ||
|
|
8897a326b3 | ||
|
|
f3e69ddef1 | ||
|
|
8ecaa2c4d1 | ||
|
|
3a607a81f6 | ||
|
|
4b113e4a82 | ||
|
|
3e0e0c1484 | ||
|
|
676e166459 | ||
|
|
f0a704b93b | ||
|
|
cfc528df9a | ||
|
|
9231ea1446 | ||
|
|
f2f9138435 | ||
|
|
624700497b | ||
|
|
5794a64da6 | ||
|
|
7e6e9d3dcd | ||
|
|
07634edfa3 | ||
|
|
4a03fbfbf0 | ||
|
|
000cdb08ec | ||
|
|
e0521b3c95 | ||
|
|
9547be89e1 | ||
|
|
40d44269fc | ||
|
|
3bf0903453 | ||
|
|
c3abf8c05c | ||
|
|
6220ce6780 | ||
|
|
6107698a76 | ||
|
|
a3c3fec9b4 | ||
|
|
f3a9b19104 | ||
|
|
d9ed6f600b | ||
|
|
8a6d86727c | ||
|
|
d9abf82918 | ||
|
|
3cd9020ee0 | ||
|
|
6d22a4d014 | ||
|
|
ccad5d40ec | ||
|
|
5b7b022d9f | ||
|
|
387139b5d3 | ||
|
|
0ae10d5fbe | ||
|
|
d53397d8f8 | ||
|
|
b1006d2a14 | ||
|
|
c5cf318cdd | ||
|
|
671338c16a | ||
|
|
bbaa70e396 | ||
|
|
536d6cf63e | ||
|
|
b564a297ab | ||
|
|
6d34ae1a50 | ||
|
|
0d2457f39e | ||
|
|
420505328c | ||
|
|
0e60d71006 | ||
|
|
0f1456819b | ||
|
|
3af89a6175 | ||
|
|
2fac871493 | ||
|
|
0166133b61 | ||
|
|
a9e0911796 | ||
|
|
213f341257 | ||
|
|
01cc2a2c67 | ||
|
|
6c7a040c02 | ||
|
|
4e40944b26 | ||
|
|
af859438b4 | ||
|
|
7ef80e87b8 | ||
|
|
f8e873cd78 | ||
|
|
5cc97f7cf1 | ||
|
|
44c56331fd | ||
|
|
1272c3962e | ||
|
|
e2799fcdff | ||
|
|
8e4f402e21 | ||
|
|
10d2949647 | ||
|
|
30ef23308b | ||
|
|
564132ee82 | ||
|
|
1e527e87df | ||
|
|
9692a04275 | ||
|
|
6ece8eb0c1 | ||
|
|
1f33237e8a | ||
|
|
9209508aac | ||
|
|
f750e6ff7e | ||
|
|
f2eecf774b | ||
|
|
98de4e75e0 | ||
|
|
459e32caf8 | ||
|
|
c4ad325e5c | ||
|
|
f55bd6d6cd | ||
|
|
f1ce700c93 | ||
|
|
c18e1e8456 | ||
|
|
1fcd1924c3 | ||
|
|
a1767f0425 | ||
|
|
44d95ca25f | ||
|
|
7432540c29 | ||
|
|
c289f58351 | ||
|
|
711e041ff5 | ||
|
|
0ac3534585 | ||
|
|
57821e0860 | ||
|
|
0dae798c9a | ||
|
|
732e780fd9 | ||
|
|
5577124b7a | ||
|
|
81b82c75a2 | ||
|
|
b32e322749 | ||
|
|
3031cc4561 | ||
|
|
d60abc648c | ||
|
|
a9a0233bb3 | ||
|
|
f6fa9e5122 | ||
|
|
29b4f7c91a | ||
|
|
0a9ee57233 | ||
|
|
94b69a9c1c | ||
|
|
f0209dd1cc | ||
|
|
366e432c18 | ||
|
|
0075c0e779 | ||
|
|
d99dfd4185 | ||
|
|
0309b3ad25 | ||
|
|
7a85532b73 | ||
|
|
7299d947f7 | ||
|
|
adec7b28f1 | ||
|
|
c2563da056 | ||
|
|
64b9e53916 | ||
|
|
86ab6f7b3d | ||
|
|
ed02733524 | ||
|
|
a99af63f34 | ||
|
|
1d900a66fe | ||
|
|
f29acc217d | ||
|
|
f60164f5c5 | ||
|
|
578cf1f00d | ||
|
|
bca3bc6b4a | ||
|
|
352c813544 | ||
|
|
7d876bddc7 | ||
|
|
43d334259b | ||
|
|
750579b1c2 | ||
|
|
d69221d85b | ||
|
|
a1c80e92cd | ||
|
|
ce1a0d66f1 | ||
|
|
f28057d620 | ||
|
|
8f6dc7e6d2 | ||
|
|
b25a237c20 | ||
|
|
ccb0b59e17 | ||
|
|
14658a2d70 | ||
|
|
5f26878c06 | ||
|
|
df78ae59fe | ||
|
|
47924dfb61 | ||
|
|
3eb442e0f6 | ||
|
|
b309fc09f6 | ||
|
|
a00fe0485b | ||
|
|
a60f65857b | ||
|
|
fe584b58b0 | ||
|
|
91ad2bf66b | ||
|
|
01988a9435 | ||
|
|
51bc8e1b2c | ||
|
|
b28c684bd3 | ||
|
|
42b2cde1e2 | ||
|
|
199a7b816c | ||
|
|
2b8fc6764e | ||
|
|
37f93cda4d | ||
|
|
4114f5b3c2 | ||
|
|
14d45eb759 | ||
|
|
0dfa9d2c2c | ||
|
|
6fce18ffe8 | ||
|
|
956c56c494 | ||
|
|
75e7c6a9eb | ||
|
|
a7fb66d269 | ||
|
|
5f48802357 | ||
|
|
a27488c8da | ||
|
|
b6dbd7512c | ||
|
|
e41386082c | ||
|
|
ae385c139b | ||
|
|
dd2f213a4f | ||
|
|
35f92a6e91 | ||
|
|
ab5648a5d6 | ||
|
|
8a9b59c66a | ||
|
|
a6e0c877b5 | ||
|
|
de5a623a00 | ||
|
|
b0f9ce081f | ||
|
|
e17b6e83a4 | ||
|
|
96f4322f74 | ||
|
|
780f59d666 | ||
|
|
8c5db2eef5 | ||
|
|
5ab004b87e | ||
|
|
1b001bdd4f | ||
|
|
a5fa44213d | ||
|
|
68e9d9d91c | ||
|
|
a14783a275 | ||
|
|
3ce58d2edf | ||
|
|
523efac4fb | ||
|
|
d352aca9cc | ||
|
|
ae6afab01b | ||
|
|
efea405b83 | ||
|
|
ad9262cf0f | ||
|
|
636c268e46 | ||
|
|
06a61f0374 | ||
|
|
4ba7763de5 | ||
|
|
b486542e7b | ||
|
|
f53ce6cbf0 | ||
|
|
b10ff655f1 | ||
|
|
bbc27dbeea | ||
|
|
dadb929afd | ||
|
|
c0f397cdd5 | ||
|
|
b597cf6e18 | ||
|
|
e3bbeb2022 | ||
|
|
cad9fc8977 | ||
|
|
2f191c1f71 | ||
|
|
dc0debce1a | ||
|
|
d96b6f0100 | ||
|
|
16f1901976 | ||
|
|
61d9e4d531 | ||
|
|
aec4479e19 | ||
|
|
ec7235c03a | ||
|
|
f3db153ea9 | ||
|
|
e1c258478f | ||
|
|
1f2e691eb1 | ||
|
|
be9a3a9975 | ||
|
|
8d17ac6f28 | ||
|
|
d9df150cf8 | ||
|
|
667a4aab1a | ||
|
|
862ffd8172 | ||
|
|
eb1dd954ee | ||
|
|
6f2e5a63d7 | ||
|
|
5bf78c5cd2 | ||
|
|
277361a562 | ||
|
|
6697d3d0d5 | ||
|
|
7202aae712 | ||
|
|
6d2c6748f7 | ||
|
|
6c98c9ccf2 | ||
|
|
afc25ec8b3 | ||
|
|
6c9553ba65 | ||
|
|
0fd5ea4689 | ||
|
|
8441208058 | ||
|
|
aa34298771 | ||
|
|
ccacd88f80 | ||
|
|
1f20b21fc8 | ||
|
|
ae7152aca7 | ||
|
|
ba36347f03 | ||
|
|
c0c5e83f31 | ||
|
|
9c05ff2f7c | ||
|
|
0ada05bf3a | ||
|
|
c77b5dfac2 | ||
|
|
673ea40238 | ||
|
|
f7ced7f253 | ||
|
|
6152ec9d0d | ||
|
|
7ed8bb259d | ||
|
|
06882d5bea | ||
|
|
f460456502 | ||
|
|
6ef9f2ff15 | ||
|
|
062af9937f | ||
|
|
0dcdda75be | ||
|
|
e9fe4a82df |
61
README.md
61
README.md
@@ -8,21 +8,18 @@
|
||||
|
||||
<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://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk"><img height="50" alt="Get it on F-Droid" src="img/f-droid-badge.png"></a>
|
||||
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
|
||||
> A fork of the [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly won’t ever be implemented, such as the federated timeline, unlisted posting and an image description viewer.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Key features
|
||||
|
||||
### **Unlisted posting**
|
||||
|
||||
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Local”, “Community” and “Posts”).**
|
||||
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Community”, “Federated” and “Posts”).**
|
||||
|
||||
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in people’s Home timelines, but only if they follow you or someone they follow reposted/replied to your post.
|
||||
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in people’s Home timelines, but only if they follow you or someone they follow reblogged/replied to your post.
|
||||
|
||||
The Mastodon documentation has some more information about [Unlisted posting](https://docs.joinmastodon.org/user/posting/#unlisted) and [Public timelines](https://docs.joinmastodon.org/user/network/#timelines).
|
||||
|
||||
@@ -34,6 +31,12 @@ Despite being one of the main features of federated social media, the Federated
|
||||
|
||||
That’s one of the reasons why choosing a small, **well-moderated instance is important**. Instance admins and moderators should always make sure to ban abusive users and stop federating with instances who platform them. On well-moderated instances, the Federated timeline can be a welcoming place to meet new people!
|
||||
|
||||
### **Draft and schedule posts**
|
||||
|
||||
**Allows for preparing a post and scheduling it to send it automatically at a specific time.**
|
||||
|
||||
You can create drafts, edit them, send them manually later or set a scheduled date. Drafts are technically saved as scheduled posts, so you can view and edit them from other apps that support scheduled posts. Scheduled posts are handled by your home instance, so they'll work even if you uninstall Megalodon.
|
||||
|
||||
### **Image description viewer**
|
||||
|
||||
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
|
||||
@@ -48,21 +51,29 @@ On the Fediverse, it’s quite common for people to pin posts they want others t
|
||||
|
||||
## Installation
|
||||
|
||||
### From app stores
|
||||
### IzzyOnDroid
|
||||
|
||||
* **[Izzy's F-Droid repository](https://apt.izzysoft.de/fdroid/repo)**: [apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
|
||||
[apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
|
||||
|
||||
Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first:
|
||||
|
||||
`https://apt.izzysoft.de/fdroid/repo`
|
||||
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
|
||||
* **[Google Play Store](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)**: [play.google.com/store/apps/details?id=org.joinmastodon.android.sk](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)
|
||||
Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first:
|
||||
|
||||
* **[F-Droid.org](https://f-droid.org)?** Not yet, sorry!
|
||||
[`https://apt.izzysoft.de/fdroid/repo`](https://apt.izzysoft.de/fdroid/repo)
|
||||
|
||||
If you want, you can help me figure out if something's missing in the [Issue #47: F-Droid.org](https://github.com/sk22/megalodon/issues/47)
|
||||
### Google Play Store
|
||||
|
||||
### Directly from GitHub
|
||||
[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
|
||||
|
||||
**[F-Droid.org?](https://f-droid.org)** Not yet, sorry!
|
||||
|
||||
If you want, you can help me figure out if something's missing in the [Issue #47: F-Droid.org](https://github.com/sk22/megalodon/issues/47)
|
||||
|
||||
### Direct
|
||||
|
||||
Press the download button to download the APK. Open the downloaded file on your Android device to install it. Megalodon will automatically notify you about new updates inside the app.
|
||||
|
||||
@@ -120,7 +131,7 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* [Implement a bookmark button and list](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/bookmarks) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/22))
|
||||
* [Add “Check for update” button in addition to integrated update checker](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/check-for-update-button)
|
||||
* [Add “Mark media as sensitive” option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/mark-media-as-sensitive)
|
||||
* [Add settings to hide replies and reposts from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
|
||||
* [Add settings to hide replies and reblogs from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
|
||||
* [Follow and unfollow hashtags](https://github.com/sk22/megalodon/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
|
||||
* [Notification bell for posts](https://github.com/sk22/megalodon/commit/b166ca705eb9169025ef32bbe6315b42491b57ea) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/81))
|
||||
* [Viewing lists and adding/removing users from lists](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
|
||||
@@ -130,11 +141,17 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* [Add notifications tab for posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/posts-notifications-tab)
|
||||
* [Show visibility of original post when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-reply-visibility)
|
||||
* [Clickable reply/boost line above posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:clickable-boost-reply-line)
|
||||
* [Clickable reply line while replying to open original post](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/clickable-reply-line-compose)
|
||||
* [Add push notification setting for post notifications](https://github.com/sk22/megalodon/commit/b190480d7739be47f23543d9e7644660f9b4b4ee)
|
||||
* [Add option to allow voting for multiple options on polls](https://github.com/sk22/megalodon/commit/5b28468efd49387b4f8b83f142f3adf3104ca60c)
|
||||
* [Add translate function](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/translate-button)
|
||||
* [Add language selector](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/language-selector)
|
||||
* [Implement deleting notifications](https://github.com/sk22/megalodon/commit/b0f9ce081f69f29ad59658fc00ca41372cd2677d) (disabled by default)
|
||||
* [Long-click boost button to "quote" a post](https://github.com/sk22/megalodon/commit/b25a237c20c6a924ed4d9b357999867c3a32b32b)
|
||||
* [Draft and schedule posts](https://github.com/sk22/megalodon/pull/217)
|
||||
* [Display original post when replying](https://github.com/sk22/megalodon/commit/375f8ceb2747705fedf43686681cc0e0b812f899)
|
||||
* [Display server announcements](https://github.com/sk22/megalodon/commit/84179bc207d6b69cc2a770a3c28fa0a39b0b54e8)
|
||||
* [Create](https://github.com/sk22/megalodon/commit/294595513a45037359b31377aafc25ae5b58d8e7), [edit](https://github.com/sk22/megalodon/commit/d47797bf7ac8cff3f9ba1cfee219a1bb2af21da6) and [delete](https://github.com/sk22/megalodon/commit/54c29fd787fc2cd0dfd2787ad796b8190f795973) lists
|
||||
* [Soft-blocking (by blocking and immediately unblocking)](https://github.com/sk22/megalodon/commit/e75d350b7a2709259e9fc5138e0e1f361bdb0972)
|
||||
|
||||
|
||||
### Behavior
|
||||
@@ -152,6 +169,12 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* [Make inline emoji search case-insensitive and don't only search from start of emoji names](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:better-inline-emoji-search) ([Pull request](https://github.com/mastodon/mastodon-android/pull/445))
|
||||
* [Include subject line when sharing e.g. a website to Megalodon](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:external-share-include-subject)
|
||||
* [Improve semantics for voting on polls (radio buttons and checkboxes)](https://github.com/sk22/megalodon/commit/6fd58c96827cb1d2da329cebdc170a1425dd18d7)
|
||||
* [Copy post URL when long-pressing share button](https://github.com/sk22/megalodon/commit/ba36347f03278763ecec617b1ce57ba89db7be72)
|
||||
* [Add option to disable swiping between tabs](https://github.com/sk22/megalodon/commit/1f20b21fc84bf006c1ec14bd2229cbfad5215ec8)
|
||||
* [Resolve Fediverse links in the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/open-urls-in-app)
|
||||
* [Preserve whitespaces in HTML](https://github.com/sk22/megalodon/commit/7d876bddc7a07d98f0fecbf62b13bdb9fcce3412)
|
||||
* [Long-click to copy links](https://github.com/sk22/megalodon/commit/b32e32274923a94742a9926ef38785f746d41405)
|
||||
* Improved filtering using Mastodon 4.0 API: [#202](https://github.com/sk22/megalodon/pull/202), [#212](https://github.com/sk22/megalodon/pull/212), [#255](https://github.com/sk22/megalodon/pull/255) by [@thiagojedi](https://github.com/thiagojedi)
|
||||
|
||||
|
||||
### Visual
|
||||
@@ -163,8 +186,10 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* [Custom "megalodon" text logo](https://github.com/sk22/megalodon/commit/563afd487ca5c608cfbb00fa3909d3c27384acc0) by [@LucasGGamerM](https://github.com/LucasGGamerM)
|
||||
* [Custom login screen](https://github.com/sk22/megalodon/commit/9bbf8c4618dbe13accaeb3b5482bf3fe88cac4c0)
|
||||
* [More distinct filled boost icon](https://github.com/sk22/megalodon/commits/more-distinct-filled-boost-icon)
|
||||
* [Material You color theme] by [@LucasGGamerM](https://github.com/LucasGGamerM)
|
||||
* Material You color theme by [@LucasGGamerM](https://github.com/LucasGGamerM)
|
||||
* [Animations for interaction buttons](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/animate-buttons)
|
||||
* [Dedicated icons for different notification types](https://github.com/sk22/megalodon/pull/178) by [@florian-obernberger](https://github.com/florian-obernberger)
|
||||
* Scale text according to system settings
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
3
fix-metadata-markdown-lists.sh
Executable file
3
fix-metadata-markdown-lists.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
find metadata -name '*.txt' -exec sed -Ei 's/^[–—─•·*]\s+/- /' {} \;
|
||||
BIN
img/izzy-badge.png
Normal file
BIN
img/izzy-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -9,8 +9,8 @@ android {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 60
|
||||
versionName "1.1.5+fork.60"
|
||||
versionCode 69
|
||||
versionName "1.1.5+fork.69"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "nl-rNL", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
info=new UpdateInfo();
|
||||
info.version=prefs.getString("version", null);
|
||||
info.size=prefs.getLong("apkSize", 0);
|
||||
info.changelog=prefs.getString("changelog", null);
|
||||
downloadID=prefs.getLong("downloadID", 0);
|
||||
if(downloadID==0 || !getUpdateApkFile().exists()){
|
||||
state=UpdateState.UPDATE_AVAILABLE;
|
||||
@@ -84,6 +85,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
.remove("apkURL")
|
||||
.remove("checkedByBuild")
|
||||
.remove("downloadID")
|
||||
.remove("changelog")
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
@@ -117,6 +119,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
try(Response resp=call.execute()){
|
||||
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
|
||||
String tag=obj.get("tag_name").getAsString();
|
||||
String changelog=obj.get("body").getAsString();
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
|
||||
Matcher matcher=pattern.matcher(tag);
|
||||
if(!matcher.find()){
|
||||
@@ -151,12 +154,14 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
UpdateInfo info=new UpdateInfo();
|
||||
info.size=size;
|
||||
info.version=version;
|
||||
info.changelog=changelog;
|
||||
this.info=info;
|
||||
|
||||
getPrefs().edit()
|
||||
.putLong("apkSize", size)
|
||||
.putString("version", version)
|
||||
.putString("apkURL", url)
|
||||
.putString("changelog", changelog)
|
||||
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
|
||||
.remove("downloadID")
|
||||
.apply();
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
85
mastodon/src/main/assets/blocks.tsv
Normal file
85
mastodon/src/main/assets/blocks.tsv
Normal file
@@ -0,0 +1,85 @@
|
||||
# lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
|
||||
# https://raw.githubusercontent.com/greyhat-academy/lists.d/main/mastodon.domains.block.list.tsv
|
||||
# This list contains domains of toxic mastodon instances
|
||||
# Last-Modified: 1672044500
|
||||
|
||||
# gab - a neonazi social network
|
||||
gab.ai
|
||||
gab.com
|
||||
gab.protohype.net
|
||||
|
||||
# consequence-free speech
|
||||
social.unzensiert.to
|
||||
freeatlantis.com
|
||||
|
||||
# reactionary bigotry and hatespeech against magrinalized groups
|
||||
poa.st
|
||||
freespeechextremist.com
|
||||
rdrama.cc
|
||||
outpoa.st
|
||||
anime.website
|
||||
gameliberty.club
|
||||
social.byoblu.com
|
||||
yggdrasil.social
|
||||
smuglo.li
|
||||
dogeposting.social
|
||||
unsafe.space
|
||||
freezepeach.xyz
|
||||
|
||||
# + CSAM
|
||||
rojogato.com
|
||||
|
||||
# antivaxxer shitposting & fearmongering
|
||||
shadowsocial.org
|
||||
|
||||
# Kiwifarms
|
||||
kiwifarms.net
|
||||
kiwifarms.cc
|
||||
kiwifarms.is
|
||||
kiwifarms.pleroma.net
|
||||
|
||||
|
||||
# https://mastodon.art/@Curator/109649354849593592
|
||||
|
||||
poa.st antisemitic racist homophobic
|
||||
nicecrew.digital antisemitic
|
||||
beefyboys.win antisemitic racist homophobic harassment
|
||||
cawfee.club antisemitic racist homophobic
|
||||
comfyboy.club antisemitic racist homophobic
|
||||
freespeechextremist.com racist homophobic
|
||||
cum.salon racist misogynist
|
||||
bae.st racist
|
||||
natehiggers.online racist
|
||||
rapemeat.solutions misogynist
|
||||
rapist.town misogynist
|
||||
rapefeminists.network misogynist
|
||||
kiwifarms.cc harassment
|
||||
noagendasocial.com noagenda
|
||||
posting.lolicon.rocks underage
|
||||
urchan.org harassment homophobic racist
|
||||
ryona.agency harassment
|
||||
yggdrasil.social antisemitic homophobic racist
|
||||
genderheretics.xyz transphobic
|
||||
baraag.net underage
|
||||
lolison.top underage
|
||||
shota.house underage
|
||||
shota.social underage
|
||||
aethy.com underage
|
||||
taullo.social underage
|
||||
childpawn.shop underage
|
||||
posting.lolicon.rocks underage
|
||||
loli.best underage
|
||||
gothloli.club underage
|
||||
smuglo.li underage
|
||||
youjo.love underage
|
||||
pedo.school underage
|
||||
lolison.network underage
|
||||
freak.university underage
|
||||
mirr0r.city underage
|
||||
xhais.love underage
|
||||
refusal.biz underage
|
||||
refusal.llc underage
|
||||
mirr0r.city underage
|
||||
nnia.space underage
|
||||
ignorelist.com malicious
|
||||
repl.co malicious
|
||||
|
@@ -12,7 +12,6 @@ import android.widget.Toast;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -36,13 +35,10 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
||||
openComposeFragment(sessions.get(0).getID());
|
||||
}else{
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000));
|
||||
new M3AlertDialogBuilder(this)
|
||||
.setItems(sessions.stream().map(as->"@"+as.self.username+"@"+as.domain).toArray(String[]::new), (dialog, which)->{
|
||||
openComposeFragment(sessions.get(which).getID());
|
||||
})
|
||||
.setTitle(R.string.choose_account)
|
||||
.setOnCancelListener(dialog -> finish())
|
||||
.show();
|
||||
UiUtils.pickAccount(this, null, R.string.choose_account, 0,
|
||||
session -> openComposeFragment(session.getID()),
|
||||
b -> b.setOnCancelListener(d -> finish())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +48,11 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
||||
|
||||
Intent intent=getIntent();
|
||||
StringBuilder builder=new StringBuilder();
|
||||
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) builder.append(intent.getStringExtra(Intent.EXTRA_SUBJECT)).append("\n\n");
|
||||
String subject = "";
|
||||
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
|
||||
subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||
if (!subject.isBlank()) builder.append(subject).append("\n\n");
|
||||
}
|
||||
if (intent.hasExtra(Intent.EXTRA_TEXT)) builder.append(intent.getStringExtra(Intent.EXTRA_TEXT)).append("\n");
|
||||
String text=builder.toString();
|
||||
List<Uri> mediaUris;
|
||||
@@ -80,6 +80,8 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
||||
args.putString("account", accountID);
|
||||
if(!TextUtils.isEmpty(text))
|
||||
args.putString("prefilledText", text);
|
||||
if(!subject.isBlank())
|
||||
args.putInt("selectionEnd", subject.length());
|
||||
if(mediaUris!=null && !mediaUris.isEmpty())
|
||||
args.putParcelableArrayList("mediaAttachments", toArrayList(mediaUris));
|
||||
Fragment fragment=new ComposeFragment();
|
||||
|
||||
@@ -5,11 +5,11 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -22,22 +22,32 @@ public class GlobalUserPreferences{
|
||||
public static boolean showReplies;
|
||||
public static boolean showBoosts;
|
||||
public static boolean loadNewPosts;
|
||||
public static boolean showFederatedTimeline;
|
||||
public static boolean showInteractionCounts;
|
||||
public static boolean alwaysExpandContentWarnings;
|
||||
public static boolean disableMarquee;
|
||||
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 String publishButtonText;
|
||||
public static ThemePreference theme;
|
||||
public static ColorPreference color;
|
||||
|
||||
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
|
||||
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
|
||||
public static Map<String, List<String>> recentLanguages;
|
||||
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -50,13 +60,21 @@ public class GlobalUserPreferences{
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
|
||||
showFederatedTimeline=prefs.getBoolean("showFederatedTimeline", !BuildConfig.BUILD_TYPE.equals("playRelease"));
|
||||
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
|
||||
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
||||
disableMarquee=prefs.getBoolean("disableMarquee", false);
|
||||
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);
|
||||
publishButtonText=prefs.getString("publishButtonText", "");
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
recentLanguages=fromJson(prefs.getString("recentLanguages", "{}"), recentLanguagesType, new HashMap<>());
|
||||
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
|
||||
pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>());
|
||||
|
||||
try {
|
||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
|
||||
@@ -73,14 +91,22 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
.putBoolean("loadNewPosts", loadNewPosts)
|
||||
.putBoolean("showFederatedTimeline", showFederatedTimeline)
|
||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||
.putBoolean("showInteractionCounts", showInteractionCounts)
|
||||
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
|
||||
.putBoolean("disableMarquee", disableMarquee)
|
||||
.putBoolean("disableSwipe", disableSwipe)
|
||||
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
|
||||
.putBoolean("translateButtonOpenedOnly", translateButtonOpenedOnly)
|
||||
.putBoolean("uniformNotificationIcon", uniformNotificationIcon)
|
||||
.putBoolean("reduceMotion", reduceMotion)
|
||||
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
||||
.putBoolean("disableAltTextReminder", disableAltTextReminder)
|
||||
.putString("publishButtonText", publishButtonText)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.putString("color", color.name())
|
||||
.putString("recentLanguages", gson.toJson(recentLanguages))
|
||||
.putString("pinnedTimelines", gson.toJson(pinnedTimelines))
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -92,7 +118,6 @@ public class GlobalUserPreferences{
|
||||
BLUE,
|
||||
BROWN,
|
||||
RED,
|
||||
ORANGE,
|
||||
YELLOW
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@@ -39,6 +37,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
private static final String TAG="PushNotificationReceive";
|
||||
|
||||
public static final int NOTIFICATION_ID=178;
|
||||
private static int notificationId = 0;
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
@@ -138,18 +137,27 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
.setContentText(pn.body)
|
||||
.setStyle(new Notification.BigTextStyle().bigText(pn.body))
|
||||
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(PendingIntent.getActivity(context, notificationId, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
|
||||
.setShowWhen(true)
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
.setAutoCancel(true)
|
||||
.setColor(context.getColor(R.color.primary_700));
|
||||
|
||||
if (!GlobalUserPreferences.uniformNotificationIcon) switch (pn.notificationType) {
|
||||
case FAVORITE -> builder.setSmallIcon(R.drawable.ic_fluent_star_24_filled);
|
||||
case REBLOG -> builder.setSmallIcon(R.drawable.ic_fluent_arrow_repeat_all_24_filled);
|
||||
case FOLLOW -> builder.setSmallIcon(R.drawable.ic_fluent_person_add_24_filled);
|
||||
case MENTION -> builder.setSmallIcon(R.drawable.ic_fluent_mention_24_filled);
|
||||
case POLL -> builder.setSmallIcon(R.drawable.ic_fluent_poll_24_filled);
|
||||
}
|
||||
|
||||
if(avatar!=null){
|
||||
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));
|
||||
}
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().size()>1){
|
||||
builder.setSubText(accountName);
|
||||
}
|
||||
nm.notify(accountID, NOTIFICATION_ID, builder.build());
|
||||
nm.notify(accountID, GlobalUserPreferences.keepOnlyLatestNotification ? NOTIFICATION_ID : notificationId++, builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,14 @@ import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
|
||||
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
@@ -47,9 +50,26 @@ public class MastodonAPIController{
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
|
||||
|
||||
private AccountSession session;
|
||||
private static List<String> badDomains = new ArrayList<>();
|
||||
|
||||
static{
|
||||
thread.start();
|
||||
try {
|
||||
final BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||
MastodonApp.context.getAssets().open("blocks.tsv")
|
||||
));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isBlank() || line.startsWith("#")) continue;
|
||||
String[] parts = line.replaceAll("\"", "").split("[\s,;]");
|
||||
if (parts.length == 0) continue;
|
||||
String domain = parts[0].toLowerCase().trim();
|
||||
if (domain.isBlank()) continue;
|
||||
badDomains.add(domain);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public MastodonAPIController(@Nullable AccountSession session){
|
||||
@@ -57,8 +77,11 @@ public class MastodonAPIController{
|
||||
}
|
||||
|
||||
public <T> void submitRequest(final MastodonAPIRequest<T> req){
|
||||
final String host = req.getURL().getHost();
|
||||
final boolean isBad = host == null || badDomains.stream().anyMatch(h -> h.equalsIgnoreCase(host) || host.toLowerCase().endsWith("." + h));
|
||||
thread.postRunnable(()->{
|
||||
try{
|
||||
if (isBad) throw new IllegalArgumentException();
|
||||
if(req.canceled)
|
||||
return;
|
||||
Request.Builder builder=new Request.Builder()
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.StringRes;
|
||||
@@ -101,9 +102,14 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
|
||||
return wrapProgress(activity, message, cancelable, null);
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable, Consumer<ProgressDialog> transform){
|
||||
progressDialog=new ProgressDialog(activity);
|
||||
progressDialog.setMessage(activity.getString(message));
|
||||
progressDialog.setCancelable(cancelable);
|
||||
if (transform != null) transform.accept(progressDialog);
|
||||
if(cancelable){
|
||||
progressDialog.setOnCancelListener(dialog->cancel());
|
||||
}
|
||||
|
||||
@@ -162,6 +162,8 @@ 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);
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.function.Consumer;
|
||||
@@ -18,12 +19,18 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class StatusInteractionController{
|
||||
private final String accountID;
|
||||
private final boolean updateCounters;
|
||||
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
|
||||
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
|
||||
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
|
||||
|
||||
public StatusInteractionController(String accountID){
|
||||
public StatusInteractionController(String accountID, boolean updateCounters) {
|
||||
this.accountID=accountID;
|
||||
this.updateCounters=updateCounters;
|
||||
}
|
||||
|
||||
public StatusInteractionController(String accountID){
|
||||
this(accountID, true);
|
||||
}
|
||||
|
||||
public void setFavorited(Status status, boolean favorited, Consumer<Status> cb){
|
||||
@@ -41,7 +48,7 @@ public class StatusInteractionController{
|
||||
runningFavoriteRequests.remove(status.id);
|
||||
result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1);
|
||||
cb.accept(result);
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,16 +57,16 @@ public class StatusInteractionController{
|
||||
error.showToast(MastodonApp.context);
|
||||
status.favourited=!favorited;
|
||||
cb.accept(status);
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningFavoriteRequests.put(status.id, req);
|
||||
status.favourited=favorited;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setReblogged(Status status, boolean reblogged, Consumer<Status> cb){
|
||||
public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){
|
||||
if(!Looper.getMainLooper().isCurrentThread())
|
||||
throw new IllegalStateException("Can only be called from main thread");
|
||||
|
||||
@@ -67,14 +74,15 @@ public class StatusInteractionController{
|
||||
if(current!=null){
|
||||
current.cancel();
|
||||
}
|
||||
SetStatusReblogged req=(SetStatusReblogged) new SetStatusReblogged(status.id, reblogged)
|
||||
SetStatusReblogged req=(SetStatusReblogged) new SetStatusReblogged(status.id, reblogged, visibility)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
public void onSuccess(Status reblog){
|
||||
Status result = reblog.getContentStatus();
|
||||
runningReblogRequests.remove(status.id);
|
||||
result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1);
|
||||
cb.accept(result);
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,13 +91,13 @@ public class StatusInteractionController{
|
||||
error.showToast(MastodonApp.context);
|
||||
status.reblogged=!reblogged;
|
||||
cb.accept(status);
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningReblogRequests.put(status.id, req);
|
||||
status.reblogged=reblogged;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setBookmarked(Status status, boolean bookmarked){
|
||||
@@ -110,7 +118,7 @@ public class StatusInteractionController{
|
||||
public void onSuccess(Status result){
|
||||
runningBookmarkRequests.remove(status.id);
|
||||
cb.accept(result);
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -119,12 +127,12 @@ public class StatusInteractionController{
|
||||
error.showToast(MastodonApp.context);
|
||||
status.bookmarked=!bookmarked;
|
||||
cb.accept(status);
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningBookmarkRequests.put(status.id, req);
|
||||
status.bookmarked=bookmarked;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.announcements;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
|
||||
public class DismissAnnouncement extends MastodonAPIRequest<Object>{
|
||||
public DismissAnnouncement(String id){
|
||||
super(HttpMethod.POST, "/announcements/" + id + "/dismiss", Object.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.joinmastodon.android.api.requests.announcements;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Announcement;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetAnnouncements extends MastodonAPIRequest<List<Announcement>> {
|
||||
public GetAnnouncements(boolean withDismissed) {
|
||||
super(MastodonAPIRequest.HttpMethod.GET, "/announcements", new TypeToken<>(){});
|
||||
addQueryParameter("with_dismissed", withDismissed ? "true" : "false");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
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) {
|
||||
super(HttpMethod.POST, "/lists", ListTimeline.class);
|
||||
Request req = new Request();
|
||||
req.title = title;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
public static class Request {
|
||||
public String title;
|
||||
public ListTimeline.RepliesPolicy repliesPolicy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class DeleteList extends MastodonAPIRequest<Object> {
|
||||
public DeleteList(String id) {
|
||||
super(HttpMethod.DELETE, "/lists/" + id, Object.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class GetList extends MastodonAPIRequest<ListTimeline> {
|
||||
public GetList(String id) {
|
||||
super(HttpMethod.GET, "/lists/" + id, ListTimeline.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
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) {
|
||||
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
|
||||
CreateList.Request req = new CreateList.Request();
|
||||
req.title = title;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.ApiUtils;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
public class DismissNotification extends MastodonAPIRequest<Object>{
|
||||
public DismissNotification(String id){
|
||||
super(HttpMethod.POST, "/notifications/" + (id != null ? id + "/dismiss" : "clear"), Object.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
|
||||
Request r=new Request();
|
||||
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
|
||||
r.data.alerts=alerts;
|
||||
r.data.policy=policy;
|
||||
r.policy=policy;
|
||||
r.subscription.keys.p256dh=encryptionKey;
|
||||
r.subscription.keys.auth=authKey;
|
||||
setRequestBody(r);
|
||||
@@ -18,6 +18,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
|
||||
private static class Request{
|
||||
public Subscription subscription=new Subscription();
|
||||
public Data data=new Data();
|
||||
public PushSubscription.Policy policy;
|
||||
|
||||
private static class Keys{
|
||||
public String p256dh;
|
||||
@@ -31,7 +32,6 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
|
||||
|
||||
private static class Data{
|
||||
public PushSubscription.Alerts alerts;
|
||||
public PushSubscription.Policy policy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,36 @@ package org.joinmastodon.android.api.requests.notifications;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class UpdatePushSettings extends MastodonAPIRequest<PushSubscription>{
|
||||
private final PushSubscription.Policy policy;
|
||||
|
||||
public UpdatePushSettings(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
|
||||
super(HttpMethod.PUT, "/push/subscription", PushSubscription.class);
|
||||
setRequestBody(new Request(alerts, policy));
|
||||
this.policy=policy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(PushSubscription respObj, Response httpResponse) throws IOException{
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
respObj.policy=policy;
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public Data data=new Data();
|
||||
public PushSubscription.Policy policy;
|
||||
|
||||
public Request(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
|
||||
this.data.alerts=alerts;
|
||||
this.data.policy=policy;
|
||||
this.policy=policy;
|
||||
}
|
||||
|
||||
private static class Data{
|
||||
public PushSubscription.Alerts alerts;
|
||||
public PushSubscription.Policy policy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
@@ -9,12 +10,29 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||
public static final Instant DRAFTS_AFTER_INSTANT = Instant.ofEpochMilli(253370764799999L) /* end of 9998 */;
|
||||
private static final float draftFactor = 31536000000f /* one year */ / 253370764799999f /* end of 9998 */;
|
||||
|
||||
public static Instant getDraftInstant() {
|
||||
// returns an instant between 9999-01-01 00:00:00 and 9999-12-31 23:59:59
|
||||
// yes, this is a weird implementation for something that hardly matters
|
||||
return DRAFTS_AFTER_INSTANT.plusMillis(1 + (long) (System.currentTimeMillis() * draftFactor));
|
||||
}
|
||||
|
||||
public CreateStatus(CreateStatus.Request req, String uuid){
|
||||
super(HttpMethod.POST, "/statuses", Status.class);
|
||||
setRequestBody(req);
|
||||
addHeader("Idempotency-Key", uuid);
|
||||
}
|
||||
|
||||
public static class Scheduled extends MastodonAPIRequest<ScheduledStatus>{
|
||||
public Scheduled(CreateStatus.Request req, String uuid){
|
||||
super(HttpMethod.POST, "/statuses", ScheduledStatus.class);
|
||||
setRequestBody(req);
|
||||
addHeader("Idempotency-Key", uuid);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Request{
|
||||
public String status;
|
||||
public List<String> mediaIds;
|
||||
|
||||
@@ -7,4 +7,10 @@ public class DeleteStatus extends MastodonAPIRequest<Status>{
|
||||
public DeleteStatus(String id){
|
||||
super(HttpMethod.DELETE, "/statuses/"+id, Status.class);
|
||||
}
|
||||
|
||||
public static class Scheduled extends MastodonAPIRequest<Object> {
|
||||
public Scheduled(String id) {
|
||||
super(HttpMethod.DELETE, "/scheduled_statuses/"+id, Object.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
|
||||
public class GetScheduledStatuses extends HeaderPaginationRequest<ScheduledStatus>{
|
||||
public GetScheduledStatuses(String maxID, int limit){
|
||||
super(HttpMethod.GET, "/scheduled_statuses", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,17 @@ package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
public class SetStatusReblogged extends MastodonAPIRequest<Status>{
|
||||
public SetStatusReblogged(String id, boolean reblogged){
|
||||
public SetStatusReblogged(String id, boolean reblogged, StatusPrivacy visibility){
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/"+(reblogged ? "reblog" : "unreblog"), Status.class);
|
||||
setRequestBody(new Object());
|
||||
Request req = new Request();
|
||||
req.visibility = visibility;
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
public static class Request {
|
||||
public StatusPrivacy visibility;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.joinmastodon.android.api.requests.tags;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetFollowedHashtags extends HeaderPaginationRequest<Hashtag> {
|
||||
public GetFollowedHashtags() {
|
||||
this(null, null, -1, null);
|
||||
}
|
||||
|
||||
public GetFollowedHashtags(String maxID, String minID, int limit, String sinceID){
|
||||
super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ import org.joinmastodon.android.model.Status;
|
||||
import java.util.List;
|
||||
|
||||
public class GetTrendingStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
public GetTrendingStatuses(int limit){
|
||||
public GetTrendingStatuses(int offset, int limit){
|
||||
super(HttpMethod.GET, "/trends/statuses", new TypeToken<>(){});
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(offset>0)
|
||||
addQueryParameter("offset", ""+offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class AccountSession{
|
||||
public Preferences preferences;
|
||||
public AccountActivationInfo activationInfo;
|
||||
private transient MastodonAPIController apiController;
|
||||
private transient StatusInteractionController statusInteractionController;
|
||||
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
|
||||
private transient CacheController cacheController;
|
||||
private transient PushSubscriptionManager pushSubscriptionManager;
|
||||
|
||||
@@ -52,6 +52,10 @@ public class AccountSession{
|
||||
return domain+"_"+self.id;
|
||||
}
|
||||
|
||||
public String getFullUsername() {
|
||||
return "@"+self.username+"@"+domain;
|
||||
}
|
||||
|
||||
public MastodonAPIController getApiController(){
|
||||
if(apiController==null)
|
||||
apiController=new MastodonAPIController(this);
|
||||
@@ -64,6 +68,12 @@ public class AccountSession{
|
||||
return statusInteractionController;
|
||||
}
|
||||
|
||||
public StatusInteractionController getRemoteStatusInteractionController(){
|
||||
if(remoteStatusInteractionController==null)
|
||||
remoteStatusInteractionController=new StatusInteractionController(getID(), false);
|
||||
return remoteStatusInteractionController;
|
||||
}
|
||||
|
||||
public CacheController getCacheController(){
|
||||
if(cacheController==null)
|
||||
cacheController=new CacheController(getID());
|
||||
|
||||
@@ -13,8 +13,6 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
@@ -108,7 +106,7 @@ public class AccountSessionManager{
|
||||
sessions.put(session.getID(), session);
|
||||
lastActiveAccountID=session.getID();
|
||||
writeAccountsFile();
|
||||
updateInstanceEmojis(instance, instance.uri);
|
||||
updateMoreInstanceInfo(instance, instance.uri);
|
||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
@@ -327,12 +325,7 @@ public class AccountSessionManager{
|
||||
@Override
|
||||
public void onSuccess(Instance instance){
|
||||
instances.put(domain, instance);
|
||||
updateInstanceEmojis(instance, domain);
|
||||
try {
|
||||
if (Integer.parseInt(instance.version.split("\\.")[0]) >= 4) {
|
||||
updateInstanceInfoV2(domain);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
updateMoreInstanceInfo(instance, domain);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -343,17 +336,19 @@ public class AccountSessionManager{
|
||||
.execNoAuth(domain);
|
||||
}
|
||||
|
||||
public void updateInstanceInfoV2(String domain) {
|
||||
public void updateMoreInstanceInfo(Instance instance, String domain) {
|
||||
new GetInstance.V2().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Instance.V2 v2) {
|
||||
Instance instanceInfo = instances.get(domain);
|
||||
if (instanceInfo != null) instanceInfo.v2 = v2;
|
||||
if (instance != null) instance.v2 = v2;
|
||||
updateInstanceEmojis(instance, domain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse errorResponse) {}
|
||||
}).execNoAuth(domain);
|
||||
public void onError(ErrorResponse errorResponse) {
|
||||
updateInstanceEmojis(instance, domain);
|
||||
}
|
||||
}).execNoAuth(instance.uri);
|
||||
}
|
||||
|
||||
private void updateInstanceEmojis(Instance instance, String domain){
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
|
||||
public class ScheduledStatusCreatedEvent {
|
||||
public final ScheduledStatus scheduledStatus;
|
||||
public final String accountID;
|
||||
|
||||
public ScheduledStatusCreatedEvent(ScheduledStatus scheduledStatus, String accountID){
|
||||
this.scheduledStatus = scheduledStatus;
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
|
||||
public class ScheduledStatusDeletedEvent{
|
||||
public final String id;
|
||||
public final String accountID;
|
||||
|
||||
public ScheduledStatusDeletedEvent(String id, String accountID){
|
||||
this.id=id;
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.announcements.GetAnnouncements;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetScheduledStatuses;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Announcement;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
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.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.PaginatedList;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class AnnouncementsFragment extends BaseStatusListFragment<Announcement> {
|
||||
private Instance instance;
|
||||
private AccountSession session;
|
||||
private List<String> unreadIDs = null;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.sk_announcements);
|
||||
session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||
instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Announcement a) {
|
||||
if(TextUtils.isEmpty(a.content)) return List.of();
|
||||
Account instanceUser = new Account();
|
||||
instanceUser.id = instanceUser.acct = instanceUser.username = session.domain;
|
||||
instanceUser.displayName = instance.title;
|
||||
instanceUser.url = "https://"+session.domain+"/about";
|
||||
instanceUser.avatar = instanceUser.avatarStatic = instance.thumbnail;
|
||||
instanceUser.emojis = List.of();
|
||||
Status fakeStatus = a.toStatus();
|
||||
TextStatusDisplayItem textItem = new TextStatusDisplayItem(a.id, HtmlParser.parse(a.content, a.emojis, a.mentions, a.tags, accountID), this, fakeStatus, true);
|
||||
textItem.textSelectable = true;
|
||||
return List.of(
|
||||
HeaderStatusDisplayItem.fromAnnouncement(a, fakeStatus, instanceUser, this, accountID, this::onMarkAsRead),
|
||||
textItem
|
||||
);
|
||||
}
|
||||
|
||||
public void onMarkAsRead(String id) {
|
||||
if (unreadIDs == null) return;
|
||||
unreadIDs.remove(id);
|
||||
if (unreadIDs.size() == 0) setResult(true, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(Announcement s) {}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id) {}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetAnnouncements(true)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Announcement> result){
|
||||
List<Announcement> unread = result.stream().filter(a -> !a.read).collect(toList());
|
||||
List<Announcement> read = result.stream().filter(a -> a.read).collect(toList());
|
||||
onDataLoaded(unread, true);
|
||||
onDataLoaded(read, false);
|
||||
unreadIDs = unread.stream().map(a -> a.id).collect(toList());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -787,7 +787,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
currentMediaHiddenLayoutsWidth=width;
|
||||
String title=getString(R.string.sensitive_content);
|
||||
TextPaint titlePaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
titlePaint.setColor(getResources().getColor(R.color.gray_50));
|
||||
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)
|
||||
@@ -798,7 +798,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||
.build();
|
||||
TextPaint textPaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
textPaint.setColor(getResources().getColor(R.color.gray_200));
|
||||
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)
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
|
||||
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
|
||||
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
|
||||
import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages;
|
||||
import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages;
|
||||
import static android.os.ext.SdkExtensions.getExtensionVersion;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.DatePickerDialog;
|
||||
import android.app.TimePickerDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -25,15 +30,17 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Layout;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -55,6 +62,7 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -67,13 +75,15 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.ProgressListener;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.EditStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID;
|
||||
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
@@ -85,6 +95,7 @@ import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.ComposeAutocompleteViewController;
|
||||
@@ -100,6 +111,7 @@ import org.joinmastodon.android.ui.utils.TransferSpeedTracker;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ComposeEditText;
|
||||
import org.joinmastodon.android.ui.views.ComposeMediaLayout;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
||||
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
|
||||
import org.joinmastodon.android.utils.MastodonLanguage;
|
||||
@@ -109,6 +121,12 @@ import org.parceler.Parcels;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -131,6 +149,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
private static final int MEDIA_RESULT=717;
|
||||
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
private static final int SCHEDULED_STATUS_OPENED_RESULT=161;
|
||||
private static final int MAX_ATTACHMENTS=4;
|
||||
private static final String TAG="ComposeFragment";
|
||||
|
||||
@@ -154,9 +173,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private String accountID;
|
||||
private int charCount, charLimit, trimmedCharCount;
|
||||
|
||||
private Button publishButton, languageButton;
|
||||
private PopupMenu languagePopup;
|
||||
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
|
||||
private Button publishButton, languageButton, scheduleTimeBtn, draftsBtn;
|
||||
private PopupMenu languagePopup, visibilityPopup, draftOptionsPopup;
|
||||
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss;
|
||||
private ImageView sensitiveIcon;
|
||||
private ComposeMediaLayout attachmentsView;
|
||||
private TextView replyText;
|
||||
@@ -165,8 +184,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private View addPollOptionBtn;
|
||||
private View sensitiveItem;
|
||||
private View pollAllowMultipleItem;
|
||||
private View scheduleDraftView;
|
||||
private ScrollView scrollView;
|
||||
private boolean initiallyScrolled = false;
|
||||
private TextView scheduleDraftText;
|
||||
private CheckBox pollAllowMultipleCheckbox;
|
||||
private TextView pollDurationView;
|
||||
private MenuItem draftMenuItem, undraftMenuItem, scheduleMenuItem, unscheduleMenuItem;
|
||||
|
||||
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
|
||||
|
||||
@@ -182,6 +206,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private EditText spoilerEdit;
|
||||
private boolean hasSpoiler;
|
||||
private boolean sensitive;
|
||||
private Instant scheduledAt = null;
|
||||
private ProgressBar sendProgress;
|
||||
private ImageView sendError;
|
||||
private View sendingOverlay;
|
||||
@@ -194,6 +219,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private boolean attachmentsErrorShowing;
|
||||
|
||||
private Status editingStatus;
|
||||
private ScheduledStatus scheduledStatus;
|
||||
private boolean redraftStatus;
|
||||
private boolean pollChanged;
|
||||
private boolean creatingView;
|
||||
@@ -215,9 +241,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
|
||||
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
|
||||
languageResolver=new MastodonLanguage.LanguageResolver(instance);
|
||||
redraftStatus=getArguments().getBoolean("redraftStatus", false);
|
||||
if(getArguments().containsKey("editStatus")){
|
||||
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
|
||||
redraftStatus=getArguments().getBoolean("redraftStatus");
|
||||
}
|
||||
if(instance==null){
|
||||
Nav.finish(this);
|
||||
@@ -227,6 +253,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
|
||||
}
|
||||
|
||||
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
|
||||
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
|
||||
if (bundle.containsKey("scheduledAt")) scheduledAt=(Instant) bundle.getSerializable("scheduledAt");
|
||||
|
||||
if(instance.maxTootChars>0)
|
||||
charLimit=instance.maxTootChars;
|
||||
else if(instance.configuration!=null && instance.configuration.statuses!=null && instance.configuration.statuses.maxCharacters>0)
|
||||
@@ -255,6 +285,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
wm=activity.getSystemService(WindowManager.class);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
creatingView=true;
|
||||
@@ -266,10 +297,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
mainEditTextWrap=view.findViewById(R.id.toot_text_wrap);
|
||||
charCounter=view.findViewById(R.id.char_counter);
|
||||
charCounter.setText(String.valueOf(charLimit));
|
||||
scrollView=view.findViewById(R.id.scroll_view);
|
||||
|
||||
selfName=view.findViewById(R.id.name);
|
||||
selfUsername=view.findViewById(R.id.username);
|
||||
selfAvatar=view.findViewById(R.id.avatar);
|
||||
selfName=view.findViewById(R.id.self_name);
|
||||
selfUsername=view.findViewById(R.id.self_username);
|
||||
selfAvatar=view.findViewById(R.id.self_avatar);
|
||||
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
|
||||
selfUsername.setText('@'+self.username+'@'+instanceDomain);
|
||||
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
|
||||
@@ -287,6 +319,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
emojiBtn=view.findViewById(R.id.btn_emoji);
|
||||
spoilerBtn=view.findViewById(R.id.btn_spoiler);
|
||||
visibilityBtn=view.findViewById(R.id.btn_visibility);
|
||||
scheduleDraftView=view.findViewById(R.id.schedule_draft_view);
|
||||
scheduleDraftText=view.findViewById(R.id.schedule_draft_text);
|
||||
scheduleDraftDismiss=view.findViewById(R.id.schedule_draft_dismiss);
|
||||
scheduleTimeBtn=view.findViewById(R.id.scheduled_time_btn);
|
||||
sensitiveIcon=view.findViewById(R.id.sensitive_icon);
|
||||
sensitiveItem=view.findViewById(R.id.sensitive_item);
|
||||
replyText=view.findViewById(R.id.reply_text);
|
||||
@@ -295,7 +331,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
pollBtn.setOnClickListener(v->togglePoll());
|
||||
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
||||
spoilerBtn.setOnClickListener(v->toggleSpoiler());
|
||||
visibilityBtn.setOnClickListener(this::onVisibilityClick);
|
||||
buildVisibilityPopup(visibilityBtn);
|
||||
visibilityBtn.setOnClickListener(v->visibilityPopup.show());
|
||||
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
|
||||
|
||||
scheduleDraftDismiss.setOnClickListener(v->updateScheduledAt(null));
|
||||
scheduleTimeBtn.setOnClickListener(v->pickScheduledDateTime());
|
||||
|
||||
sensitiveItem.setOnClickListener(v->toggleSensitive());
|
||||
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
|
||||
@Override
|
||||
@@ -345,8 +387,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
DraftPollOption opt=createDraftPollOption();
|
||||
opt.edit.setText(eopt.title);
|
||||
}
|
||||
pollDuration=(int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond();
|
||||
pollDurationStr=UiUtils.formatTimeLeft(getActivity(), editingStatus.poll.expiresAt);
|
||||
pollDuration=scheduledStatus == null
|
||||
? (int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond()
|
||||
: Integer.parseInt(scheduledStatus.params.poll.expiresIn);
|
||||
pollDurationStr=UiUtils.formatTimeLeft(getActivity(), scheduledStatus == null
|
||||
? editingStatus.poll.expiresAt
|
||||
: Instant.now().plus(pollDuration, ChronoUnit.SECONDS));
|
||||
updatePollOptionHints();
|
||||
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
|
||||
}else{
|
||||
@@ -368,6 +414,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
|
||||
sensitive = savedInstanceState==null && editingStatus != null ? editingStatus.sensitive
|
||||
: savedInstanceState!=null && savedInstanceState.getBoolean("sensitive", false);
|
||||
if (sensitive) {
|
||||
sensitiveItem.setVisibility(View.VISIBLE);
|
||||
sensitiveIcon.setSelected(true);
|
||||
}
|
||||
|
||||
if(savedInstanceState!=null && savedInstanceState.containsKey("attachments")){
|
||||
ArrayList<Parcelable> serializedAttachments=savedInstanceState.getParcelableArrayList("attachments");
|
||||
for(Parcelable a:serializedAttachments){
|
||||
@@ -385,10 +438,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
if(editingStatus!=null && editingStatus.visibility!=null) {
|
||||
statusVisibility=editingStatus.visibility;
|
||||
} else {
|
||||
loadDefaultStatusVisibility(savedInstanceState);
|
||||
}
|
||||
|
||||
loadDefaultStatusVisibility(savedInstanceState);
|
||||
updateVisibilityIcon();
|
||||
visibilityPopup.getMenu().findItem(switch(statusVisibility){
|
||||
case PUBLIC -> R.id.vis_public;
|
||||
case UNLISTED -> R.id.vis_unlisted;
|
||||
case PRIVATE -> R.id.vis_followers;
|
||||
case DIRECT -> R.id.vis_private;
|
||||
}).setChecked(true);
|
||||
|
||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
|
||||
@@ -425,6 +485,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
outState.putParcelableArrayList("attachments", serializedAttachments);
|
||||
}
|
||||
outState.putSerializable("visibility", statusVisibility);
|
||||
if (scheduledAt != null) outState.putSerializable("scheduledAt", scheduledAt);
|
||||
if (scheduledStatus != null) outState.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -444,6 +506,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
mainEditText.setSelectionListener(this);
|
||||
mainEditText.addTextChangedListener(new TextWatcher(){
|
||||
private int lastChangeStart, lastChangeCount;
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after){
|
||||
|
||||
@@ -453,6 +517,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||
if(s.length()==0)
|
||||
return;
|
||||
lastChangeStart=start;
|
||||
lastChangeCount=count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
if(s.length()==0)
|
||||
return;
|
||||
int start=lastChangeStart;
|
||||
int count=lastChangeCount;
|
||||
// offset one char back to catch an already typed '@' or '#' or ':'
|
||||
int realStart=start;
|
||||
start=Math.max(0, start-1);
|
||||
@@ -498,15 +572,84 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
editable.removeSpan(span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
updateCharCounter();
|
||||
}
|
||||
});
|
||||
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
|
||||
if(replyTo!=null){
|
||||
View replyWrap = view.findViewById(R.id.reply_wrap);
|
||||
scrollView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
||||
int scrollHeight = scrollView.getHeight();
|
||||
if (replyWrap.getMinimumHeight() != scrollHeight) {
|
||||
replyWrap.setMinimumHeight(scrollHeight);
|
||||
if (!initiallyScrolled) {
|
||||
initiallyScrolled = true;
|
||||
scrollView.post(() -> {
|
||||
int bottom = scrollView.getChildAt(0).getBottom();
|
||||
int delta = bottom - (scrollView.getScrollY() + scrollView.getHeight());
|
||||
int space = GlobalUserPreferences.reduceMotion ? 0 : Math.min(V.dp(70), delta);
|
||||
scrollView.scrollBy(0, delta - space);
|
||||
if (!GlobalUserPreferences.reduceMotion) {
|
||||
scrollView.postDelayed(() -> scrollView.smoothScrollBy(0, space), 130);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
View originalPost = view.findViewById(R.id.original_post);
|
||||
originalPost.setVisibility(View.VISIBLE);
|
||||
originalPost.setOnClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(replyTo));
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
});
|
||||
|
||||
ImageView avatar = view.findViewById(R.id.avatar);
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(replyTo.account.avatar));
|
||||
ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(12));
|
||||
}
|
||||
};
|
||||
avatar.setOutlineProvider(roundCornersOutline);
|
||||
avatar.setClipToOutline(true);
|
||||
avatar.setOnClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(replyTo.account));
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
});
|
||||
|
||||
((TextView) view.findViewById(R.id.name)).setText(replyTo.account.displayName);
|
||||
((TextView) view.findViewById(R.id.username)).setText(replyTo.account.getDisplayUsername());
|
||||
view.findViewById(R.id.visibility).setVisibility(View.GONE);
|
||||
Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
|
||||
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
|
||||
});
|
||||
ImageView moreBtn = view.findViewById(R.id.more);
|
||||
moreBtn.setImageDrawable(visibilityIcon);
|
||||
moreBtn.setBackground(null);
|
||||
TextView timestamp = view.findViewById(R.id.timestamp);
|
||||
if (replyTo.editedAt==null) timestamp.setText(UiUtils.formatRelativeTimestamp(getContext(), replyTo.createdAt));
|
||||
else timestamp.setText(getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(getContext(), replyTo.editedAt)));
|
||||
if (replyTo.spoilerText != null && !replyTo.spoilerText.isBlank()) {
|
||||
view.findViewById(R.id.spoiler_header).setVisibility(View.VISIBLE);
|
||||
((TextView) view.findViewById(R.id.spoiler_title_inline)).setText(replyTo.spoilerText);
|
||||
}
|
||||
|
||||
SpannableStringBuilder content = HtmlParser.parse(replyTo.content, replyTo.emojis, replyTo.mentions, replyTo.tags, accountID);
|
||||
LinkedTextView text = view.findViewById(R.id.text);
|
||||
if (content.length() > 0) text.setText(content);
|
||||
else view.findViewById(R.id.display_item_text).setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16)));
|
||||
|
||||
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
|
||||
int visibilityNameRes = switch (replyTo.visibility) {
|
||||
case PUBLIC -> R.string.visibility_public;
|
||||
@@ -515,24 +658,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
case DIRECT -> R.string.visibility_private;
|
||||
};
|
||||
replyText.setContentDescription(getString(R.string.in_reply_to, replyTo.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + getString(visibilityNameRes));
|
||||
Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
|
||||
case DIRECT -> R.drawable.ic_at_symbol;
|
||||
});
|
||||
visibilityIcon.setBounds(0, 0, V.dp(20), V.dp(20));
|
||||
Drawable replyArrow = getActivity().getDrawable(R.drawable.ic_fluent_arrow_reply_20_filled);
|
||||
replyArrow.setBounds(0, 0, V.dp(20), V.dp(20));
|
||||
replyText.setCompoundDrawables(replyArrow, null, visibilityIcon, null);
|
||||
|
||||
replyText.setOnClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(replyTo));
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
scrollView.smoothScrollTo(0, 0);
|
||||
});
|
||||
|
||||
ArrayList<String> mentions=new ArrayList<>();
|
||||
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
||||
if(!replyTo.account.id.equals(ownID))
|
||||
@@ -576,7 +705,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
DraftMediaAttachment da=new DraftMediaAttachment();
|
||||
da.serverAttachment=att;
|
||||
da.description=att.description;
|
||||
da.uri=Uri.parse(att.previewUrl);
|
||||
da.uri=att.previewUrl!=null ? Uri.parse(att.previewUrl) : null;
|
||||
da.state=AttachmentUploadState.DONE;
|
||||
attachmentsView.addView(createMediaAttachmentView(da));
|
||||
attachments.add(da);
|
||||
@@ -592,6 +721,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
ignoreSelectionChanges=false;
|
||||
initialText=prefilledText;
|
||||
}
|
||||
if (getArguments().containsKey("selectionStart") || getArguments().containsKey("selectionEnd")) {
|
||||
int selectionStart=getArguments().getInt("selectionStart", 0);
|
||||
int selectionEnd=getArguments().getInt("selectionEnd", selectionStart);
|
||||
mainEditText.setSelection(selectionStart, selectionEnd);
|
||||
}
|
||||
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
|
||||
if(mediaUris!=null && !mediaUris.isEmpty()){
|
||||
for(Uri uri:mediaUris){
|
||||
@@ -611,42 +745,59 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
publishButton=new Button(getActivity());
|
||||
publishButton.setText(editingStatus==null || redraftStatus ? R.string.publish : R.string.save);
|
||||
publishButton.setOnClickListener(this::onPublishClick);
|
||||
LinearLayout wrap=new LinearLayout(getActivity());
|
||||
wrap.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
||||
sendProgress=new ProgressBar(getActivity());
|
||||
LinearLayout.LayoutParams progressLP=new LinearLayout.LayoutParams(V.dp(24), V.dp(24));
|
||||
progressLP.setMarginEnd(V.dp(16));
|
||||
progressLP.gravity=Gravity.CENTER_VERTICAL;
|
||||
wrap.addView(sendProgress, progressLP);
|
||||
|
||||
sendError=new ImageView(getActivity());
|
||||
sendError.setImageResource(R.drawable.ic_fluent_error_circle_24_regular);
|
||||
sendError.setImageTintList(getResources().getColorStateList(R.color.error_600));
|
||||
sendError.setScaleType(ImageView.ScaleType.CENTER);
|
||||
wrap.addView(sendError, progressLP);
|
||||
|
||||
sendError.setVisibility(View.GONE);
|
||||
sendProgress.setVisibility(View.GONE);
|
||||
|
||||
LinearLayout.LayoutParams langParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
langParams.setMarginEnd(V.dp(8));
|
||||
wrap.addView(buildLanguageSelector(), langParams);
|
||||
|
||||
wrap.addView(publishButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
||||
wrap.setClipToPadding(false);
|
||||
MenuItem item=menu.add(editingStatus==null ? R.string.publish : R.string.save);
|
||||
LinearLayout wrap=new LinearLayout(getActivity());
|
||||
getActivity().getLayoutInflater().inflate(R.layout.compose_action, wrap);
|
||||
item.setActionView(wrap);
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
updatePublishButtonState();
|
||||
|
||||
draftsBtn = wrap.findViewById(R.id.drafts_btn);
|
||||
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn);
|
||||
draftOptionsPopup.inflate(R.menu.compose_more);
|
||||
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft);
|
||||
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft);
|
||||
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule);
|
||||
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule);
|
||||
draftOptionsPopup.setOnMenuItemClickListener(i->{
|
||||
int id = i.getItemId();
|
||||
if (id == R.id.draft) updateScheduledAt(getDraftInstant());
|
||||
else if (id == R.id.schedule) pickScheduledDateTime();
|
||||
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null);
|
||||
else navigateToUnsentPosts();
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
|
||||
|
||||
publishButton = wrap.findViewById(R.id.publish_btn);
|
||||
languageButton = wrap.findViewById(R.id.language_btn);
|
||||
sendProgress = wrap.findViewById(R.id.send_progress);
|
||||
sendError = wrap.findViewById(R.id.send_error);
|
||||
|
||||
publishButton.setOnClickListener(this::onPublishClick);
|
||||
draftsBtn.setOnClickListener(v-> draftOptionsPopup.show());
|
||||
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
|
||||
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
|
||||
buildLanguageSelector(languageButton);
|
||||
}
|
||||
|
||||
private void navigateToUnsentPosts() {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("hide_fab", true);
|
||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(draftsBtn.getWindowToken(), 0);
|
||||
if (hasDraft()) {
|
||||
Nav.go(getActivity(), ScheduledStatusListFragment.class, args);
|
||||
} else {
|
||||
// result for the previous ScheduledStatusList
|
||||
setResult(true, null);
|
||||
// finishing fragment in "onFragmentResult"
|
||||
Nav.goForResult(getActivity(), ScheduledStatusListFragment.class, args, SCHEDULED_STATUS_OPENED_RESULT, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLanguage(String lang) {
|
||||
updateLanguage(languageResolver.from(lang));
|
||||
updateLanguage(lang == null ? languageResolver.getDefault() : languageResolver.from(lang));
|
||||
}
|
||||
|
||||
private void updateLanguage(MastodonLanguage loc) {
|
||||
@@ -656,21 +807,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private Button buildLanguageSelector() {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
getActivity().getTheme().resolveAttribute(android.R.attr.textColorSecondary, typedValue, true);
|
||||
|
||||
languageButton=new Button(getActivity());
|
||||
languageButton.setTextColor(typedValue.data);
|
||||
languageButton.setBackground(getActivity().getDrawable(R.drawable.bg_text_button));
|
||||
languageButton.setPadding(V.dp(8), 0, V.dp(8), 0);
|
||||
languageButton.setCompoundDrawablesRelativeWithIntrinsicBounds(getActivity().getDrawable(R.drawable.ic_fluent_local_language_16_regular), null, null, null);
|
||||
languageButton.setCompoundDrawableTintList(languageButton.getTextColors());
|
||||
languageButton.setCompoundDrawablePadding(V.dp(6));
|
||||
|
||||
private void buildLanguageSelector(Button btn) {
|
||||
languagePopup=new PopupMenu(getActivity(), languageButton);
|
||||
languageButton.setOnTouchListener(languagePopup.getDragToOpenListener());
|
||||
languageButton.setOnClickListener(v->languagePopup.show());
|
||||
btn.setOnTouchListener(languagePopup.getDragToOpenListener());
|
||||
btn.setOnClickListener(v->languagePopup.show());
|
||||
|
||||
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
|
||||
updateLanguage(prefs != null && prefs.postingDefaultLanguage != null && prefs.postingDefaultLanguage.length() > 0
|
||||
@@ -694,8 +834,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
updateLanguage(allLanguages.get(i.getItemId()));
|
||||
return true;
|
||||
});
|
||||
|
||||
return languageButton;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -732,6 +870,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
updatePublishButtonState();
|
||||
}
|
||||
|
||||
private void resetPublishButtonText() {
|
||||
int publishText = editingStatus==null || redraftStatus ? R.string.publish : R.string.save;
|
||||
if (publishText == R.string.publish && !GlobalUserPreferences.publishButtonText.isEmpty()) {
|
||||
publishButton.setText(GlobalUserPreferences.publishButtonText);
|
||||
} else {
|
||||
publishButton.setText(publishText);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePublishButtonState(){
|
||||
uuid=null;
|
||||
int nonEmptyPollOptionsCount=0;
|
||||
@@ -747,6 +894,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
nonDoneAttachmentCount++;
|
||||
}
|
||||
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||
sendError.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void onCustomEmojiClick(Emoji emoji){
|
||||
@@ -765,15 +913,68 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
publish();
|
||||
}
|
||||
|
||||
private void publishErrorCallback(ErrorResponse error) {
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
sendProgress.setVisibility(View.GONE);
|
||||
sendError.setVisibility(View.VISIBLE);
|
||||
publishButton.setEnabled(true);
|
||||
if (error != null) error.showToast(getActivity());
|
||||
}
|
||||
|
||||
private void createScheduledStatusFinish(ScheduledStatus result) {
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
Toast.makeText(getContext(), scheduledAt.isAfter(DRAFTS_AFTER_INSTANT) ?
|
||||
R.string.sk_draft_saved : R.string.sk_post_scheduled, Toast.LENGTH_SHORT).show();
|
||||
Nav.finish(ComposeFragment.this);
|
||||
E.post(new ScheduledStatusCreatedEvent(result, accountID));
|
||||
}
|
||||
|
||||
private void maybeDeleteScheduledPost(Runnable callback) {
|
||||
if (scheduledStatus != null) {
|
||||
new DeleteStatus.Scheduled(scheduledStatus.id).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {
|
||||
E.post(new ScheduledStatusDeletedEvent(scheduledStatus.id, accountID));
|
||||
callback.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
publishErrorCallback(error);
|
||||
}
|
||||
}).exec(accountID);
|
||||
} else {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void publish(){
|
||||
publish(false);
|
||||
}
|
||||
|
||||
private void publish(boolean forceWithoutAltTexts){
|
||||
String text=mainEditText.getText().toString();
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
req.status=text;
|
||||
req.visibility=statusVisibility;
|
||||
req.sensitive=sensitive;
|
||||
req.language=language;
|
||||
req.scheduledAt = scheduledAt;
|
||||
if(!attachments.isEmpty()){
|
||||
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
|
||||
Optional<DraftMediaAttachment> withoutAltText = attachments.stream().filter(a -> a.description == null || a.description.isBlank()).findFirst();
|
||||
boolean isDraft = scheduledAt != null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT);
|
||||
if (!forceWithoutAltTexts && !GlobalUserPreferences.disableAltTextReminder && !isDraft && withoutAltText.isPresent()) {
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_alt_text_missing_title)
|
||||
.setMessage(R.string.sk_alt_text_missing)
|
||||
.setPositiveButton(R.string.add_alt_text, (d, w) -> editMediaDescription(withoutAltText.get()))
|
||||
.setNegativeButton(R.string.sk_publish_anyway, (d, w) -> publish(true))
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){
|
||||
req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id;
|
||||
@@ -808,35 +1009,32 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Callback<Status> resCallback=new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
if(editingStatus==null){
|
||||
E.post(new StatusCreatedEvent(result, accountID));
|
||||
if(replyTo!=null){
|
||||
replyTo.repliesCount++;
|
||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||
maybeDeleteScheduledPost(() -> {
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
if(editingStatus==null){
|
||||
E.post(new StatusCreatedEvent(result, accountID));
|
||||
if(replyTo!=null){
|
||||
replyTo.repliesCount++;
|
||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||
}
|
||||
}else{
|
||||
E.post(new StatusUpdatedEvent(result));
|
||||
}
|
||||
}else{
|
||||
E.post(new StatusUpdatedEvent(result));
|
||||
}
|
||||
Nav.finish(ComposeFragment.this);
|
||||
if (getArguments().getBoolean("navigateToStatus", false)) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(result));
|
||||
if(replyTo!=null) args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}
|
||||
Nav.finish(ComposeFragment.this);
|
||||
if (getArguments().getBoolean("navigateToStatus", false)) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(result));
|
||||
if(replyTo!=null) args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
sendProgress.setVisibility(View.GONE);
|
||||
sendError.setVisibility(View.VISIBLE);
|
||||
publishButton.setEnabled(true);
|
||||
error.showToast(getActivity());
|
||||
publishErrorCallback(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -844,10 +1042,37 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
new EditStatus(req, editingStatus.id)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
}else{
|
||||
}else if(req.scheduledAt == null){
|
||||
new CreateStatus(req, uuid)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
}else if(req.scheduledAt.isAfter(Instant.now().plus(10, ChronoUnit.MINUTES))){
|
||||
// checking for 10 instead of 5 minutes (as per mastodon) because i really don't want
|
||||
// bugs to occur because the client's clock is wrong by a minute or two - the api
|
||||
// returns a status instead of a scheduled status if scheduled time is less than 5
|
||||
// minutes into the future and this is 1. unexpected for the user and 2. hard to handle
|
||||
new CreateStatus.Scheduled(req, uuid)
|
||||
.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ScheduledStatus result) {
|
||||
maybeDeleteScheduledPost(() -> {
|
||||
createScheduledStatusFinish(result);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
publishErrorCallback(error);
|
||||
}
|
||||
}).exec(accountID);
|
||||
}else{
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_scheduled_too_soon_title)
|
||||
.setMessage(R.string.sk_scheduled_too_soon)
|
||||
.setPositiveButton(R.string.ok, (a, b)->{})
|
||||
.show();
|
||||
publishErrorCallback(null);
|
||||
publishButton.setEnabled(false);
|
||||
}
|
||||
|
||||
if (replyTo == null) {
|
||||
@@ -867,6 +1092,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
List<String> existingMediaIDs=editingStatus.mediaAttachments.stream().map(a->a.id).collect(Collectors.toList());
|
||||
if(!existingMediaIDs.equals(attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList())))
|
||||
return true;
|
||||
if(!statusVisibility.equals(editingStatus.visibility)) return true;
|
||||
if(scheduledStatus != null && !scheduledStatus.scheduledAt.equals(scheduledAt)) return true;
|
||||
return pollChanged;
|
||||
}
|
||||
boolean pollFieldsHaveContent=false;
|
||||
@@ -911,25 +1138,66 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (reqCode == SCHEDULED_STATUS_OPENED_RESULT && success && getActivity() != null) {
|
||||
Nav.finish(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmDiscardDraftAndFinish(){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes)
|
||||
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
|
||||
.setPositiveButton(R.string.save, (d, w) -> {
|
||||
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
|
||||
publish();
|
||||
})
|
||||
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if Android platform photopicker is available on the device\
|
||||
* @return whether the device supports photopicker intents.
|
||||
*/
|
||||
private boolean isPhotoPickerAvailable() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return true;
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return getExtensionVersion(Build.VERSION_CODES.R) >= 2;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the correct intent for the device version to select media.
|
||||
*
|
||||
* <p>For Device version > T or R_SDK_v2, use the android platform photopicker via
|
||||
* {@link MediaStore#ACTION_PICK_IMAGES}
|
||||
*
|
||||
* <p>For earlier versions use the built in docs ui via {@link Intent#ACTION_GET_CONTENT}
|
||||
*/
|
||||
private void openFilePicker(){
|
||||
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
if(instance.configuration!=null && instance.configuration.mediaAttachments!=null && instance.configuration.mediaAttachments.supportedMimeTypes!=null && !instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, instance.configuration.mediaAttachments.supportedMimeTypes.toArray(new String[0]));
|
||||
}else{
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
|
||||
Intent intent;
|
||||
boolean usePhotoPicker = isPhotoPickerAvailable();
|
||||
if (usePhotoPicker) {
|
||||
intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
|
||||
} else {
|
||||
intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
}
|
||||
if (!usePhotoPicker && instance.configuration != null &&
|
||||
instance.configuration.mediaAttachments != null &&
|
||||
instance.configuration.mediaAttachments.supportedMimeTypes != null &&
|
||||
!instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()) {
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES,
|
||||
instance.configuration.mediaAttachments.supportedMimeTypes.toArray(
|
||||
new String[0]));
|
||||
} else {
|
||||
if (!usePhotoPicker) {
|
||||
// If photo picker is being used these are the default mimetypes.
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
|
||||
}
|
||||
}
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
startActivityForResult(intent, MEDIA_RESULT);
|
||||
@@ -1012,7 +1280,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
|
||||
ImageView img=thumb.findViewById(R.id.thumb);
|
||||
if(draft.serverAttachment!=null){
|
||||
ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250)));
|
||||
if(draft.serverAttachment.previewUrl!=null)
|
||||
ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250)));
|
||||
}else{
|
||||
if(draft.mimeType.startsWith("image/")){
|
||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(draft.uri, V.dp(250), V.dp(250)));
|
||||
@@ -1097,7 +1366,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
@Override
|
||||
public void onProgress(long transferred, long total){
|
||||
if(updateUploadEtaRunnable==null){
|
||||
UiUtils.runOnUiThread(updateUploadEtaRunnable=ComposeFragment.this::updateUploadETAs, 100);
|
||||
// getting a NoSuchMethodError: No static method -$$Nest$mupdateUploadETAs(ComposeFragment;)V in class ComposeFragment
|
||||
// when using method reference out of nowhere after changing code elsewhere. no idea. programming is awful, actually
|
||||
// noinspection Convert2MethodRef
|
||||
UiUtils.runOnUiThread(updateUploadEtaRunnable=()->ComposeFragment.this.updateUploadETAs(), 50);
|
||||
}
|
||||
int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax());
|
||||
if(Build.VERSION.SDK_INT>=24)
|
||||
@@ -1260,13 +1532,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
att.uploadStateText.setText(getString(R.string.file_upload_time_remaining, time));
|
||||
}
|
||||
}
|
||||
UiUtils.runOnUiThread(updateUploadEtaRunnable, 100);
|
||||
UiUtils.runOnUiThread(updateUploadEtaRunnable, 50);
|
||||
}
|
||||
|
||||
private void onEditMediaDescriptionClick(View v){
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||
if(att.serverAttachment==null)
|
||||
return;
|
||||
editMediaDescription(att);
|
||||
}
|
||||
|
||||
private void editMediaDescription(DraftMediaAttachment att) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("attachment", att.serverAttachment.id);
|
||||
@@ -1392,23 +1668,69 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if (attachments.isEmpty()) sensitive = false;
|
||||
}
|
||||
|
||||
private void pickScheduledDateTime() {
|
||||
LocalDateTime soon = LocalDateTime.now()
|
||||
.plus(15, ChronoUnit.MINUTES) // so 14:59 doesn't get rounded up to…
|
||||
.plus(1, ChronoUnit.HOURS) // …15:00, but rather 16:00
|
||||
.withMinute(0);
|
||||
new DatePickerDialog(getActivity(), (datePicker, year, arrayMonth, dayOfMonth) -> {
|
||||
new TimePickerDialog(getActivity(), (timePicker, hour, minute) -> {
|
||||
updateScheduledAt(LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute)
|
||||
.toInstant(OffsetDateTime.now().getOffset()));
|
||||
}, soon.getHour(), soon.getMinute(), DateFormat.is24HourFormat(getActivity())).show();
|
||||
}, soon.getYear(), soon.getMonthValue() - 1, soon.getDayOfMonth()).show();
|
||||
}
|
||||
|
||||
private void updateScheduledAt(Instant scheduledAt) {
|
||||
this.scheduledAt = scheduledAt;
|
||||
updatePublishButtonState();
|
||||
scheduleDraftView.setVisibility(scheduledAt == null ? View.GONE : View.VISIBLE);
|
||||
draftMenuItem.setVisible(true);
|
||||
scheduleMenuItem.setVisible(true);
|
||||
undraftMenuItem.setVisible(false);
|
||||
unscheduleMenuItem.setVisible(false);
|
||||
if (scheduledAt != null) {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||
if (scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)) {
|
||||
draftMenuItem.setVisible(false);
|
||||
undraftMenuItem.setVisible(true);
|
||||
scheduleTimeBtn.setVisibility(View.GONE);
|
||||
scheduleDraftText.setText(R.string.sk_compose_draft);
|
||||
scheduleDraftText.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_drafts_20_regular, 0, 0, 0);
|
||||
scheduleDraftDismiss.setContentDescription(getString(R.string.sk_compose_no_draft));
|
||||
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_drafts_20_filled, 0, 0, 0);
|
||||
publishButton.setText(scheduledStatus != null && scheduledStatus.scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)
|
||||
? R.string.save : R.string.sk_draft);
|
||||
} else {
|
||||
scheduleMenuItem.setVisible(false);
|
||||
unscheduleMenuItem.setVisible(true);
|
||||
String at = scheduledAt.atZone(ZoneId.systemDefault()).format(formatter);
|
||||
scheduleTimeBtn.setVisibility(View.VISIBLE);
|
||||
scheduleTimeBtn.setText(at);
|
||||
scheduleDraftText.setText(R.string.sk_compose_scheduled);
|
||||
scheduleDraftText.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
scheduleDraftDismiss.setContentDescription(getString(R.string.sk_compose_no_schedule));
|
||||
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_clock_20_filled, 0, 0, 0);
|
||||
publishButton.setText(scheduledStatus != null && scheduledStatus.scheduledAt.equals(scheduledAt)
|
||||
? R.string.save : R.string.sk_schedule);
|
||||
}
|
||||
} else {
|
||||
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_clock_20_regular, 0, 0, 0);
|
||||
resetPublishButtonText();
|
||||
}
|
||||
}
|
||||
|
||||
private int getMediaAttachmentsCount(){
|
||||
return attachments.size();
|
||||
}
|
||||
|
||||
private void onVisibilityClick(View v){
|
||||
PopupMenu menu=new PopupMenu(getActivity(), v);
|
||||
menu.inflate(R.menu.compose_visibility);
|
||||
Menu m=menu.getMenu();
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), menu);
|
||||
private void buildVisibilityPopup(View v){
|
||||
visibilityPopup=new PopupMenu(getActivity(), v);
|
||||
visibilityPopup.inflate(R.menu.compose_visibility);
|
||||
Menu m=visibilityPopup.getMenu();
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
|
||||
m.setGroupCheckable(0, true, true);
|
||||
m.findItem(switch(statusVisibility){
|
||||
case PUBLIC -> R.id.vis_public;
|
||||
case UNLISTED -> R.id.vis_unlisted;
|
||||
case PRIVATE -> R.id.vis_followers;
|
||||
case DIRECT -> R.id.vis_private;
|
||||
}).setChecked(true);
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
|
||||
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
@@ -1426,7 +1748,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
return true;
|
||||
}
|
||||
});
|
||||
menu.show();
|
||||
}
|
||||
|
||||
private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
|
||||
@@ -1467,7 +1788,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_24_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular;
|
||||
case DIRECT -> R.drawable.ic_at_symbol;
|
||||
case DIRECT -> R.drawable.ic_fluent_mention_24_regular;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import static android.view.Menu.NONE;
|
||||
|
||||
import static org.joinmastodon.android.ui.utils.UiUtils.makeBackItem;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
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.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
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 me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class EditTimelinesFragment extends BaseRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
|
||||
private String accountID;
|
||||
private TimelinesAdapter adapter;
|
||||
private final ItemTouchHelper itemTouchHelper;
|
||||
private Menu optionsMenu;
|
||||
private boolean updated;
|
||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>();
|
||||
private final List<ListTimeline> listTimelines = new ArrayList<>();
|
||||
private final List<Hashtag> hashtags = new ArrayList<>();
|
||||
|
||||
public EditTimelinesFragment() {
|
||||
super(10);
|
||||
ItemTouchHelper.SimpleCallback itemTouchCallback = new ItemTouchHelperCallback() ;
|
||||
itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
setTitle(R.string.sk_timelines);
|
||||
accountID = getArguments().getString("account");
|
||||
|
||||
new GetLists().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> result) {
|
||||
listTimelines.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetFollowedHashtags().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result) {
|
||||
hashtags.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
itemTouchHelper.attachToRecyclerView(list);
|
||||
refreshLayout.setEnabled(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
this.optionsMenu = menu;
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_back) {
|
||||
updateOptionsMenu();
|
||||
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
|
||||
return true;
|
||||
}
|
||||
TimelineDefinition tl = timelineByMenuItem.get(item);
|
||||
if (tl != null) {
|
||||
data.add(tl.copy());
|
||||
adapter.notifyItemInserted(data.size());
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
timelineByMenuItem.put(item, tl);
|
||||
}
|
||||
|
||||
private void updateOptionsMenu() {
|
||||
optionsMenu.clear();
|
||||
timelineByMenuItem.clear();
|
||||
|
||||
SubMenu menu = optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
|
||||
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
|
||||
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline);
|
||||
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
||||
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list);
|
||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_list_24_regular);
|
||||
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag);
|
||||
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
||||
|
||||
makeBackItem(timelinesMenu);
|
||||
makeBackItem(listsMenu);
|
||||
makeBackItem(hashtagsMenu);
|
||||
|
||||
TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
||||
|
||||
timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0);
|
||||
listsMenu.getItem().setVisible(listsMenu.size() > 0);
|
||||
hashtagsMenu.getItem().setVisible(hashtagsMenu.size() > 0);
|
||||
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
|
||||
}
|
||||
|
||||
private void saveTimelines() {
|
||||
updated = true;
|
||||
GlobalUserPreferences.pinnedTimelines.put(accountID, data.size() > 0 ? data : List.of(TimelineDefinition.HOME_TIMELINE));
|
||||
GlobalUserPreferences.save();
|
||||
}
|
||||
|
||||
private void removeTimeline(int position) {
|
||||
data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter() {
|
||||
return adapter = new TimelinesAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (updated) UiUtils.restartApp();
|
||||
}
|
||||
|
||||
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new TimelineViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final ImageView dragger;
|
||||
|
||||
public TimelineViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
dragger=findViewById(R.id.dragger_thingy);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onBind(TimelineDefinition item) {
|
||||
title.setText(item.getTitle(getContext()));
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
|
||||
dragger.setVisibility(View.VISIBLE);
|
||||
dragger.setOnTouchListener((View v, MotionEvent event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
itemTouchHelper.startDrag(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
editText.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
|
||||
public ItemTouchHelperCallback() {
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
||||
int fromPosition = viewHolder.getAbsoluteAdapterPosition();
|
||||
int toPosition = target.getAbsoluteAdapterPosition();
|
||||
if (Math.max(fromPosition, toPosition) >= data.size() || Math.min(fromPosition, toPosition) < 0) {
|
||||
return false;
|
||||
} else {
|
||||
Collections.swap(data, fromPosition, toPosition);
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
saveTimelines();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
|
||||
viewHolder.itemView.animate().alpha(0.65f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().alpha(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
int position = viewHolder.getAbsoluteAdapterPosition();
|
||||
removeTimeline(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public abstract class FabStatusListFragment extends StatusListFragment {
|
||||
protected ImageButton fab;
|
||||
|
||||
public FabStatusListFragment() {
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab = view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(this::onFabLongClick);
|
||||
}
|
||||
|
||||
protected void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
protected boolean onFabLongClick(View v) {
|
||||
return UiUtils.pickAccountForCompose(getActivity(), accountID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class FollowedHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop {
|
||||
private String nextMaxID;
|
||||
private String accountId;
|
||||
|
||||
public FollowedHashtagsFragment() {
|
||||
super(20);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle args=getArguments();
|
||||
accountId=args.getString("account");
|
||||
setTitle(R.string.sk_hashtags_you_follow);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetFollowedHashtags(offset==0 ? null : nextMaxID, null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter() {
|
||||
return new HashtagsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public HashtagViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new HashtagViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull HashtagViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class HashtagViewHolder extends BindableViewHolder<Hashtag> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
|
||||
public HashtagViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Hashtag item) {
|
||||
title.setText(item.name);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_number_symbol_24_regular), null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -16,6 +17,8 @@ import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,7 +28,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HashtagTimelineFragment extends StatusListFragment{
|
||||
public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
private String hashtag;
|
||||
private boolean following;
|
||||
private ImageButton fab;
|
||||
@@ -40,7 +43,6 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
super.onAttach(activity);
|
||||
updateTitle(getArguments().getString("hashtag"));
|
||||
following=getArguments().getBoolean("following", false);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@@ -58,11 +60,31 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.hashtag_timeline, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
followButton = menu.findItem(R.id.follow_hashtag);
|
||||
updateFollowingState(following);
|
||||
|
||||
followButton.setOnMenuItemClickListener(i -> {
|
||||
new GetHashtag(hashtag).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag hashtag) {
|
||||
updateTitle(hashtag.name);
|
||||
updateFollowingState(hashtag.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (super.onOptionsItemSelected(item)) return true;
|
||||
if (item.getItemId() == R.id.follow_hashtag) {
|
||||
updateFollowingState(!following);
|
||||
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
|
||||
new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag i) {
|
||||
@@ -77,20 +99,13 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}).exec(accountID);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
new GetHashtag(hashtag).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag hashtag) {
|
||||
updateTitle(hashtag.name);
|
||||
updateFollowingState(hashtag.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
@Override
|
||||
protected TimelineDefinition makeTimelineDefinition() {
|
||||
return TimelineDefinition.ofHashtag(hashtag);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,6 +132,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' '));
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.NotificationManager;
|
||||
import android.graphics.Outline;
|
||||
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;
|
||||
@@ -20,6 +21,7 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.discover.SearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -41,7 +43,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||
private FragmentRootLinearLayout content;
|
||||
private HomeTimelineFragment homeTimelineFragment;
|
||||
private HomeTabFragment homeTabFragment;
|
||||
private NotificationsFragment notificationsFragment;
|
||||
private DiscoverFragment searchFragment;
|
||||
private ProfileFragment profileFragment;
|
||||
@@ -65,8 +67,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
if(savedInstanceState==null){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
homeTimelineFragment=new HomeTimelineFragment();
|
||||
homeTimelineFragment.setArguments(args);
|
||||
homeTabFragment=new HomeTabFragment();
|
||||
homeTabFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
searchFragment=new DiscoverFragment();
|
||||
@@ -110,7 +112,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
if(savedInstanceState==null){
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(R.id.fragment_wrap, homeTimelineFragment)
|
||||
.add(R.id.fragment_wrap, homeTabFragment)
|
||||
.add(R.id.fragment_wrap, searchFragment).hide(searchFragment)
|
||||
.add(R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment)
|
||||
.add(R.id.fragment_wrap, profileFragment).hide(profileFragment)
|
||||
@@ -136,16 +138,15 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
@Override
|
||||
public void onViewStateRestored(Bundle savedInstanceState){
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
if(savedInstanceState==null || homeTimelineFragment!=null)
|
||||
return;
|
||||
homeTimelineFragment=(HomeTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTimelineFragment");
|
||||
if(savedInstanceState==null) return;
|
||||
homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment");
|
||||
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
|
||||
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
||||
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
||||
currentTab=savedInstanceState.getInt("selectedTab");
|
||||
Fragment current=fragmentForTab(currentTab);
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.hide(homeTimelineFragment)
|
||||
.hide(homeTabFragment)
|
||||
.hide(searchFragment)
|
||||
.hide(notificationsFragment)
|
||||
.hide(profileFragment)
|
||||
@@ -180,7 +181,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0);
|
||||
homeTimelineFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
homeTabFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
searchFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
notificationsFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
profileFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
@@ -188,7 +189,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
private Fragment fragmentForTab(@IdRes int tab){
|
||||
if(tab==R.id.tab_home){
|
||||
return homeTimelineFragment;
|
||||
return homeTabFragment;
|
||||
}else if(tab==R.id.tab_search){
|
||||
return searchFragment;
|
||||
}else if(tab==R.id.tab_notifications){
|
||||
@@ -202,7 +203,9 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
private void onTabSelected(@IdRes int tab){
|
||||
Fragment newFragment=fragmentForTab(tab);
|
||||
if(tab==currentTab){
|
||||
if(newFragment instanceof ScrollableToTop scrollable)
|
||||
if (tab == R.id.tab_search)
|
||||
searchFragment.onSelect();
|
||||
else if(newFragment instanceof ScrollableToTop scrollable)
|
||||
scrollable.scrollToTop();
|
||||
return;
|
||||
}
|
||||
@@ -222,7 +225,11 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
((NotificationsFragment) newFragment).loadData();
|
||||
// TODO make an interface?
|
||||
NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
|
||||
nm.cancel(accountID, PushNotificationReceiver.NOTIFICATION_ID);
|
||||
for (StatusBarNotification notification : nm.getActiveNotifications()) {
|
||||
if (accountID.equals(notification.getTag())) {
|
||||
nm.cancel(accountID, notification.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,17 +255,18 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
tabBar.selectTab(R.id.tab_home);
|
||||
onTabSelected(R.id.tab_home);
|
||||
return true;
|
||||
} else {
|
||||
return homeTabFragment.onBackPressed();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("selectedTab", currentTab);
|
||||
getChildFragmentManager().putFragment(outState, "homeTimelineFragment", homeTimelineFragment);
|
||||
getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment);
|
||||
getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
|
||||
getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
|
||||
if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment);
|
||||
if (searchFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment);
|
||||
if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
|
||||
if (profileFragment.isAdded()) getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
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.announcements.GetAnnouncements;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.Announcement;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener {
|
||||
private static final int ANNOUNCEMENTS_RESULT = 654;
|
||||
private static final int PINNED_UPDATED_RESULT = 523;
|
||||
|
||||
private String accountID;
|
||||
private MenuItem announcements;
|
||||
// private ImageView toolbarLogo;
|
||||
private Button toolbarShowNewPostsBtn;
|
||||
private boolean newPostsBtnShown;
|
||||
private AnimatorSet currentNewPostsAnim;
|
||||
private ViewPager2 pager;
|
||||
private View switcher;
|
||||
private FrameLayout toolbarFrame;
|
||||
private ImageView timelineIcon;
|
||||
private ImageView collapsedChevron;
|
||||
private TextView timelineTitle;
|
||||
private PopupMenu switcherPopup;
|
||||
private final Map<Integer, ListTimeline> listItems = new HashMap<>();
|
||||
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
|
||||
private List<TimelineDefinition> timelineDefinitions;
|
||||
private int count;
|
||||
private Fragment[] fragments;
|
||||
private FrameLayout[] tabViews;
|
||||
private TimelineDefinition[] timelines;
|
||||
private Map<Integer, TimelineDefinition> timelinesByMenuItem = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID = getArguments().getString("account");
|
||||
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES);
|
||||
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];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
FrameLayout view = new FrameLayout(getContext());
|
||||
pager = new ViewPager2(getContext());
|
||||
toolbarFrame = (FrameLayout) LayoutInflater.from(getContext()).inflate(R.layout.home_toolbar, getToolbar(), false);
|
||||
|
||||
if (fragments[0] == null) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("__is_tab", true);
|
||||
args.putBoolean("onlyPosts", true);
|
||||
|
||||
for (int i = 0; i < timelineDefinitions.size(); i++) {
|
||||
TimelineDefinition tl = timelineDefinitions.get(i);
|
||||
fragments[i] = tl.getFragment();
|
||||
timelines[i] = tl;
|
||||
}
|
||||
|
||||
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
|
||||
for (int i = 0; i < count; i++) {
|
||||
fragments[i].setArguments(timelines[i].populateArguments(new Bundle(args)));
|
||||
FrameLayout tabView = new FrameLayout(getActivity());
|
||||
tabView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
tabView.setVisibility(View.GONE);
|
||||
tabView.setId(i + 1);
|
||||
transaction.add(i + 1, fragments[i]);
|
||||
view.addView(tabView);
|
||||
tabViews[i] = tabView;
|
||||
}
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
view.addView(pager, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
timelineIcon = toolbarFrame.findViewById(R.id.timeline_icon);
|
||||
timelineTitle = toolbarFrame.findViewById(R.id.timeline_title);
|
||||
collapsedChevron = toolbarFrame.findViewById(R.id.collapsed_chevron);
|
||||
switcher = toolbarFrame.findViewById(R.id.switcher_btn);
|
||||
switcherPopup = new PopupMenu(getContext(), switcher);
|
||||
switcherPopup.setOnMenuItemClickListener(this::onSwitcherItemSelected);
|
||||
UiUtils.enablePopupMenuIcons(getContext(), switcherPopup);
|
||||
switcher.setOnClickListener(v->{
|
||||
updateSwitcherMenu();
|
||||
switcherPopup.show();
|
||||
});
|
||||
View.OnTouchListener listener = switcherPopup.getDragToOpenListener();
|
||||
switcher.setOnTouchListener((v, m)-> {
|
||||
updateSwitcherMenu();
|
||||
return listener.onTouch(v, m);
|
||||
});
|
||||
|
||||
UiUtils.reduceSwipeSensitivity(pager);
|
||||
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
|
||||
pager.setAdapter(new HomePagerAdapter());
|
||||
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
||||
@Override
|
||||
public void onPageSelected(int position){
|
||||
updateSwitcherIcon(position);
|
||||
if (!timelines[position].equals(TimelineDefinition.HOME_TIMELINE)) hideNewPostsButton();
|
||||
if (fragments[position] instanceof BaseRecyclerFragment<?> page){
|
||||
if(!page.loaded && !page.isDataLoading()) page.loadData();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!GlobalUserPreferences.reduceMotion) {
|
||||
pager.setPageTransformer((v, pos) -> {
|
||||
if (tabViews[pager.getCurrentItem()] != v) return;
|
||||
float scaleFactor = Math.max(0.85f, 1 - Math.abs(pos) * 0.06f);
|
||||
switcher.setScaleY(scaleFactor);
|
||||
switcher.setScaleX(scaleFactor);
|
||||
switcher.setAlpha(Math.max(0.65f, 1 - Math.abs(pos)));
|
||||
});
|
||||
}
|
||||
|
||||
updateToolbarLogo();
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.register(this);
|
||||
updateUpdateState(GithubSelfUpdater.getInstance().getState());
|
||||
}
|
||||
|
||||
new GetLists().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
addItemsToMap(lists, listItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetFollowedHashtags().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> hashtags) {
|
||||
addItemsToMap(hashtags, hashtagsItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
public void updateToolbarLogo(){
|
||||
Toolbar toolbar = getToolbar();
|
||||
ViewParent parentView = toolbarFrame.getParent();
|
||||
if (parentView == toolbar) return;
|
||||
if (parentView instanceof Toolbar parentToolbar) parentToolbar.removeView(toolbarFrame);
|
||||
toolbar.addView(toolbarFrame, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
toolbar.setOnClickListener(v->scrollToTop());
|
||||
toolbar.setNavigationContentDescription(R.string.back);
|
||||
toolbar.setContentInsetsAbsolute(0, toolbar.getContentInsetRight());
|
||||
|
||||
updateSwitcherIcon(pager.getCurrentItem());
|
||||
|
||||
// toolbarLogo=new ImageView(getActivity());
|
||||
// toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
|
||||
// toolbarLogo.setImageResource(R.drawable.logo);
|
||||
// toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
|
||||
|
||||
toolbarShowNewPostsBtn=toolbarFrame.findViewById(R.id.show_new_posts_btn);
|
||||
toolbarShowNewPostsBtn.setCompoundDrawableTintList(toolbarShowNewPostsBtn.getTextColors());
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N) UiUtils.fixCompoundDrawableTintOnAndroid6(toolbarShowNewPostsBtn);
|
||||
toolbarShowNewPostsBtn.setOnClickListener(this::onNewPostsBtnClick);
|
||||
|
||||
if(newPostsBtnShown){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
collapsedChevron.setVisibility(View.VISIBLE);
|
||||
collapsedChevron.setAlpha(1f);
|
||||
timelineTitle.setVisibility(View.GONE);
|
||||
timelineTitle.setAlpha(0f);
|
||||
}else{
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
toolbarShowNewPostsBtn.setAlpha(0f);
|
||||
collapsedChevron.setVisibility(View.GONE);
|
||||
collapsedChevron.setAlpha(0f);
|
||||
toolbarShowNewPostsBtn.setScaleX(.8f);
|
||||
toolbarShowNewPostsBtn.setScaleY(.8f);
|
||||
timelineTitle.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
ViewTreeObserver vto = toolbar.getViewTreeObserver();
|
||||
if (vto.isAlive()) {
|
||||
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
Toolbar t = getToolbar();
|
||||
if (t == null) return;
|
||||
int toolbarWidth = t.getWidth();
|
||||
if (toolbarWidth == 0) return;
|
||||
t.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
|
||||
int toolbarFrameWidth = toolbarFrame.getWidth();
|
||||
int padding = toolbarWidth - toolbarFrameWidth;
|
||||
// toolbar frame goes from screen edge to beginning of right-aligned option buttons.
|
||||
// centering button by applying the same space on the left
|
||||
((FrameLayout) toolbarShowNewPostsBtn.getParent()).setPaddingRelative(padding, 0, 0, 0);
|
||||
toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth - padding * 2);
|
||||
|
||||
switcher.setPivotX(V.dp(28)); // padding + half of icon
|
||||
switcher.setPivotY(switcher.getHeight() / 2f);
|
||||
timelineTitle.setPivotX(timelineTitle.getWidth() - V.dp(8));
|
||||
timelineTitle.setPivotY(timelineTitle.getHeight() / 2f);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.home, menu);
|
||||
announcements = menu.findItem(R.id.announcements);
|
||||
|
||||
new GetAnnouncements(false).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Announcement> result) {
|
||||
boolean hasUnread = result.stream().anyMatch(a -> !a.read);
|
||||
announcements.setIcon(hasUnread ? R.drawable.ic_announcements_24_badged : R.drawable.ic_fluent_megaphone_24_regular);
|
||||
updateBadgedOptionsItem(announcements, hasUnread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), menu);
|
||||
}
|
||||
|
||||
private void updateBadgedOptionsItem(MenuItem item, boolean asAction) {
|
||||
item.setShowAsAction(asAction ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER);
|
||||
if (asAction) {
|
||||
UiUtils.resetPopupItemTint(item);
|
||||
} else {
|
||||
UiUtils.insetPopupMenuIcon(getContext(), item);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void addItemsToMap(List<T> addItems, Map<Integer, T> items) {
|
||||
if (addItems.size() == 0) return;
|
||||
for (int i = 0; i < addItems.size(); i++) items.put(View.generateViewId(), addItems.get(i));
|
||||
updateSwitcherMenu();
|
||||
}
|
||||
|
||||
private void updateSwitcherMenu() {
|
||||
Context context = getContext();
|
||||
Menu switcherMenu = switcherPopup.getMenu();
|
||||
switcherMenu.clear();
|
||||
timelinesByMenuItem.clear();
|
||||
|
||||
for (TimelineDefinition tl : timelines) {
|
||||
int menuItemId = View.generateViewId();
|
||||
timelinesByMenuItem.put(menuItemId, tl);
|
||||
MenuItem item = switcherMenu.add(0, menuItemId, 0, tl.getTitle(getContext()));
|
||||
item.setIcon(tl.getIcon().iconRes);
|
||||
UiUtils.insetPopupMenuIcon(getContext(), item);
|
||||
}
|
||||
|
||||
if (!listItems.isEmpty()) {
|
||||
SubMenu listsMenu = switcherMenu.addSubMenu(R.string.sk_list_timelines);
|
||||
UiUtils.insetPopupMenuIcon(context, listsMenu.getItem().setVisible(true)
|
||||
.setIcon(R.drawable.ic_fluent_people_list_24_regular));
|
||||
listsMenu.clear();
|
||||
UiUtils.insetPopupMenuIcon(context, UiUtils.makeBackItem(listsMenu));
|
||||
listItems.forEach((id, list) -> {
|
||||
MenuItem item = listsMenu.add(Menu.NONE, id, Menu.NONE, list.title);
|
||||
item.setIcon(R.drawable.ic_fluent_people_list_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(context, item);
|
||||
});
|
||||
}
|
||||
|
||||
if (!hashtagsItems.isEmpty()) {
|
||||
SubMenu hashtagsMenu = switcherMenu.addSubMenu(R.string.sk_hashtags_you_follow);
|
||||
UiUtils.insetPopupMenuIcon(context, hashtagsMenu.getItem().setVisible(true)
|
||||
.setIcon(R.drawable.ic_fluent_number_symbol_24_regular));
|
||||
hashtagsMenu.clear();
|
||||
UiUtils.insetPopupMenuIcon(context, UiUtils.makeBackItem(hashtagsMenu));
|
||||
hashtagsItems.forEach((id, hashtag) -> {
|
||||
MenuItem item = hashtagsMenu.add(Menu.NONE, id, Menu.NONE, hashtag.name);
|
||||
item.setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(context, item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onSwitcherItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
ListTimeline list;
|
||||
Hashtag hashtag;
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString("account", accountID);
|
||||
|
||||
if (id == R.id.menu_back) {
|
||||
switcher.post(() -> switcherPopup.show());
|
||||
return true;
|
||||
} else if ((list = listItems.get(id)) != null) {
|
||||
args.putString("listID", list.id);
|
||||
args.putString("listTitle", list.title);
|
||||
args.putInt("repliesPolicy", list.repliesPolicy.ordinal());
|
||||
Nav.goForResult(getActivity(), ListTimelineFragment.class, args, PINNED_UPDATED_RESULT, this);
|
||||
} else if ((hashtag = hashtagsItems.get(id)) != null) {
|
||||
args.putString("hashtag", hashtag.name);
|
||||
args.putBoolean("following", hashtag.following);
|
||||
Nav.goForResult(getActivity(), HashtagTimelineFragment.class, args, PINNED_UPDATED_RESULT, this);
|
||||
} else {
|
||||
TimelineDefinition tl = timelinesByMenuItem.get(id);
|
||||
if (tl != null) {
|
||||
for (int i = 0; i < timelines.length; i++) {
|
||||
if (timelines[i] == tl) {
|
||||
navigateTo(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private void navigateTo(int i) {
|
||||
navigateTo(i, !GlobalUserPreferences.reduceMotion);
|
||||
}
|
||||
|
||||
private void navigateTo(int i, boolean smooth) {
|
||||
pager.setCurrentItem(i, smooth);
|
||||
updateSwitcherIcon(i);
|
||||
}
|
||||
|
||||
private void updateSwitcherIcon(int i) {
|
||||
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
||||
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.settings) {
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
} else if (id == R.id.announcements) {
|
||||
Nav.goForResult(getActivity(), AnnouncementsFragment.class, args, ANNOUNCEMENTS_RESULT, this);
|
||||
} else if (id == R.id.edit_timelines) {
|
||||
Nav.go(getActivity(), EditTimelinesFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
((ScrollableToTop) fragments[pager.getCurrentItem()]).scrollToTop();
|
||||
}
|
||||
|
||||
public void hideNewPostsButton(){
|
||||
if(!newPostsBtnShown)
|
||||
return;
|
||||
newPostsBtnShown=false;
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
timelineTitle.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(timelineTitle, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(timelineTitle, View.SCALE_X, 1f),
|
||||
ObjectAnimator.ofFloat(timelineTitle, View.SCALE_Y, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, .8f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f),
|
||||
ObjectAnimator.ofFloat(collapsedChevron, View.ALPHA, 0f)
|
||||
);
|
||||
set.setDuration(GlobalUserPreferences.reduceMotion ? 0 : 300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
collapsedChevron.setVisibility(View.GONE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
currentNewPostsAnim=set;
|
||||
set.start();
|
||||
}
|
||||
|
||||
public void showNewPostsButton(){
|
||||
if(newPostsBtnShown || pager == null || !timelines[pager.getCurrentItem()].equals(TimelineDefinition.HOME_TIMELINE))
|
||||
return;
|
||||
newPostsBtnShown=true;
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
collapsedChevron.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(timelineTitle, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(timelineTitle, View.SCALE_X, .8f),
|
||||
ObjectAnimator.ofFloat(timelineTitle, View.SCALE_Y, .8f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f),
|
||||
ObjectAnimator.ofFloat(collapsedChevron, View.ALPHA, 1f)
|
||||
);
|
||||
set.setDuration(GlobalUserPreferences.reduceMotion ? 0 : 300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
timelineTitle.setVisibility(View.GONE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
currentNewPostsAnim=set;
|
||||
set.start();
|
||||
}
|
||||
|
||||
public boolean isNewPostsBtnShown() {
|
||||
return newPostsBtnShown;
|
||||
}
|
||||
|
||||
private void onNewPostsBtnClick(View view) {
|
||||
if(newPostsBtnShown){
|
||||
hideNewPostsButton();
|
||||
scrollToTop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
if (reqCode == ANNOUNCEMENTS_RESULT && success) {
|
||||
announcements.setIcon(R.drawable.ic_fluent_megaphone_24_regular);
|
||||
announcements.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
|
||||
UiUtils.insetPopupMenuIcon(getContext(), announcements);
|
||||
} else if (reqCode == PINNED_UPDATED_RESULT && result != null && result.getBoolean("pinnedUpdated", false)) {
|
||||
UiUtils.restartApp();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING) {
|
||||
MenuItem settings = getToolbar().getMenu().findItem(R.id.settings);
|
||||
settings.setIcon(R.drawable.ic_settings_24_badged);
|
||||
updateBadgedOptionsItem(settings, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateState(ev.state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(pager.getCurrentItem() > 0){
|
||||
navigateTo(0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
if (savedInstanceState == null) return;
|
||||
navigateTo(savedInstanceState.getInt("selectedTab"), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("selectedTab", pager.getCurrentItem());
|
||||
}
|
||||
|
||||
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
|
||||
@NonNull
|
||||
@Override
|
||||
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
FrameLayout tabView = tabViews[viewType % getItemCount()];
|
||||
((ViewGroup)tabView.getParent()).removeView(tabView);
|
||||
tabView.setVisibility(View.VISIBLE);
|
||||
return new SimpleViewHolder(tabView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +1,46 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTimelineFragment extends StatusListFragment{
|
||||
private ImageButton fab;
|
||||
private ImageView toolbarLogo;
|
||||
private Button toolbarShowNewPostsBtn;
|
||||
private boolean newPostsBtnShown;
|
||||
private AnimatorSet currentNewPostsAnim;
|
||||
|
||||
public class HomeTimelineFragment extends FabStatusListFragment {
|
||||
private HomeTabFragment parent;
|
||||
private String maxID;
|
||||
|
||||
public HomeTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
private String lastSavedMarkerID;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setHasOptionsMenu(true);
|
||||
if (getParentFragment() instanceof HomeTabFragment home) parent = home;
|
||||
loadData();
|
||||
}
|
||||
|
||||
@@ -104,41 +72,15 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
updateToolbarLogo();
|
||||
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
if(newPostsBtnShown && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
|
||||
hideNewPostsButton();
|
||||
if(parent != null && parent.isNewPostsBtnShown() && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
|
||||
parent.hideNewPostsButton();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.register(this);
|
||||
updateUpdateState(GithubSelfUpdater.getInstance().getState());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.home, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig){
|
||||
super.onConfigurationChanged(newConfig);
|
||||
updateToolbarLogo();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,14 +95,31 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!data.isEmpty()){
|
||||
String topPostID=displayItems.get(list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset()).parentID;
|
||||
if(!topPostID.equals(lastSavedMarkerID)){
|
||||
lastSavedMarkerID=topPostID;
|
||||
new SaveMarkers(topPostID, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SaveMarkers.Response result){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
lastSavedMarkerID=null;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
}
|
||||
|
||||
private void loadNewPosts(){
|
||||
@@ -187,13 +146,11 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!filters.isEmpty()){
|
||||
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
|
||||
}
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
|
||||
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
showNewPostsButton();
|
||||
if (parent != null) parent.showNewPostsButton();
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
}
|
||||
}
|
||||
@@ -264,18 +221,14 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
outer:
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
|
||||
for(Status s:result){
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(s)){
|
||||
continue outer;
|
||||
}
|
||||
if(filterPredicate.test(s)){
|
||||
targetList.addAll(buildDisplayItems(s));
|
||||
insertedPosts.add(s);
|
||||
}
|
||||
targetList.addAll(buildDisplayItems(s));
|
||||
insertedPosts.add(s);
|
||||
}
|
||||
if(targetList.isEmpty()){
|
||||
// oops. We didn't add new posts, but at least we know there are none.
|
||||
@@ -313,131 +266,10 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
}
|
||||
if (parent != null) parent.hideNewPostsButton();
|
||||
super.onRefresh();
|
||||
}
|
||||
|
||||
private void updateToolbarLogo(){
|
||||
toolbarLogo=new ImageView(getActivity());
|
||||
toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
|
||||
toolbarLogo.setImageResource(R.drawable.logo);
|
||||
toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
|
||||
|
||||
toolbarShowNewPostsBtn=new Button(getActivity());
|
||||
toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
|
||||
toolbarShowNewPostsBtn.setTextColor(0xffffffff);
|
||||
toolbarShowNewPostsBtn.setStateListAnimator(null);
|
||||
toolbarShowNewPostsBtn.setBackgroundResource(R.drawable.bg_button_new_posts);
|
||||
toolbarShowNewPostsBtn.setText(R.string.see_new_posts);
|
||||
toolbarShowNewPostsBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_fluent_arrow_up_16_filled, 0, 0, 0);
|
||||
toolbarShowNewPostsBtn.setCompoundDrawableTintList(toolbarShowNewPostsBtn.getTextColors());
|
||||
toolbarShowNewPostsBtn.setCompoundDrawablePadding(V.dp(8));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(toolbarShowNewPostsBtn);
|
||||
toolbarShowNewPostsBtn.setOnClickListener(this::onNewPostsBtnClick);
|
||||
|
||||
if(newPostsBtnShown){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
toolbarLogo.setAlpha(0f);
|
||||
}else{
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
toolbarShowNewPostsBtn.setAlpha(0f);
|
||||
toolbarShowNewPostsBtn.setScaleX(.8f);
|
||||
toolbarShowNewPostsBtn.setScaleY(.8f);
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
FrameLayout logoWrap=new FrameLayout(getActivity());
|
||||
FrameLayout.LayoutParams logoParams=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER);
|
||||
logoParams.setMargins(0, V.dp(2), 0, 0);
|
||||
logoWrap.addView(toolbarLogo, logoParams);
|
||||
logoWrap.addView(toolbarShowNewPostsBtn, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(32), Gravity.CENTER));
|
||||
|
||||
Toolbar toolbar=getToolbar();
|
||||
toolbar.addView(logoWrap, new Toolbar.LayoutParams(Gravity.CENTER));
|
||||
}
|
||||
|
||||
private void showNewPostsButton(){
|
||||
if(newPostsBtnShown)
|
||||
return;
|
||||
newPostsBtnShown=true;
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
currentNewPostsAnim=set;
|
||||
set.start();
|
||||
}
|
||||
|
||||
private void hideNewPostsButton(){
|
||||
if(!newPostsBtnShown)
|
||||
return;
|
||||
newPostsBtnShown=false;
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, .8f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
currentNewPostsAnim=set;
|
||||
set.start();
|
||||
}
|
||||
|
||||
private void onNewPostsBtnClick(View v){
|
||||
if(newPostsBtnShown){
|
||||
hideNewPostsButton();
|
||||
scrollToTop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
|
||||
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateState(ev.state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public interface IsOnTop {
|
||||
boolean isOnTop();
|
||||
|
||||
default boolean isRecyclerViewOnTop(RecyclerView list) {
|
||||
return !list.canScrollVertically(-1);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,44 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.media.MediaRouter;
|
||||
import android.os.Bundle;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.GetList;
|
||||
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
|
||||
public class ListTimelineFragment extends StatusListFragment {
|
||||
public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
private String listID;
|
||||
private String listTitle;
|
||||
private ListTimeline.RepliesPolicy repliesPolicy;
|
||||
private ImageButton fab;
|
||||
private Bundle resultArgs = new Bundle();
|
||||
|
||||
public ListTimelineFragment() {
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@@ -32,17 +47,85 @@ public class ListTimelineFragment extends StatusListFragment {
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
listID=getArguments().getString("listID");
|
||||
listTitle=getArguments().getString("listTitle");
|
||||
Bundle args = getArguments();
|
||||
listID = args.getString("listID");
|
||||
listTitle = args.getString("listTitle");
|
||||
repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
||||
resultArgs.putString("listID", listID);
|
||||
|
||||
setTitle(listTitle);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
new GetList(listID).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline listTimeline) {
|
||||
// TODO: save updated info
|
||||
if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title);
|
||||
if (!listTimeline.repliesPolicy.equals(repliesPolicy)) repliesPolicy = listTimeline.repliesPolicy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.list, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
// TODO: implement edit, delete
|
||||
// inflater.inflate(R.menu.list, menu);
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), menu, R.id.pin);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_edit_list_title)
|
||||
.setIcon(R.drawable.ic_fluent_people_list_28_regular)
|
||||
.setView(editor)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
new UpdateList(listID, editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
setTitle(list.title);
|
||||
listTitle = list.title;
|
||||
repliesPolicy = list.repliesPolicy;
|
||||
resultArgs.putString("listTitle", listTitle);
|
||||
resultArgs.putInt("repliesPolicy", repliesPolicy.ordinal());
|
||||
setResult(true, resultArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
} else if (item.getItemId() == R.id.delete) {
|
||||
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
|
||||
resultArgs.putBoolean("deleted", true);
|
||||
setResult(true, resultArgs);
|
||||
Nav.finish(this);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getResultArgs() {
|
||||
return resultArgs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TimelineDefinition makeTimelineDefinition() {
|
||||
return TimelineDefinition.ofList(listID, listTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,6 +152,7 @@ public class ListTimelineFragment extends StatusListFragment {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID));
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
|
||||
@@ -6,9 +6,7 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -17,32 +15,39 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
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.ListTimelineEditor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> implements ScrollableToTop {
|
||||
private static final int LIST_CHANGED_RESULT = 987;
|
||||
|
||||
private String accountId;
|
||||
private String profileAccountId;
|
||||
private String profileDisplayUsername;
|
||||
private HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||
private HashMap<String, Boolean> userInList = new HashMap<>();
|
||||
private int inProgress = 0;
|
||||
private ListsAdapter adapter;
|
||||
private boolean pinnedUpdated;
|
||||
|
||||
public ListTimelinesFragment() {
|
||||
super(10);
|
||||
@@ -53,12 +58,14 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle args=getArguments();
|
||||
accountId=args.getString("account");
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
if(args.containsKey("profileAccount")){
|
||||
profileAccountId=args.getString("profileAccount");
|
||||
profileDisplayUsername=args.getString("profileDisplayUsername");
|
||||
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
|
||||
// setHasOptionsMenu(true);
|
||||
} else {
|
||||
setTitle(R.string.sk_your_lists);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,20 +76,51 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
loadData();
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
// Button saveButton=new Button(getActivity());
|
||||
// saveButton.setText(R.string.save);
|
||||
// saveButton.setOnClickListener(this::onSaveClick);
|
||||
// LinearLayout wrap=new LinearLayout(getActivity());
|
||||
// wrap.setOrientation(LinearLayout.HORIZONTAL);
|
||||
// wrap.addView(saveButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
// wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
||||
// wrap.setClipToPadding(false);
|
||||
// MenuItem item=menu.add(R.string.save);
|
||||
// item.setActionView(wrap);
|
||||
// item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
// }
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (pinnedUpdated) UiUtils.restartApp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.create) {
|
||||
ListTimelineEditor editor = new ListTimelineEditor(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<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
saveListMembership(list.id, true);
|
||||
data.add(0, list);
|
||||
adapter.notifyItemRangeInserted(0, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountId);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveListMembership(String listId, boolean isMember) {
|
||||
userInList.put(listId, isMember);
|
||||
@@ -127,8 +165,30 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter() {
|
||||
return new ListsAdapter();
|
||||
public void onFragmentResult(int reqCode, boolean listChanged, Bundle result){
|
||||
if (reqCode == LIST_CHANGED_RESULT && listChanged) {
|
||||
if (result.getBoolean("pinnedUpdated")) pinnedUpdated = true;
|
||||
String listID = result.getString("listID");
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(listID)) {
|
||||
if (result.getBoolean("deleted")) {
|
||||
data.remove(i);
|
||||
adapter.notifyItemRemoved(i);
|
||||
} else {
|
||||
item.title = result.getString("listTitle", item.title);
|
||||
item.repliesPolicy = ListTimeline.RepliesPolicy.values()[result.getInt("repliesPolicy")];
|
||||
adapter.notifyItemChanged(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
|
||||
return adapter = new ListsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,7 +219,7 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
private final CheckBox listToggle;
|
||||
|
||||
public ListViewHolder(){
|
||||
super(getActivity(), R.layout.item_list_timeline, list);
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
listToggle=findViewById(R.id.list_toggle);
|
||||
}
|
||||
@@ -167,8 +227,10 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
title.setText(item.title);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_list_24_regular), null, null, null);
|
||||
if (profileAccountId != null) {
|
||||
Boolean checked = userInList.get(item.id);
|
||||
listToggle.setVisibility(View.VISIBLE);
|
||||
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
|
||||
listToggle.setOnClickListener(this::onClickToggle);
|
||||
} else {
|
||||
@@ -182,7 +244,12 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
UiUtils.openListTimeline(getActivity(), accountId, item);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountId);
|
||||
args.putString("listID", item.id);
|
||||
args.putString("listTitle", item.title);
|
||||
args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
|
||||
Nav.goForResult(getActivity(), ListTimelineFragment.class, args, LIST_CHANGED_RESULT, ListTimelinesFragment.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
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.events.FollowRequestHandledEvent;
|
||||
@@ -43,7 +44,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
|
||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment, postsFragment;
|
||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
||||
|
||||
private String accountID;
|
||||
|
||||
@@ -73,15 +74,26 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() != R.id.follow_requests) return false;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), FollowRequestsListFragment.class, args);
|
||||
return true;
|
||||
if (item.getItemId() == R.id.follow_requests) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), FollowRequestsListFragment.class, args);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.clear_notifications) {
|
||||
UiUtils.confirmDeleteNotification(getActivity(), accountID, null, ()->{
|
||||
for (int i = 0; i < tabViews.length; i++) {
|
||||
getFragmentForPage(i).reload();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,14 +102,14 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
UiUtils.reduceSwipeSensitivity(pager);
|
||||
|
||||
tabViews=new FrameLayout[3];
|
||||
tabViews=new FrameLayout[2];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.notifications_all;
|
||||
case 1 -> R.id.notifications_mentions;
|
||||
case 2 -> R.id.notifications_posts;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -109,6 +121,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
|
||||
pager.setOffscreenPageLimit(4);
|
||||
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
|
||||
pager.setAdapter(new DiscoverPagerAdapter());
|
||||
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
||||
@Override
|
||||
@@ -136,15 +149,9 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
mentionsFragment=new NotificationsListFragment();
|
||||
mentionsFragment.setArguments(args);
|
||||
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("onlyPosts", true);
|
||||
postsFragment=new NotificationsListFragment();
|
||||
postsFragment.setArguments(args);
|
||||
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(R.id.notifications_all, allNotificationsFragment)
|
||||
.add(R.id.notifications_mentions, mentionsFragment)
|
||||
.add(R.id.notifications_posts, postsFragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@@ -154,7 +161,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
tab.setText(switch(position){
|
||||
case 0 -> R.string.all_notifications;
|
||||
case 1 -> R.string.mentions;
|
||||
case 2 -> R.string.posts;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
tab.view.textView.setAllCaps(true);
|
||||
@@ -203,7 +209,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
return switch(page){
|
||||
case 0 -> allNotificationsFragment;
|
||||
case 1 -> mentionsFragment;
|
||||
case 2 -> postsFragment;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||
};
|
||||
}
|
||||
@@ -224,7 +229,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return 3;
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -10,7 +12,6 @@ import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.NotificationDeletedEvent;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
@@ -20,6 +21,7 @@ import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -40,6 +42,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
private boolean onlyMentions;
|
||||
private boolean onlyPosts;
|
||||
private String maxID;
|
||||
private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -78,9 +81,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
case FAVORITE -> getString(R.string.user_favorited);
|
||||
case POLL -> getString(R.string.poll_ended);
|
||||
};
|
||||
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText) : null;
|
||||
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText, n, null) : null;
|
||||
if(n.status!=null){
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null);
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n);
|
||||
if(titleItem!=null){
|
||||
for(StatusDisplayItem item:items){
|
||||
if(item instanceof ImageStatusDisplayItem imgItem){
|
||||
@@ -174,6 +177,7 @@ 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);
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
@@ -210,7 +214,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
}
|
||||
|
||||
private void removeNotification(Notification n){
|
||||
public void removeNotification(Notification n){
|
||||
data.remove(n);
|
||||
preloadedData.remove(n);
|
||||
int index=-1;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.Menu;
|
||||
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.model.TimelineDefinition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class PinnableStatusListFragment extends StatusListFragment {
|
||||
protected boolean pinnedUpdated;
|
||||
protected List<TimelineDefinition> pinnedTimelines;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
updatePinButton(menu.findItem(R.id.pin));
|
||||
}
|
||||
|
||||
protected boolean isPinned() {
|
||||
return pinnedTimelines.contains(makeTimelineDefinition());
|
||||
}
|
||||
|
||||
protected void updatePinButton(MenuItem pin) {
|
||||
boolean pinned = isPinned();
|
||||
pin.setIcon(pinned ?
|
||||
R.drawable.ic_fluent_pin_24_filled :
|
||||
R.drawable.ic_fluent_pin_24_regular);
|
||||
pin.setTitle(pinned ? R.string.sk_unpin_timeline : R.string.sk_pin_timeline);
|
||||
}
|
||||
|
||||
protected abstract TimelineDefinition makeTimelineDefinition();
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.pin) {
|
||||
togglePin(item);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
protected void togglePin(MenuItem pin) {
|
||||
pinnedUpdated = true;
|
||||
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
|
||||
TimelineDefinition def = makeTimelineDefinition();
|
||||
boolean pinned = isPinned();
|
||||
if (pinned) pinnedTimelines.remove(def);
|
||||
else pinnedTimelines.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();
|
||||
updatePinButton(pin);
|
||||
}
|
||||
|
||||
protected Bundle getResultArgs() {
|
||||
return new Bundle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
Bundle resultArgs = getResultArgs();
|
||||
if (pinnedUpdated) {
|
||||
resultArgs.putBoolean("pinnedUpdated", true);
|
||||
setResult(true, resultArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Outline;
|
||||
@@ -18,6 +19,8 @@ import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ImageSpan;
|
||||
@@ -161,6 +164,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
}
|
||||
|
||||
private String getPrefilledText() {
|
||||
return account == null || AccountSessionManager.getInstance().isSelf(accountID, account)
|
||||
? null : '@'+account.acct+' ';
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
@@ -233,7 +241,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
tabViews[i]=tabView;
|
||||
}
|
||||
|
||||
UiUtils.reduceSwipeSensitivity(pager);
|
||||
pager.setOffscreenPageLimit(5);
|
||||
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
|
||||
pager.setAdapter(new ProfilePagerAdapter());
|
||||
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
|
||||
|
||||
@@ -271,6 +281,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
cover.setOnClickListener(this::onCoverClick);
|
||||
refreshLayout.setOnRefreshListener(this);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(v->UiUtils.pickAccountForCompose(getActivity(), accountID, getPrefilledText()));
|
||||
|
||||
if(loaded){
|
||||
bindHeaderView();
|
||||
@@ -284,14 +295,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||
|
||||
username.setOnLongClickListener(v->{
|
||||
String username=account.acct;
|
||||
if(!username.contains("@")){
|
||||
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
}
|
||||
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
|
||||
Toast.makeText(getActivity(), R.string.text_copied, Toast.LENGTH_SHORT).show();
|
||||
String usernameString=account.acct;
|
||||
if(!usernameString.contains("@")){
|
||||
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
}
|
||||
UiUtils.copyText(username, '@'+usernameString);
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -548,21 +556,28 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(relationship==null && !isOwnProfile)
|
||||
return;
|
||||
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
||||
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.bookmarks, R.id.followed_hashtags);
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
|
||||
if(isOwnProfile)
|
||||
return;
|
||||
|
||||
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
|
||||
MenuItem mute = menu.findItem(R.id.mute);
|
||||
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
||||
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(getContext(), mute);
|
||||
|
||||
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
|
||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
|
||||
menu.findItem(R.id.manage_user_lists).setVisible(relationship.following);
|
||||
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
|
||||
if(relationship.following) {
|
||||
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getDisplayUsername()));
|
||||
manageUserLists.setVisible(true);
|
||||
MenuItem hideBoosts = menu.findItem(R.id.hide_boosts);
|
||||
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername()));
|
||||
hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(getContext(), hideBoosts);
|
||||
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
|
||||
}else {
|
||||
menu.findItem(R.id.hide_boosts).setVisible(false);
|
||||
manageUserLists.setVisible(false);
|
||||
}
|
||||
if(!account.isLocal())
|
||||
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
@@ -582,6 +597,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
confirmToggleMuted();
|
||||
}else if(id==R.id.block){
|
||||
confirmToggleBlocked();
|
||||
}else if(id==R.id.soft_block){
|
||||
confirmSoftBlockUser();
|
||||
}else if(id==R.id.report){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -620,9 +637,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}else if(id==R.id.manage_user_lists){
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("profileAccount", profileAccountID);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
if (!isOwnProfile) {
|
||||
args.putString("profileAccount", profileAccountID);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
}
|
||||
Nav.go(getActivity(), ListTimelinesFragment.class, args);
|
||||
}else if(id==R.id.followed_hashtags){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), FollowedHashtagsFragment.class, args);
|
||||
}else if(id==R.id.scheduled){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), ScheduledStatusListFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -878,6 +905,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
|
||||
}
|
||||
|
||||
private void confirmSoftBlockUser(){
|
||||
UiUtils.confirmSoftBlockUser(getActivity(), accountID, account, this::updateRelationship);
|
||||
}
|
||||
|
||||
private void updateRelationship(Relationship r){
|
||||
relationship=r;
|
||||
updateRelationship();
|
||||
@@ -934,9 +965,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
if(!AccountSessionManager.getInstance().isSelf(accountID, account)){
|
||||
args.putString("prefilledText", '@'+account.acct+' ');
|
||||
}
|
||||
if(getPrefilledText() != null) args.putString("prefilledText", getPrefilledText());
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetScheduledStatuses;
|
||||
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
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 me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ScheduledStatusListFragment extends BaseStatusListFragment<ScheduledStatus> {
|
||||
private String nextMaxID;
|
||||
private ImageButton fab;
|
||||
private static final int SCHEDULED_STATUS_LIST_OPENED = 161;
|
||||
|
||||
public ScheduledStatusListFragment() {
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.sk_unsent_posts);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
|
||||
fab.setOnClickListener(v -> Nav.go(getActivity(), ComposeFragment.class, args));
|
||||
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, args));
|
||||
if (getArguments().getBoolean("hide_fab", false)) fab.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
|
||||
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(ScheduledStatus s) {}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id) {
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
ScheduledStatus scheduledStatus = getStatusByID(id);
|
||||
Status status = scheduledStatus.toStatus();
|
||||
args.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
|
||||
args.putParcelable("editStatus", Parcels.wrap(status));
|
||||
args.putString("sourceText", status.text);
|
||||
args.putString("sourceSpoiler", status.spoilerText);
|
||||
args.putBoolean("redraftStatus", true);
|
||||
setResult(true, null);
|
||||
|
||||
// closing this scheduled status list if another status list is opened from compose fragment
|
||||
Nav.goForResult(getActivity(), ComposeFragment.class, args, SCHEDULED_STATUS_LIST_OPENED, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result) {
|
||||
if (reqCode == SCHEDULED_STATUS_LIST_OPENED && success && getActivity() != null) {
|
||||
Nav.finish(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetScheduledStatuses(offset==0 ? null : nextMaxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<ScheduledStatus> result){
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
// copied from StatusListFragment.java
|
||||
@Subscribe
|
||||
public void onScheduledStatusDeleted(ScheduledStatusDeletedEvent ev){
|
||||
if(!ev.accountID.equals(accountID)) return;
|
||||
ScheduledStatus status=getStatusByID(ev.id);
|
||||
if(status==null) return;
|
||||
removeStatus(status);
|
||||
}
|
||||
|
||||
// copied from StatusListFragment.java
|
||||
@Subscribe
|
||||
public void onScheduledStatusCreated(ScheduledStatusCreatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID)) return;
|
||||
prependItems(Collections.singletonList(ev.scheduledStatus), true);
|
||||
scrollToTop();
|
||||
}
|
||||
|
||||
// copied from StatusListFragment.java
|
||||
protected void removeStatus(ScheduledStatus status){
|
||||
data.remove(status);
|
||||
preloadedData.remove(status);
|
||||
int index=-1;
|
||||
for(int i=0;i<displayItems.size();i++){
|
||||
if(status.id.equals(displayItems.get(i).parentID)){
|
||||
index=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(index==-1)
|
||||
return;
|
||||
int lastIndex;
|
||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(status.id))
|
||||
break;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
// copied from StatusListFragment.java
|
||||
protected ScheduledStatus getStatusByID(String id){
|
||||
for(ScheduledStatus s:data){
|
||||
if(s.id.equals(id)){
|
||||
return s;
|
||||
}
|
||||
}
|
||||
for(ScheduledStatus s:preloadedData){
|
||||
if(s.id.equals(id)){
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -20,6 +21,7 @@ import android.view.animation.LinearInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
@@ -42,15 +44,18 @@ import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
@@ -58,6 +63,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
@@ -76,7 +83,8 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private PushSubscription pushSubscription;
|
||||
|
||||
private ImageView themeTransitionWindowView;
|
||||
private TextItem checkForUpdateItem;
|
||||
private TextItem checkForUpdateItem, clearImageCacheItem;
|
||||
private ImageCache imageCache;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -84,8 +92,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
setTitle(R.string.settings);
|
||||
imageCache = ImageCache.getInstance(getActivity());
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
String instanceName = UiUtils.getInstanceName(accountID);
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
@@ -98,11 +109,60 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
items.add(new HeaderItem(R.string.settings_theme));
|
||||
items.add(themeItem=new ThemeItem());
|
||||
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||
items.add(new ButtonItem(R.string.sk_settings_color_palette, R.drawable.ic_fluent_color_24_regular, b->{
|
||||
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.color_palettes);
|
||||
popupMenu.getMenu().findItem(R.id.m3_color).setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
|
||||
popupMenu.setOnMenuItemClickListener(SettingsFragment.this::onColorPreferenceClick);
|
||||
b.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
b.setOnClickListener(v->popupMenu.show());
|
||||
b.setText(switch(GlobalUserPreferences.color){
|
||||
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;
|
||||
});
|
||||
}));
|
||||
items.add(new ButtonItem(R.string.sk_settings_publish_button_text, R.drawable.ic_fluent_send_24_regular, b->{
|
||||
updatePublishText(b);
|
||||
|
||||
b.setOnClickListener(l->{
|
||||
TextInputFrameLayout input = new TextInputFrameLayout(
|
||||
getContext(),
|
||||
getString(R.string.publish),
|
||||
GlobalUserPreferences.publishButtonText.trim()
|
||||
);
|
||||
new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_publish_button_text_title).setView(input)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
GlobalUserPreferences.publishButtonText = input.getEditText().getText().toString().trim();
|
||||
GlobalUserPreferences.save();
|
||||
updatePublishText(b);
|
||||
})
|
||||
.setNeutralButton(R.string.clear, (d, which) -> {
|
||||
GlobalUserPreferences.publishButtonText = "";
|
||||
GlobalUserPreferences.save();
|
||||
updatePublishText(b);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
});
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_uniform_icon_for_notifications, R.drawable.ic_ntf_logo, GlobalUserPreferences.uniformNotificationIcon, i->{
|
||||
GlobalUserPreferences.uniformNotificationIcon=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_disable_marquee, R.drawable.ic_fluent_text_more_24_regular, GlobalUserPreferences.disableMarquee, i->{
|
||||
GlobalUserPreferences.disableMarquee=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new ColorPicker());
|
||||
items.add(new SwitchItem(R.string.sk_settings_reduce_motion, R.drawable.ic_fluent_star_emphasis_24_regular, GlobalUserPreferences.reduceMotion, i->{
|
||||
GlobalUserPreferences.reduceMotion=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_behavior));
|
||||
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
|
||||
@@ -121,6 +181,29 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
|
||||
GlobalUserPreferences.disableSwipe=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_enable_delete_notifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, GlobalUserPreferences.enableDeleteNotifications, i->{
|
||||
GlobalUserPreferences.enableDeleteNotifications=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_disable_alt_text_reminder, R.drawable.ic_fluent_image_alt_text_24_regular, GlobalUserPreferences.disableAltTextReminder, i->{
|
||||
GlobalUserPreferences.disableAltTextReminder=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{
|
||||
GlobalUserPreferences.translateButtonOpenedOnly=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
boolean translationAvailable = instance.v2 != null && instance.v2.configuration.translation != null && instance.v2.configuration.translation.enabled;
|
||||
items.add(new SmallTextItem(getString(translationAvailable ?
|
||||
R.string.sk_settings_translation_availability_note_available :
|
||||
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
|
||||
|
||||
items.add(new HeaderItem(R.string.home_timeline));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
|
||||
@@ -135,11 +218,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
GlobalUserPreferences.loadNewPosts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_federated_timeline, R.drawable.ic_fluent_earth_24_regular, GlobalUserPreferences.showFederatedTimeline, i->{
|
||||
GlobalUserPreferences.showFederatedTimeline=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_notifications));
|
||||
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
||||
@@ -147,30 +225,52 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_fluent_mention_24_regular, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
|
||||
items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_alert_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked)));
|
||||
items.add(new SwitchItem(R.string.sk_settings_single_notification, R.drawable.ic_fluent_convert_range_24_regular, GlobalUserPreferences.keepOnlyLatestNotification, i->{
|
||||
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_boring));
|
||||
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
|
||||
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
items.add(new HeaderItem(R.string.settings_account));
|
||||
items.add(new TextItem(R.string.sk_settings_profile, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/profile"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_posting, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/preferences/other"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_filters, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/filters"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular));
|
||||
|
||||
items.add(new RedHeaderItem(R.string.settings_spicy));
|
||||
items.add(new HeaderItem(instanceName));
|
||||
items.add(new TextItem(R.string.sk_settings_rules, ()->{
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}, R.drawable.ic_fluent_task_list_ltr_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_about_instance , ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/about"), R.drawable.ic_fluent_info_24_regular));
|
||||
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
|
||||
|
||||
items.add(new HeaderItem(R.string.sk_settings_about));
|
||||
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://ko-fi.com/xsk22"), R.drawable.ic_fluent_heart_24_regular));
|
||||
if (GithubSelfUpdater.needSelfUpdating()) {
|
||||
checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
|
||||
items.add(checkForUpdateItem);
|
||||
}
|
||||
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon")));
|
||||
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
|
||||
clearImageCacheItem = new TextItem(R.string.settings_clear_cache, UiUtils.formatFileSize(getContext(), imageCache.getDiskCache().size(), true), this::clearImageCache, 0);
|
||||
items.add(clearImageCacheItem);
|
||||
items.add(new TextItem(R.string.sk_clear_recent_languages, ()->UiUtils.showConfirmationAlert(getActivity(), R.string.sk_clear_recent_languages, R.string.sk_confirm_clear_recent_languages, R.string.clear, ()->{
|
||||
GlobalUserPreferences.recentLanguages.remove(accountID);
|
||||
GlobalUserPreferences.save();
|
||||
})));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
||||
|
||||
items.add(new FooterItem(getString(R.string.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
||||
}
|
||||
|
||||
private void updatePublishText(Button btn) {
|
||||
if (GlobalUserPreferences.publishButtonText.isBlank()) btn.setText(R.string.publish);
|
||||
else btn.setText(GlobalUserPreferences.publishButtonText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
@@ -216,11 +316,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
if(needAppRestart){
|
||||
Intent intent = Intent.makeRestartActivityTask(MastodonApp.context.getPackageManager().getLaunchIntentForPackage(MastodonApp.context.getPackageName()).getComponent());
|
||||
MastodonApp.context.startActivity(intent);
|
||||
Runtime.getRuntime().exit(0);
|
||||
}
|
||||
if(needAppRestart) UiUtils.restartApp();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -393,9 +489,13 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private void clearImageCache(){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
imageCache.clear();
|
||||
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(clearImageCacheItem)) instanceof TextViewHolder tvh) {
|
||||
clearImageCacheItem.secondaryText = UiUtils.formatFileSize(getContext(), 0, true);
|
||||
tvh.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -433,6 +533,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
this.text=getString(text);
|
||||
}
|
||||
|
||||
public HeaderItem(String text) {
|
||||
this.text=text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 0;
|
||||
@@ -467,7 +571,17 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
public class ColorPicker extends Item{
|
||||
public class ButtonItem extends Item{
|
||||
private int text;
|
||||
private int icon;
|
||||
private Consumer<Button> buttonConsumer;
|
||||
|
||||
public ButtonItem(@StringRes int text, @DrawableRes int icon, Consumer<Button> buttonConsumer) {
|
||||
this.text = text;
|
||||
this.icon = icon;
|
||||
this.buttonConsumer = buttonConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 8;
|
||||
@@ -490,19 +604,44 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
private class TextItem extends Item{
|
||||
private class SmallTextItem extends Item {
|
||||
private String text;
|
||||
private Runnable onClick;
|
||||
private boolean loading;
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick) {
|
||||
this(text, onClick, false);
|
||||
public SmallTextItem(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick, boolean loading){
|
||||
@Override
|
||||
public int getViewType() {
|
||||
return 9;
|
||||
}
|
||||
}
|
||||
|
||||
private class TextItem extends Item{
|
||||
private String text;
|
||||
private String secondaryText;
|
||||
private Runnable onClick;
|
||||
private boolean loading;
|
||||
private int icon;
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick) {
|
||||
this(text, null, onClick, false, 0);
|
||||
}
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick, @DrawableRes int icon) {
|
||||
this(text, null, onClick, false, icon);
|
||||
}
|
||||
|
||||
public TextItem(@StringRes int text, String secondaryText, Runnable onClick, @DrawableRes int icon) {
|
||||
this(text, secondaryText, onClick, false, icon);
|
||||
}
|
||||
|
||||
public TextItem(@StringRes int text, String secondaryText, Runnable onClick, boolean loading, @DrawableRes int icon){
|
||||
this.text=getString(text);
|
||||
this.onClick=onClick;
|
||||
this.loading=loading;
|
||||
this.icon=icon;
|
||||
this.secondaryText = secondaryText;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -558,7 +697,8 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
case 5 -> new HeaderViewHolder(true);
|
||||
case 6 -> new FooterViewHolder();
|
||||
case 7 -> new UpdateViewHolder();
|
||||
case 8 -> new ColorPickerViewHolder();
|
||||
case 8 -> new ButtonViewHolder();
|
||||
case 9 -> new SmallTextViewHolder();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||
};
|
||||
}
|
||||
@@ -687,37 +827,24 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
}
|
||||
private class ColorPickerViewHolder extends BindableViewHolder<ColorPicker>{
|
||||
private class ButtonViewHolder extends BindableViewHolder<ButtonItem>{
|
||||
private final Button button;
|
||||
private final PopupMenu popupMenu;
|
||||
private final ImageView icon;
|
||||
private final TextView text;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public ColorPickerViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_color_picker, list);
|
||||
public ButtonViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_button, list);
|
||||
text=findViewById(R.id.text);
|
||||
icon=findViewById(R.id.icon);
|
||||
button=findViewById(R.id.color_picker_button);
|
||||
popupMenu=new PopupMenu(getActivity(), button, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.color_picker);
|
||||
popupMenu.setOnMenuItemClickListener(SettingsFragment.this::onColorPreferenceClick);
|
||||
button.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
button.setOnClickListener(v->popupMenu.show());
|
||||
button=findViewById(R.id.button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ColorPicker item){
|
||||
icon.setImageResource(R.drawable.ic_fluent_color_24_regular);
|
||||
button.setText(switch(GlobalUserPreferences.color){
|
||||
case MATERIAL3 -> R.string.sk_color_theme_material3;
|
||||
case PINK -> R.string.sk_color_theme_pink;
|
||||
case PURPLE -> R.string.sk_color_theme_purple;
|
||||
case GREEN -> R.string.sk_color_theme_green;
|
||||
case BLUE -> R.string.sk_color_theme_blue;
|
||||
case BROWN -> R.string.sk_color_theme_brown;
|
||||
case RED -> R.string.sk_color_theme_red;
|
||||
case YELLOW -> R.string.sk_color_theme_yellow;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+GlobalUserPreferences.color);
|
||||
});
|
||||
public void onBind(ButtonItem item){
|
||||
text.setText(item.text);
|
||||
icon.setImageResource(item.icon);
|
||||
item.buttonConsumer.accept(button);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,19 +892,27 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
|
||||
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
private final TextView text, secondaryText;
|
||||
private final ProgressBar progress;
|
||||
private final ImageView icon;
|
||||
|
||||
public TextViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_text, list);
|
||||
text = itemView.findViewById(R.id.text);
|
||||
secondaryText = itemView.findViewById(R.id.secondary_text);
|
||||
progress = itemView.findViewById(R.id.progress);
|
||||
icon = itemView.findViewById(R.id.icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(TextItem item){
|
||||
icon.setVisibility(item.icon != 0 ? View.VISIBLE : View.GONE);
|
||||
secondaryText.setVisibility(item.secondaryText != null ? View.VISIBLE : View.GONE);
|
||||
|
||||
text.setText(item.text);
|
||||
progress.animate().alpha(item.loading ? 1 : 0);
|
||||
icon.setImageResource(item.icon);
|
||||
secondaryText.setText(item.secondaryText);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -786,6 +921,24 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
private class SmallTextViewHolder extends BindableViewHolder<SmallTextItem> {
|
||||
private final TextView text;
|
||||
;
|
||||
|
||||
public SmallTextViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_text, list);
|
||||
text = itemView.findViewById(R.id.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(SmallTextItem item){
|
||||
text.setText(item.text);
|
||||
text.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary));
|
||||
text.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
}
|
||||
}
|
||||
|
||||
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
|
||||
private final TextView text;
|
||||
public FooterViewHolder(){
|
||||
@@ -801,7 +954,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
|
||||
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
|
||||
|
||||
private final TextView text;
|
||||
private final TextView text, changelog;
|
||||
private final Button button;
|
||||
private final ImageButton cancelBtn;
|
||||
private final ProgressBar progress;
|
||||
@@ -812,6 +965,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
public UpdateViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_update, list);
|
||||
text=findViewById(R.id.text);
|
||||
changelog=findViewById(R.id.changelog);
|
||||
button=findViewById(R.id.button);
|
||||
cancelBtn=findViewById(R.id.cancel_btn);
|
||||
progress=findViewById(R.id.progress);
|
||||
@@ -855,6 +1009,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
progress.setVisibility(View.GONE);
|
||||
progress.removeCallbacks(progressUpdater);
|
||||
}
|
||||
changelog.setText(info.changelog);
|
||||
}
|
||||
|
||||
private void updateProgress(){
|
||||
|
||||
@@ -54,7 +54,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
|
||||
int idx=data.indexOf(s);
|
||||
if(idx>=0){
|
||||
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||
@@ -139,7 +139,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
action=getString(R.string.edit_multiple_changed);
|
||||
}
|
||||
}
|
||||
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null));
|
||||
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null, null));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -29,7 +30,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
protected EventListener eventListener=new EventListener();
|
||||
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true);
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,6 +172,12 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig){
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo();
|
||||
}
|
||||
|
||||
public class EventListener{
|
||||
|
||||
@Subscribe
|
||||
@@ -189,8 +196,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
}
|
||||
}
|
||||
for(Status s:preloadedData){
|
||||
if(s.id.equals(ev.id)){
|
||||
s.update(ev);
|
||||
if(s.getContentStatus().id.equals(ev.id)){
|
||||
s.getContentStatus().update(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
@@ -17,6 +16,7 @@ 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.UiUtils;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -92,16 +92,10 @@ public class ThreadFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
private List<Status> filterStatuses(List<Status> statuses){
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.THREAD)).collect(Collectors.toList());
|
||||
if(filters.isEmpty())
|
||||
return statuses;
|
||||
return statuses.stream().filter(status->{
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList());
|
||||
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD);
|
||||
return statuses.stream()
|
||||
.filter(statusFilterPredicate)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -225,6 +225,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
contextMenu=new PopupMenu(getActivity(), menuAnchor);
|
||||
contextMenu.inflate(R.menu.profile);
|
||||
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -283,29 +284,32 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getDisplayUsername())).setVisible(relationship.following);
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
|
||||
|
||||
MenuItem mute = menu.findItem(R.id.mute);
|
||||
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
||||
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(getContext(), mute);
|
||||
|
||||
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
|
||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
|
||||
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername())).setVisible(relationship.following);
|
||||
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername()));
|
||||
hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular);
|
||||
hideBoosts.setVisible(true);
|
||||
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getDisplayUsername()));
|
||||
UiUtils.insetPopupMenuIcon(getContext(), hideBoosts);
|
||||
|
||||
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
|
||||
manageUserLists.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
manageUserLists.setVisible(true);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
menu.findItem(R.id.block_domain).setVisible(false);
|
||||
|
||||
menuAnchor.setTranslationX(x);
|
||||
menuAnchor.setTranslationY(y);
|
||||
@@ -346,6 +350,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
||||
}else if(id==R.id.block){
|
||||
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
|
||||
}else if(id==R.id.soft_block){
|
||||
UiUtils.confirmSoftBlockUser(getActivity(), accountID, account, this::updateRelationship);
|
||||
}else if(id==R.id.report){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -48,7 +49,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop{
|
||||
public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop {
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
@@ -137,6 +138,11 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnTop() {
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
|
||||
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public AccountsAdapter(){
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
@@ -18,11 +17,10 @@ import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.fragments.ListTimelinesFragment;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
@@ -38,7 +36,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{
|
||||
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop {
|
||||
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager2 pager;
|
||||
@@ -55,15 +53,10 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
private DiscoverNewsFragment newsFragment;
|
||||
private DiscoverAccountsFragment accountsFragment;
|
||||
private SearchFragment searchFragment;
|
||||
private LocalTimelineFragment localTimelineFragment;
|
||||
private FederatedTimelineFragment federatedTimelineFragment;
|
||||
private ListTimelinesFragment listTimelinesFragment;
|
||||
|
||||
private String accountID;
|
||||
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||
|
||||
private final boolean noFederated = !GlobalUserPreferences.showFederatedTimeline;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -81,19 +74,15 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
|
||||
tabViews=new FrameLayout[noFederated ? 6 : 7];
|
||||
tabViews=new FrameLayout[4];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
int switchIndex = noFederated && i > 0 ? i + 1 : i;
|
||||
tabView.setId(switch(switchIndex){
|
||||
case 0 -> R.id.discover_local_timeline;
|
||||
case 1 -> R.id.discover_federated_timeline;
|
||||
case 2 -> R.id.discover_hashtags;
|
||||
case 3 -> R.id.discover_posts;
|
||||
case 4 -> R.id.discover_news;
|
||||
case 5 -> R.id.discover_users;
|
||||
case 6 -> R.id.discover_lists;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+switchIndex);
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.discover_hashtags;
|
||||
case 1 -> R.id.discover_posts;
|
||||
case 2 -> R.id.discover_news;
|
||||
case 3 -> R.id.discover_users;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
|
||||
@@ -103,7 +92,9 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
tabLayout.setTabTextSize(V.dp(16));
|
||||
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
|
||||
UiUtils.reduceSwipeSensitivity(pager);
|
||||
pager.setOffscreenPageLimit(4);
|
||||
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
|
||||
pager.setAdapter(new DiscoverPagerAdapter());
|
||||
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
||||
@Override
|
||||
@@ -118,7 +109,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
}
|
||||
});
|
||||
|
||||
if(localTimelineFragment==null){
|
||||
if(hashtagsFragment==null){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("__is_tab", true);
|
||||
@@ -135,41 +126,23 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
accountsFragment=new DiscoverAccountsFragment();
|
||||
accountsFragment.setArguments(args);
|
||||
|
||||
localTimelineFragment=new LocalTimelineFragment();
|
||||
localTimelineFragment.setArguments(args);
|
||||
|
||||
listTimelinesFragment=new ListTimelinesFragment();
|
||||
listTimelinesFragment.setArguments(args);
|
||||
|
||||
FragmentTransaction transaction = getChildFragmentManager().beginTransaction()
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(R.id.discover_posts, postsFragment)
|
||||
.add(R.id.discover_local_timeline, localTimelineFragment)
|
||||
.add(R.id.discover_hashtags, hashtagsFragment)
|
||||
.add(R.id.discover_news, newsFragment)
|
||||
.add(R.id.discover_users, accountsFragment)
|
||||
.add(R.id.discover_lists, listTimelinesFragment);
|
||||
|
||||
if (!noFederated) {
|
||||
federatedTimelineFragment=new FederatedTimelineFragment();
|
||||
federatedTimelineFragment.setArguments(args);
|
||||
transaction.add(R.id.discover_federated_timeline, federatedTimelineFragment);
|
||||
}
|
||||
|
||||
transaction.commit();
|
||||
.commit();
|
||||
}
|
||||
|
||||
tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){
|
||||
@Override
|
||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||
if (noFederated && position > 0) position++;
|
||||
tab.setText(switch(position){
|
||||
case 0 -> R.string.local_timeline;
|
||||
case 1 -> R.string.sk_federated_timeline;
|
||||
case 2 -> R.string.hashtags;
|
||||
case 3 -> R.string.posts;
|
||||
case 4 -> R.string.news;
|
||||
case 5 -> R.string.for_you;
|
||||
case 6 -> R.string.sk_list_timelines;
|
||||
case 0 -> R.string.hashtags;
|
||||
case 1 -> R.string.posts;
|
||||
case 2 -> R.string.news;
|
||||
case 3 -> R.string.for_you;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
tab.view.textView.setAllCaps(true);
|
||||
@@ -254,9 +227,26 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnTop() {
|
||||
return searchActive ? searchFragment.isOnTop()
|
||||
: ((IsOnTop)getFragmentForPage(pager.getCurrentItem())).isOnTop();
|
||||
}
|
||||
|
||||
public void onSelect() {
|
||||
if (isOnTop()) selectSearch();
|
||||
else scrollToTop();
|
||||
}
|
||||
|
||||
private void selectSearch() {
|
||||
searchEdit.requestFocus();
|
||||
onSearchEditFocusChanged(searchEdit, true);
|
||||
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
|
||||
}
|
||||
|
||||
public void loadData(){
|
||||
if(localTimelineFragment!=null && !localTimelineFragment.loaded && !localTimelineFragment.dataLoading)
|
||||
localTimelineFragment.loadData();
|
||||
if(hashtagsFragment!=null && !hashtagsFragment.loaded && !hashtagsFragment.dataLoading)
|
||||
hashtagsFragment.loadData();
|
||||
}
|
||||
|
||||
private void onSearchEditFocusChanged(View v, boolean hasFocus){
|
||||
@@ -291,15 +281,11 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
}
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
if (noFederated && page > 0) page++;
|
||||
return switch(page){
|
||||
case 0 -> localTimelineFragment;
|
||||
case 1 -> federatedTimelineFragment;
|
||||
case 2 -> hashtagsFragment;
|
||||
case 3 -> postsFragment;
|
||||
case 4 -> newsFragment;
|
||||
case 5 -> accountsFragment;
|
||||
case 6 -> listTimelinesFragment;
|
||||
case 0 -> hashtagsFragment;
|
||||
case 1 -> postsFragment;
|
||||
case 2 -> newsFragment;
|
||||
case 3 -> accountsFragment;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.model.Card;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
@@ -34,7 +35,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements ScrollableToTop{
|
||||
public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements ScrollableToTop, IsOnTop {
|
||||
private String accountID;
|
||||
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
|
||||
@@ -81,6 +82,11 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnTop() {
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
|
||||
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public LinksAdapter(){
|
||||
super(imgLoader);
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
@@ -12,16 +13,16 @@ import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class DiscoverPostsFragment extends StatusListFragment{
|
||||
public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop {
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetTrendingStatuses(count)
|
||||
currentRequest=new GetTrendingStatuses(offset, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, false);
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
@@ -31,4 +32,9 @@ public class DiscoverPostsFragment extends StatusListFragment{
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnTop() {
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package org.joinmastodon.android.fragments.discover;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.FabStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -15,7 +17,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FederatedTimelineFragment extends StatusListFragment{
|
||||
public class FederatedTimelineFragment extends FabStatusListFragment {
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
|
||||
private String maxID;
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package org.joinmastodon.android.fragments.discover;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.FabStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -15,7 +17,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class LocalTimelineFragment extends StatusListFragment{
|
||||
public class LocalTimelineFragment extends FabStatusListFragment {
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE);
|
||||
private String maxID;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -37,11 +38,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
public class SearchFragment extends BaseStatusListFragment<SearchResult> implements IsOnTop {
|
||||
private String currentQuery;
|
||||
private List<StatusDisplayItem> prevDisplayItems;
|
||||
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
|
||||
@@ -62,6 +62,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
loadData();
|
||||
setEmptyText(R.string.sk_recent_searches_placeholder);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,7 +76,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
return switch(s.type){
|
||||
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
|
||||
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
|
||||
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true);
|
||||
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,7 +174,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
return;
|
||||
}
|
||||
UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType());
|
||||
boolean recent=isInRecentMode();
|
||||
boolean recent=isInRecentMode() && !displayItems.isEmpty();
|
||||
if(recent!=headerAdapter.isVisible())
|
||||
headerAdapter.setVisible(recent);
|
||||
imgLoader.forceUpdateImages();
|
||||
@@ -299,6 +300,11 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnTop() {
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ProgressVisibilityListener{
|
||||
void onProgressVisibilityChanged(boolean visible);
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
@@ -23,7 +24,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop{
|
||||
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop {
|
||||
private String accountID;
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
|
||||
|
||||
@@ -66,6 +67,11 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnTop() {
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
|
||||
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -139,6 +139,7 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
|
||||
headerView.findViewById(R.id.visibility).setVisibility(View.GONE);
|
||||
headerView.findViewById(R.id.separator).setVisibility(View.GONE);
|
||||
headerView.findViewById(R.id.timestamp).setVisibility(View.GONE);
|
||||
headerView.findViewById(R.id.unread_indicator).setVisibility(View.GONE);
|
||||
((TextView) headerView.findViewById(R.id.username)).setText(R.string.sk_app_username);
|
||||
((TextView) headerView.findViewById(R.id.name)).setText(R.string.sk_app_name);
|
||||
((ImageView) headerView.findViewById(R.id.avatar)).setImageDrawable(getActivity().getDrawable(R.mipmap.ic_launcher));
|
||||
@@ -205,11 +206,6 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
|
||||
|
||||
public InstanceViewHolder(){
|
||||
super(getActivity(), R.layout.item_instance_custom, list);
|
||||
|
||||
// itemView.setPadding(V.dp(16), V.dp(16), V.dp(16), V.dp(16));
|
||||
// TypedValue value = new TypedValue();
|
||||
// getActivity().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, value, true);
|
||||
// itemView.setBackground(getActivity().getTheme().getDrawable(R.drawable.bg_search_field));
|
||||
title=findViewById(R.id.title);
|
||||
description=findViewById(R.id.description);
|
||||
userCount=findViewById(R.id.user_count);
|
||||
|
||||
@@ -65,7 +65,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(new ItemsAdapter());
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
@@ -77,8 +77,8 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
// setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
// view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -155,7 +155,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
}
|
||||
|
||||
private void onButtonClick(){
|
||||
if(!password.getText().equals(passwordConfirm.getText())){
|
||||
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
|
||||
passwordConfirm.setError(getString(R.string.signup_passwords_dont_match));
|
||||
passwordConfirmWrap.setErrorState();
|
||||
return;
|
||||
|
||||
@@ -237,7 +237,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
|
||||
for(StatusDisplayItem item:items){
|
||||
if(item instanceof ImageStatusDisplayItem isdi){
|
||||
isdi.horizontalInset=V.dp(40+32);
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -28,15 +29,17 @@ import java.util.ArrayList;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
private String accountID;
|
||||
private Account reportAccount;
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private View buttonBar, forwardReportItem;
|
||||
private TextView forwardReportText;
|
||||
private Switch forwardReportSwitch;
|
||||
private EditText commentEdit;
|
||||
private boolean forwardReport;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -77,7 +80,17 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
view.findViewById(R.id.btn_back).setOnClickListener(this::onButtonClick);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
commentEdit=view.findViewById(R.id.text);
|
||||
|
||||
forwardReportSwitch = view.findViewById(R.id.forward_report_switch);
|
||||
forwardReportItem = view.findViewById(R.id.forward_report);
|
||||
forwardReportText = view.findViewById(R.id.forward_report_text);
|
||||
String domain = reportAccount.getDomain();
|
||||
if (domain == null) {
|
||||
forwardReportItem.setVisibility(View.GONE);
|
||||
} else {
|
||||
forwardReportItem.setOnClickListener(this::onForwardReportClick);
|
||||
forwardReportText.setText(getActivity().getString(R.string.sk_forward_report_to, domain));
|
||||
forwardReportSwitch.setChecked(forwardReport = true);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -102,7 +115,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
ReportReason reason=ReportReason.valueOf(getArguments().getString("reason"));
|
||||
ArrayList<String> statusIDs=getArguments().getStringArrayList("statusIDs");
|
||||
ArrayList<String> ruleIDs=getArguments().getStringArrayList("ruleIDs");
|
||||
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), true)
|
||||
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), forwardReport)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
@@ -123,6 +136,11 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onForwardReportClick(View v) {
|
||||
forwardReport = !forwardReport;
|
||||
forwardReportSwitch.setChecked(forwardReport);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
|
||||
if(ev.reportAccountID.equals(reportAccount.id))
|
||||
|
||||
@@ -164,6 +164,10 @@ public class Account extends BaseModel{
|
||||
return '@'+acct;
|
||||
}
|
||||
|
||||
public String getShortUsername() {
|
||||
return '@'+acct.split("@")[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Account{"+
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Parcel
|
||||
public class Announcement extends BaseModel implements DisplayItemsParent {
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public String content;
|
||||
public Instant startsAt;
|
||||
public Instant endsAt;
|
||||
public boolean published;
|
||||
public boolean allDay;
|
||||
public Instant publishedAt;
|
||||
public Instant updatedAt;
|
||||
public boolean read;
|
||||
public List<Emoji> emojis;
|
||||
public List<Mention> mentions;
|
||||
public List<Hashtag> tags;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Announcement{" +
|
||||
"id='" + id + '\'' +
|
||||
", content='" + content + '\'' +
|
||||
", startsAt=" + startsAt +
|
||||
", endsAt=" + endsAt +
|
||||
", published=" + published +
|
||||
", allDay=" + allDay +
|
||||
", publishedAt=" + publishedAt +
|
||||
", updatedAt=" + updatedAt +
|
||||
", read=" + read +
|
||||
", emojis=" + emojis +
|
||||
", mentions=" + mentions +
|
||||
", tags=" + tags +
|
||||
'}';
|
||||
}
|
||||
|
||||
public Status toStatus() {
|
||||
Status s = new Status();
|
||||
s.id = id;
|
||||
s.mediaAttachments = List.of();
|
||||
s.createdAt = startsAt != null ? startsAt : publishedAt;
|
||||
if (updatedAt != null) s.editedAt = updatedAt;
|
||||
s.content = s.text = content;
|
||||
s.spoilerText = "";
|
||||
s.visibility = StatusPrivacy.PUBLIC;
|
||||
s.mentions = List.of();
|
||||
s.tags = List.of();
|
||||
s.emojis = List.of();
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,11 @@ import org.parceler.Parcel;
|
||||
import org.parceler.ParcelConstructor;
|
||||
import org.parceler.ParcelProperty;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Parcel
|
||||
public class Attachment extends BaseModel{
|
||||
@RequiredField
|
||||
// @RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public Type type;
|
||||
@@ -85,6 +87,12 @@ public class Attachment extends BaseModel{
|
||||
if(placeholder!=null)
|
||||
blurhashPlaceholder=new BlurHashDrawable(placeholder, getWidth(), getHeight());
|
||||
}
|
||||
|
||||
if (id == null) {
|
||||
// akkoma servers doesn't provide IDs for attachments,
|
||||
// but IDs are needed by the AudioPlayerService
|
||||
id = "" + this.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,12 +6,14 @@ import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Parcel
|
||||
public class Filter extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@@ -21,6 +23,7 @@ public class Filter extends BaseModel{
|
||||
public Instant expiresAt;
|
||||
public boolean irreversible;
|
||||
public boolean wholeWord;
|
||||
public FilterAction filterAction;
|
||||
|
||||
@SerializedName("context")
|
||||
private List<FilterContext> _context;
|
||||
@@ -76,4 +79,11 @@ public class Filter extends BaseModel{
|
||||
@SerializedName("thread")
|
||||
THREAD
|
||||
}
|
||||
|
||||
public enum FilterAction{
|
||||
@SerializedName("hide")
|
||||
HIDE,
|
||||
@SerializedName("warn")
|
||||
WARN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.parceler.Parcel;
|
||||
|
||||
@Parcel
|
||||
public class FilterResult extends BaseModel {
|
||||
public Filter filter;
|
||||
}
|
||||
@@ -27,7 +27,7 @@ public class Instance extends BaseModel{
|
||||
/**
|
||||
* Admin-defined description of the Mastodon site.
|
||||
*/
|
||||
@RequiredField
|
||||
// @RequiredField
|
||||
public String description;
|
||||
/**
|
||||
* A shorter description defined by the admin.
|
||||
@@ -37,7 +37,7 @@ public class Instance extends BaseModel{
|
||||
/**
|
||||
* An email that may be contacted for any inquiries.
|
||||
*/
|
||||
@RequiredField
|
||||
// @RequiredField
|
||||
public String email;
|
||||
/**
|
||||
* The version of Mastodon installed on the instance.
|
||||
|
||||
@@ -57,6 +57,11 @@ public class Poll extends BaseModel{
|
||||
public String title;
|
||||
public Integer votesCount;
|
||||
|
||||
public Option() {}
|
||||
public Option(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Option{"+
|
||||
|
||||
@@ -23,6 +23,7 @@ public class PushSubscription extends BaseModel implements Cloneable{
|
||||
", endpoint='"+endpoint+'\''+
|
||||
", alerts="+alerts+
|
||||
", serverKey='"+serverKey+'\''+
|
||||
", policy="+policy+
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.model.Poll.Option;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Parcel
|
||||
public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public Instant scheduledAt;
|
||||
@RequiredField
|
||||
public Params params;
|
||||
@RequiredField
|
||||
public List<Attachment> mediaAttachments;
|
||||
|
||||
@Override
|
||||
public String getID() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class Params {
|
||||
@RequiredField
|
||||
public String text;
|
||||
public String spoilerText;
|
||||
@RequiredField
|
||||
public StatusPrivacy visibility;
|
||||
public long inReplyToId;
|
||||
public ScheduledPoll poll;
|
||||
public boolean sensitive;
|
||||
public boolean withRateLimit;
|
||||
public String language;
|
||||
public String idempotency;
|
||||
public String applicationId;
|
||||
public List<String> mediaIds;
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class ScheduledPoll {
|
||||
@RequiredField
|
||||
public String expiresIn;
|
||||
@RequiredField
|
||||
public List<String> options;
|
||||
public boolean multiple;
|
||||
public boolean hideTotals;
|
||||
|
||||
public Poll toPoll() {
|
||||
Poll p = new Poll();
|
||||
p.voted = true;
|
||||
p.emojis = List.of();
|
||||
p.ownVotes = List.of();
|
||||
p.multiple = multiple;
|
||||
p.options = options.stream().map(Option::new).collect(Collectors.toList());
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
public Status toStatus() {
|
||||
Status s = new Status();
|
||||
s.id = id;
|
||||
s.mediaAttachments = mediaAttachments;
|
||||
s.createdAt = scheduledAt;
|
||||
s.inReplyToId = params.inReplyToId > 0 ? "" + params.inReplyToId : null;
|
||||
s.content = s.text = params.text;
|
||||
s.spoilerText = params.spoilerText;
|
||||
s.visibility = params.visibility;
|
||||
s.language = params.language;
|
||||
s.sensitive = params.sensitive;
|
||||
s.mentions = List.of();
|
||||
s.tags = List.of();
|
||||
s.emojis = List.of();
|
||||
if (params.poll != null) s.poll = params.poll.toPoll();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
public long favouritesCount;
|
||||
public long repliesCount;
|
||||
public Instant editedAt;
|
||||
public List<FilterResult> filtered;
|
||||
|
||||
public String url;
|
||||
public String inReplyToId;
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.HomeTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ListTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
||||
import org.joinmastodon.android.fragments.discover.FederatedTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class TimelineDefinition {
|
||||
private TimelineType type;
|
||||
private String title;
|
||||
private @Nullable Icon icon;
|
||||
|
||||
private @Nullable String listId;
|
||||
private @Nullable String listTitle;
|
||||
|
||||
private @Nullable String hashtagName;
|
||||
|
||||
public static TimelineDefinition ofList(String listId, String listTitle) {
|
||||
TimelineDefinition def = new TimelineDefinition(TimelineType.LIST, listTitle);
|
||||
def.listId = listId;
|
||||
def.listTitle = listTitle;
|
||||
return def;
|
||||
}
|
||||
|
||||
public static TimelineDefinition ofList(ListTimeline list) {
|
||||
return ofList(list.id, list.title);
|
||||
}
|
||||
|
||||
public static TimelineDefinition ofHashtag(String hashtag) {
|
||||
TimelineDefinition def = new TimelineDefinition(TimelineType.HASHTAG, hashtag);
|
||||
def.hashtagName = hashtag;
|
||||
return def;
|
||||
}
|
||||
|
||||
public static TimelineDefinition ofHashtag(Hashtag hashtag) {
|
||||
return ofHashtag(hashtag.name);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public TimelineDefinition() {}
|
||||
|
||||
public TimelineDefinition(TimelineType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public TimelineDefinition(TimelineType type, String title) {
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getTitle(Context ctx) {
|
||||
return title != null ? title : getDefaultTitle(ctx);
|
||||
}
|
||||
|
||||
public String getCustomTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title == null || title.isBlank() ? null : title;
|
||||
}
|
||||
|
||||
public String getDefaultTitle(Context ctx) {
|
||||
return switch (type) {
|
||||
case HOME -> ctx.getString(R.string.sk_timeline_home);
|
||||
case LOCAL -> ctx.getString(R.string.sk_timeline_local);
|
||||
case FEDERATED -> ctx.getString(R.string.sk_timeline_federated);
|
||||
case POST_NOTIFICATIONS -> ctx.getString(R.string.sk_timeline_posts);
|
||||
case LIST -> listTitle;
|
||||
case HASHTAG -> hashtagName;
|
||||
};
|
||||
}
|
||||
|
||||
public Icon getDefaultIcon() {
|
||||
return switch (type) {
|
||||
case HOME -> Icon.HOME;
|
||||
case LOCAL -> Icon.LOCAL;
|
||||
case FEDERATED -> Icon.FEDERATED;
|
||||
case POST_NOTIFICATIONS -> Icon.POST_NOTIFICATIONS;
|
||||
case LIST -> Icon.LIST;
|
||||
case HASHTAG -> Icon.HASHTAG;
|
||||
};
|
||||
}
|
||||
|
||||
public Fragment getFragment() {
|
||||
return switch (type) {
|
||||
case HOME -> new HomeTimelineFragment();
|
||||
case LOCAL -> new LocalTimelineFragment();
|
||||
case FEDERATED -> new FederatedTimelineFragment();
|
||||
case LIST -> new ListTimelineFragment();
|
||||
case HASHTAG -> new HashtagTimelineFragment();
|
||||
case POST_NOTIFICATIONS -> new NotificationsListFragment();
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Icon getIcon() {
|
||||
return icon == null ? getDefaultIcon() : icon;
|
||||
}
|
||||
|
||||
public void setIcon(@Nullable Icon icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public TimelineType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
TimelineDefinition that = (TimelineDefinition) o;
|
||||
if (type != that.type) return false;
|
||||
if (type == TimelineType.LIST) return Objects.equals(listId, that.listId);
|
||||
if (type == TimelineType.HASHTAG) return Objects.equals(hashtagName.toLowerCase(), that.hashtagName.toLowerCase());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = type.ordinal();
|
||||
result = 31 * result + (listId != null ? listId.hashCode() : 0);
|
||||
result = 31 * result + (hashtagName.toLowerCase() != null ? hashtagName.toLowerCase().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
public TimelineDefinition copy() {
|
||||
TimelineDefinition def = new TimelineDefinition(type, title);
|
||||
def.listId = listId;
|
||||
def.listTitle = listTitle;
|
||||
def.hashtagName = hashtagName;
|
||||
def.icon = icon == null ? null : Icon.values()[icon.ordinal()];
|
||||
return def;
|
||||
}
|
||||
|
||||
public Bundle populateArguments(Bundle args) {
|
||||
if (type == TimelineType.LIST) {
|
||||
args.putString("listTitle", title);
|
||||
args.putString("listID", listId);
|
||||
} else if (type == TimelineType.HASHTAG) {
|
||||
args.putString("hashtag", hashtagName);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG }
|
||||
|
||||
public enum Icon {
|
||||
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
|
||||
STAR(R.drawable.ic_fluent_star_24_regular, R.string.sk_icon_star),
|
||||
|
||||
HOME(R.drawable.ic_fluent_home_24_regular, R.string.sk_timeline_home, true),
|
||||
LOCAL(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true),
|
||||
FEDERATED(R.drawable.ic_fluent_earth_24_regular, R.string.sk_timeline_federated, true),
|
||||
POST_NOTIFICATIONS(R.drawable.ic_fluent_alert_24_regular, R.string.sk_timeline_posts, true),
|
||||
LIST(R.drawable.ic_fluent_people_list_24_regular, R.string.sk_list, true),
|
||||
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true);
|
||||
|
||||
public final int iconRes, nameRes;
|
||||
public final boolean hidden;
|
||||
|
||||
Icon(@DrawableRes int iconRes, @StringRes int nameRes) {
|
||||
this(iconRes, nameRes, false);
|
||||
}
|
||||
|
||||
Icon(@DrawableRes int iconRes, @StringRes int nameRes, boolean hidden) {
|
||||
this.iconRes = iconRes;
|
||||
this.nameRes = nameRes;
|
||||
this.hidden = hidden;
|
||||
}
|
||||
}
|
||||
|
||||
public static final TimelineDefinition HOME_TIMELINE = new TimelineDefinition(TimelineType.HOME);
|
||||
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
|
||||
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
|
||||
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
|
||||
|
||||
public static final List<TimelineDefinition> DEFAULT_TIMELINES = BuildConfig.BUILD_TYPE.equals("playRelease")
|
||||
? List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy())
|
||||
: List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy(), FEDERATED_TIMELINE.copy());
|
||||
public static final List<TimelineDefinition> ALL_TIMELINES = List.of(
|
||||
HOME_TIMELINE.copy(),
|
||||
LOCAL_TIMELINE.copy(),
|
||||
FEDERATED_TIMELINE.copy(),
|
||||
POSTS_TIMELINE.copy()
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
@@ -31,8 +32,16 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
||||
if(titleID!=0){
|
||||
View title=alert.findViewById(titleID);
|
||||
if(title!=null){
|
||||
int iconID=getContext().getResources().getIdentifier("icon", "id", "android");
|
||||
int alertTitleID=getContext().getResources().getIdentifier("alertTitle", "id", "android");
|
||||
if (alertTitleID != 0 && iconID != 0) {
|
||||
ImageView icon = title.findViewById(iconID);
|
||||
if (icon.getDrawable() != null) {
|
||||
title.findViewById(alertTitleID).setPadding(V.dp(8), 0, 0, 0);
|
||||
}
|
||||
}
|
||||
int pad=V.dp(24);
|
||||
title.setPadding(pad, pad, pad, pad);
|
||||
title.setPadding(pad, pad, pad, V.dp(12));
|
||||
}
|
||||
}
|
||||
int titleDividerID=getContext().getResources().getIdentifier("titleDividerNoCustom", "id", "android");
|
||||
|
||||
@@ -98,7 +98,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
|
||||
case DIRECT -> R.drawable.ic_at_symbol;
|
||||
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
@@ -49,7 +57,17 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
||||
private final TextView reply, boost, favorite, bookmark;
|
||||
private final ImageView share;
|
||||
private static final AnimationSet scaleDown, scaleUp;
|
||||
private static final Animation opacityOut, opacityIn;
|
||||
|
||||
private View touchingView = null;
|
||||
private boolean longClickPerformed = false;
|
||||
private final Runnable longClickRunnable = () -> {
|
||||
longClickPerformed = touchingView != null && touchingView.performLongClick();
|
||||
if (longClickPerformed && touchingView != null) {
|
||||
touchingView.startAnimation(opacityIn);
|
||||
touchingView.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
|
||||
}
|
||||
};
|
||||
|
||||
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
@@ -61,24 +79,13 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
};
|
||||
|
||||
static {
|
||||
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
|
||||
Animation scaleDownAnim = new ScaleAnimation(1, 0.85f, 1, 0.85f, Animation.ABSOLUTE, V.dp(20), Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
Animation scaleUpAnim = new ScaleAnimation(0.85f, 1, 0.85f, 1, Animation.ABSOLUTE, V.dp(20), Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
Animation opacityOutAnim = new AlphaAnimation(1, 0.75f);
|
||||
Animation opacityInAnim = new AlphaAnimation(0.75f, 1);
|
||||
|
||||
scaleDown = new AnimationSet(true);
|
||||
scaleDown.setDuration(350);
|
||||
scaleDown.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
scaleDown.setFillAfter(true);
|
||||
scaleDown.addAnimation(scaleDownAnim);
|
||||
scaleDown.addAnimation(opacityOutAnim);
|
||||
|
||||
scaleUp = new AnimationSet(true);
|
||||
scaleUp.setDuration(100);
|
||||
scaleUp.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
scaleUp.addAnimation(scaleUpAnim);
|
||||
scaleUp.addAnimation(opacityInAnim);
|
||||
opacityOut = new AlphaAnimation(1, 0.55f);
|
||||
opacityOut.setDuration(300);
|
||||
opacityOut.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
opacityOut.setFillAfter(true);
|
||||
opacityIn = new AlphaAnimation(0.55f, 1);
|
||||
opacityIn.setDuration(400);
|
||||
opacityIn.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
}
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
@@ -101,18 +108,23 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
View bookmark=findViewById(R.id.bookmark_btn);
|
||||
reply.setOnTouchListener(this::onButtonTouch);
|
||||
reply.setOnClickListener(this::onReplyClick);
|
||||
reply.setOnLongClickListener(this::onReplyLongClick);
|
||||
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
boost.setOnTouchListener(this::onButtonTouch);
|
||||
boost.setOnClickListener(this::onBoostClick);
|
||||
boost.setOnLongClickListener(this::onBoostLongClick);
|
||||
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
favorite.setOnTouchListener(this::onButtonTouch);
|
||||
favorite.setOnClickListener(this::onFavoriteClick);
|
||||
favorite.setOnLongClickListener(this::onFavoriteLongClick);
|
||||
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
bookmark.setOnTouchListener(this::onButtonTouch);
|
||||
bookmark.setOnClickListener(this::onBookmarkClick);
|
||||
bookmark.setOnLongClickListener(this::onBookmarkLongClick);
|
||||
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
share.setOnTouchListener(this::onButtonTouch);
|
||||
share.setOnClickListener(this::onShareClick);
|
||||
share.setOnLongClickListener(this::onShareLongClick);
|
||||
share.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
}
|
||||
|
||||
@@ -121,6 +133,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
bindButton(reply, item.status.repliesCount);
|
||||
bindButton(boost, item.status.reblogsCount);
|
||||
bindButton(favorite, item.status.favouritesCount);
|
||||
reply.setSelected(item.status.repliesCount > 0);
|
||||
boost.setSelected(item.status.reblogged);
|
||||
favorite.setSelected(item.status.favourited);
|
||||
bookmark.setSelected(item.status.bookmarked);
|
||||
@@ -138,52 +151,204 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onButtonTouch(View v, MotionEvent event){
|
||||
boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame &&
|
||||
parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled());
|
||||
int action = event.getAction();
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||
touchingView = null;
|
||||
v.removeCallbacks(longClickRunnable);
|
||||
if (!longClickPerformed) v.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
|
||||
if (disabled) return true;
|
||||
if (action == MotionEvent.ACTION_UP && !longClickPerformed) v.performClick();
|
||||
else if (!longClickPerformed) v.startAnimation(opacityIn);
|
||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||
longClickPerformed = false;
|
||||
touchingView = v;
|
||||
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
|
||||
v.setPivotX(V.dp(20));
|
||||
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
|
||||
if (disabled) return true;
|
||||
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
|
||||
v.startAnimation(opacityOut);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onReplyClick(View v){
|
||||
v.startAnimation(scaleUp);
|
||||
v.startAnimation(opacityIn);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
args.putParcelable("replyTo", Parcels.wrap(item.status));
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
private boolean onButtonTouch(View v, MotionEvent event){
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) v.performClick();
|
||||
else if (event.getAction() == MotionEvent.ACTION_DOWN) v.startAnimation(scaleDown);
|
||||
else if (event.getAction() == MotionEvent.ACTION_CANCEL) v.startAnimation(scaleUp);
|
||||
private boolean onReplyLongClick(View v) {
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||
UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> {
|
||||
Bundle args=new Bundle();
|
||||
String accountID = session.getID();
|
||||
args.putString("account", accountID);
|
||||
UiUtils.lookupStatus(v.getContext(), item.status, accountID, item.accountID, status -> {
|
||||
args.putParcelable("replyTo", Parcels.wrap(status));
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
});
|
||||
}, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onBoostClick(View v){
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, r->{
|
||||
v.startAnimation(scaleUp);
|
||||
boost.setSelected(item.status.reblogged);
|
||||
bindButton(boost, r.reblogsCount);
|
||||
boost.setSelected(!item.status.reblogged);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r));
|
||||
}
|
||||
|
||||
private void boostConsumer(View v, Status r) {
|
||||
v.startAnimation(opacityIn);
|
||||
bindButton(boost, r.reblogsCount);
|
||||
}
|
||||
|
||||
private boolean onBoostLongClick(View v){
|
||||
Context ctx = itemView.getContext();
|
||||
View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null);
|
||||
Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create();
|
||||
AccountSession session = AccountSessionManager.getInstance().getAccount(item.accountID);
|
||||
|
||||
Consumer<StatusPrivacy> doReblog = (visibility) -> {
|
||||
v.startAnimation(opacityOut);
|
||||
session.getStatusInteractionController()
|
||||
.setReblogged(item.status, !item.status.reblogged, visibility, r->boostConsumer(v, r));
|
||||
dialog.dismiss();
|
||||
};
|
||||
|
||||
View separator = menu.findViewById(R.id.separator);
|
||||
TextView reblogHeader = menu.findViewById(R.id.reblog_header);
|
||||
TextView undoReblog = menu.findViewById(R.id.delete_reblog);
|
||||
TextView reblogAs = menu.findViewById(R.id.reblog_as);
|
||||
TextView itemPublic = menu.findViewById(R.id.vis_public);
|
||||
TextView itemUnlisted = menu.findViewById(R.id.vis_unlisted);
|
||||
TextView itemFollowers = menu.findViewById(R.id.vis_followers);
|
||||
|
||||
undoReblog.setVisibility(item.status.reblogged ? View.VISIBLE : View.GONE);
|
||||
separator.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
|
||||
reblogHeader.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
|
||||
reblogAs.setVisibility(AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1 ? View.VISIBLE : View.GONE);
|
||||
|
||||
itemPublic.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PUBLIC) ? View.GONE : View.VISIBLE);
|
||||
itemUnlisted.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) ? View.GONE : View.VISIBLE);
|
||||
itemFollowers.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PRIVATE) ? View.GONE : View.VISIBLE);
|
||||
|
||||
Drawable checkMark = ctx.getDrawable(R.drawable.ic_fluent_checkmark_circle_20_regular);
|
||||
Drawable publicDrawable = ctx.getDrawable(R.drawable.ic_fluent_earth_24_regular);
|
||||
Drawable unlistedDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_community_24_regular);
|
||||
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_checkmark_24_regular);
|
||||
|
||||
StatusPrivacy defaultVisibility = session.preferences != null ? session.preferences.postingDefaultVisibility : null;
|
||||
// e.g. post visibility is unlisted, but default is public
|
||||
// in this case, we want to display the check mark on the most visible visibility
|
||||
if (defaultVisibility != null && item.status.visibility.isLessVisibleThan(defaultVisibility)) {
|
||||
for (StatusPrivacy vis : StatusPrivacy.values()) {
|
||||
if (vis.equals(item.status.visibility)) {
|
||||
defaultVisibility = vis;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
itemPublic.setCompoundDrawablesWithIntrinsicBounds(publicDrawable, null, StatusPrivacy.PUBLIC.equals(defaultVisibility) ? checkMark : null, null);
|
||||
itemUnlisted.setCompoundDrawablesWithIntrinsicBounds(unlistedDrawable, null, StatusPrivacy.UNLISTED.equals(defaultVisibility) ? checkMark : null, null);
|
||||
itemFollowers.setCompoundDrawablesWithIntrinsicBounds(followersDrawable, null, StatusPrivacy.PRIVATE.equals(defaultVisibility) ? checkMark : null, null);
|
||||
|
||||
undoReblog.setOnClickListener(c->doReblog.accept(null));
|
||||
itemPublic.setOnClickListener(c->doReblog.accept(StatusPrivacy.PUBLIC));
|
||||
itemUnlisted.setOnClickListener(c->doReblog.accept(StatusPrivacy.UNLISTED));
|
||||
itemFollowers.setOnClickListener(c->doReblog.accept(StatusPrivacy.PRIVATE));
|
||||
reblogAs.setOnClickListener(c->{
|
||||
dialog.dismiss();
|
||||
UiUtils.pickInteractAs(v.getContext(),
|
||||
item.accountID, item.status,
|
||||
s -> s.reblogged,
|
||||
(ic, status, consumer) -> ic.setReblogged(status, true, null, consumer),
|
||||
R.string.sk_reblog_as,
|
||||
R.string.sk_reblogged_as,
|
||||
R.string.sk_already_reblogged,
|
||||
// TODO: replace once available: https://raw.githubusercontent.com/microsoft/fluentui-system-icons/main/android/library/src/main/res/drawable/ic_fluent_arrow_repeat_all_28_regular.xml
|
||||
R.drawable.ic_fluent_arrow_repeat_all_24_regular
|
||||
);
|
||||
});
|
||||
|
||||
menu.findViewById(R.id.quote).setOnClickListener(c->{
|
||||
dialog.dismiss();
|
||||
v.startAnimation(opacityIn);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
StringBuilder prefilledText = new StringBuilder().append("\n\n");
|
||||
String ownID = AccountSessionManager.getInstance().getAccount(item.accountID).self.id;
|
||||
if (!item.status.account.id.equals(ownID)) prefilledText.append('@').append(item.status.account.acct).append(' ');
|
||||
prefilledText.append(item.status.url);
|
||||
args.putString("prefilledText", prefilledText.toString());
|
||||
args.putInt("selectionStart", 0);
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onFavoriteClick(View v){
|
||||
favorite.setSelected(!item.status.favourited);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
|
||||
v.startAnimation(scaleUp);
|
||||
favorite.setSelected(r.favourited);
|
||||
v.startAnimation(opacityIn);
|
||||
bindButton(favorite, r.favouritesCount);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean onFavoriteLongClick(View v) {
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||
UiUtils.pickInteractAs(v.getContext(),
|
||||
item.accountID, item.status,
|
||||
s -> s.favourited,
|
||||
(ic, status, consumer) -> ic.setFavorited(status, true, consumer),
|
||||
R.string.sk_favorite_as,
|
||||
R.string.sk_favorited_as,
|
||||
R.string.sk_already_favorited,
|
||||
R.drawable.ic_fluent_star_28_regular
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onBookmarkClick(View v){
|
||||
bookmark.setSelected(!item.status.bookmarked);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
|
||||
v.startAnimation(scaleUp);
|
||||
bookmark.setSelected(item.status.bookmarked);
|
||||
v.startAnimation(opacityIn);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean onBookmarkLongClick(View v) {
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||
UiUtils.pickInteractAs(v.getContext(),
|
||||
item.accountID, item.status,
|
||||
s -> s.bookmarked,
|
||||
(ic, status, consumer) -> ic.setBookmarked(status, true, consumer),
|
||||
R.string.sk_bookmark_as,
|
||||
R.string.sk_bookmarked_as,
|
||||
R.string.sk_already_bookmarked,
|
||||
R.drawable.ic_fluent_bookmark_28_regular
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onShareClick(View v){
|
||||
v.startAnimation(scaleUp);
|
||||
v.startAnimation(opacityIn);
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, item.status.url);
|
||||
v.getContext().startActivity(Intent.createChooser(intent, v.getContext().getString(R.string.share_toot_title)));
|
||||
}
|
||||
|
||||
private boolean onShareLongClick(View v){
|
||||
UiUtils.copyText(v, item.status.url);
|
||||
return true;
|
||||
}
|
||||
|
||||
private int descriptionForId(int id){
|
||||
if(id==R.id.reply_btn)
|
||||
return R.string.button_reply;
|
||||
|
||||
@@ -9,8 +9,10 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
@@ -22,16 +24,23 @@ import android.widget.Toast;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.announcements.DismissAnnouncement;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Announcement;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
@@ -39,8 +48,13 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
@@ -62,15 +76,22 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
private boolean hasVisibilityToggle;
|
||||
boolean needBottomPadding;
|
||||
private String extraText;
|
||||
private Notification notification;
|
||||
private ScheduledStatus scheduledStatus;
|
||||
private Announcement announcement;
|
||||
private Consumer<String> consumeReadAnnouncement;
|
||||
|
||||
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText){
|
||||
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText, Notification notification, ScheduledStatus scheduledStatus){
|
||||
super(parentID, parentFragment);
|
||||
user=scheduledStatus != null ? AccountSessionManager.getInstance().getAccount(accountID).self : user;
|
||||
this.user=user;
|
||||
this.createdAt=createdAt;
|
||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic, V.dp(50), V.dp(50));
|
||||
this.accountID=accountID;
|
||||
parsedName=new SpannableStringBuilder(user.displayName);
|
||||
this.status=status;
|
||||
this.notification=notification;
|
||||
this.scheduledStatus=scheduledStatus;
|
||||
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
|
||||
emojiHelper.setText(parsedName);
|
||||
if(status!=null){
|
||||
@@ -87,6 +108,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
this.extraText=extraText;
|
||||
}
|
||||
|
||||
public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer<String> consumeReadID) {
|
||||
HeaderStatusDisplayItem item = new HeaderStatusDisplayItem(a.id, instanceUser, a.startsAt, parentFragment, accountID, fakeStatus, null, null, null);
|
||||
item.announcement = a;
|
||||
item.consumeReadAnnouncement = consumeReadID;
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.HEADER;
|
||||
@@ -106,8 +134,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView name, username, timestamp, extraText;
|
||||
private final ImageView avatar, more, visibility;
|
||||
private final TextView name, username, timestamp, extraText, separator;
|
||||
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator;
|
||||
private final PopupMenu optionsMenu;
|
||||
private Relationship relationship;
|
||||
private APIRequest<?> currentRelationshipRequest;
|
||||
@@ -123,22 +151,31 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
super(activity, R.layout.display_item_header, parent);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
separator=findViewById(R.id.separator);
|
||||
timestamp=findViewById(R.id.timestamp);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
more=findViewById(R.id.more);
|
||||
visibility=findViewById(R.id.visibility);
|
||||
deleteNotification=findViewById(R.id.delete_notification);
|
||||
unreadIndicator=findViewById(R.id.unread_indicator);
|
||||
extraText=findViewById(R.id.extra_text);
|
||||
avatar.setOnClickListener(this::onAvaClick);
|
||||
avatar.setOutlineProvider(roundCornersOutline);
|
||||
avatar.setClipToOutline(true);
|
||||
more.setOnClickListener(this::onMoreClick);
|
||||
visibility.setOnClickListener(v->item.parentFragment.onVisibilityIconClick(this));
|
||||
deleteNotification.setOnClickListener(v->UiUtils.confirmDeleteNotification(activity, item.parentFragment.getAccountID(), item.notification, ()->{
|
||||
if (item.parentFragment instanceof NotificationsListFragment fragment) {
|
||||
fragment.removeNotification(item.notification);
|
||||
}
|
||||
}));
|
||||
|
||||
optionsMenu=new PopupMenu(activity, more);
|
||||
optionsMenu.inflate(R.menu.post);
|
||||
optionsMenu.setOnMenuItemClickListener(menuItem->{
|
||||
Account account=item.user;
|
||||
int id=menuItem.getItemId();
|
||||
|
||||
if(id==R.id.edit || id==R.id.delete_and_redraft) {
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
@@ -153,6 +190,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}else if(item.scheduledStatus!=null){
|
||||
args.putString("sourceText", item.status.text);
|
||||
args.putString("sourceSpoiler", item.status.spoilerText);
|
||||
args.putBoolean("redraftStatus", true);
|
||||
args.putParcelable("scheduledStatus", Parcels.wrap(item.scheduledStatus));
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}else{
|
||||
new GetStatusSourceText(item.status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@@ -178,8 +221,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
.exec(item.parentFragment.getAccountID());
|
||||
}
|
||||
}else if(id==R.id.delete){
|
||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||
}else if(id==R.id.pin || id==R.id.unpin){
|
||||
if (item.scheduledStatus != null) {
|
||||
UiUtils.confirmDeleteScheduledPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.scheduledStatus, ()->{});
|
||||
} else {
|
||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||
}
|
||||
}else if(id==R.id.pin || id==R.id.unpin) {
|
||||
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
|
||||
}else if(id==R.id.mute){
|
||||
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
|
||||
@@ -191,8 +238,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
args.putParcelable("status", Parcels.wrap(item.status));
|
||||
args.putParcelable("reportAccount", Parcels.wrap(item.status.account));
|
||||
Nav.go(item.parentFragment.getActivity(), ReportReasonChoiceFragment.class, args);
|
||||
}else if(id==R.id.open_in_browser){
|
||||
}else if(id==R.id.open_in_browser) {
|
||||
UiUtils.launchWebBrowser(activity, item.status.url);
|
||||
}else if(id==R.id.copy_link){
|
||||
UiUtils.copyText(parent, item.status.url);
|
||||
}else if(id==R.id.follow){
|
||||
if(relationship==null)
|
||||
return true;
|
||||
@@ -206,7 +255,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
progress.dismiss();
|
||||
}, rel->{
|
||||
relationship=rel;
|
||||
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : R.string.unfollowed_user, account.getDisplayUsername()), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : R.string.unfollowed_user, account.getShortUsername()), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}else if(id==R.id.block_domain){
|
||||
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
|
||||
@@ -215,17 +264,43 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(activity, optionsMenu);
|
||||
}
|
||||
|
||||
private void populateAccountsMenu(Menu menu) {
|
||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||
sessions.stream().filter(s -> !s.getID().equals(item.accountID)).forEach(s -> {
|
||||
String username = "@"+s.self.username+"@"+s.domain;
|
||||
menu.add(username).setOnMenuItemClickListener(c->{
|
||||
UiUtils.openURL(item.parentFragment.getActivity(), s.getID(), item.status.url, false);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(HeaderStatusDisplayItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText('@'+item.user.acct);
|
||||
if(item.status==null || item.status.editedAt==null)
|
||||
separator.setVisibility(View.VISIBLE);
|
||||
|
||||
if (item.scheduledStatus!=null)
|
||||
if (item.scheduledStatus.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT)) {
|
||||
timestamp.setText(R.string.sk_draft);
|
||||
} else {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||
timestamp.setText(item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter));
|
||||
}
|
||||
else if ((item.status==null || item.status.editedAt==null) && item.createdAt != null)
|
||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||
else
|
||||
else if (item.status != null && item.status.editedAt != null)
|
||||
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
|
||||
else {
|
||||
separator.setVisibility(View.GONE);
|
||||
timestamp.setText("");
|
||||
}
|
||||
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
|
||||
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification!=null && !item.inset ? View.VISIBLE : View.GONE);
|
||||
if(item.hasVisibilityToggle){
|
||||
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
|
||||
visibility.setContentDescription(item.parentFragment.getString(item.status.spoilerRevealed ? R.string.hide_content : R.string.reveal_content));
|
||||
@@ -247,6 +322,44 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
currentRelationshipRequest.cancel();
|
||||
}
|
||||
relationship=null;
|
||||
|
||||
String desc;
|
||||
if (item.announcement != null) {
|
||||
if (unreadIndicator.getVisibility() == View.GONE) {
|
||||
more.setAlpha(0f);
|
||||
unreadIndicator.setAlpha(0f);
|
||||
unreadIndicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
float alpha = item.announcement.read ? 0 : 1;
|
||||
more.setImageResource(R.drawable.ic_fluent_checkmark_20_filled);
|
||||
desc = item.parentFragment.getString(R.string.sk_mark_as_read);
|
||||
more.animate().alpha(alpha);
|
||||
unreadIndicator.animate().alpha(alpha);
|
||||
more.setEnabled(!item.announcement.read);
|
||||
more.setOnClickListener(v -> {
|
||||
if (item.announcement.read) return;
|
||||
new DismissAnnouncement(item.announcement.id).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {
|
||||
item.consumeReadAnnouncement.accept(item.announcement.id);
|
||||
item.announcement.read = true;
|
||||
rebind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(item.parentFragment.getActivity());
|
||||
}
|
||||
}).exec(item.accountID);
|
||||
});
|
||||
} else {
|
||||
more.setImageResource(R.drawable.ic_fluent_more_vertical_20_filled);
|
||||
desc = item.parentFragment.getString(R.string.more_options);
|
||||
more.setOnClickListener(this::onMoreClick);
|
||||
}
|
||||
|
||||
more.setContentDescription(desc);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) more.setTooltipText(desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -267,6 +380,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onAvaClick(View v){
|
||||
if (item.announcement != null) {
|
||||
UiUtils.openURL(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.user.url);
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.user));
|
||||
@@ -298,15 +415,31 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void updateOptionsMenu(){
|
||||
Account account=item.user;
|
||||
if (item.announcement != null) return;
|
||||
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
|
||||
Menu menu=optionsMenu.getMenu();
|
||||
|
||||
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
|
||||
SubMenu accountsMenu = openWithAccounts != null ? openWithAccounts.getSubMenu() : null;
|
||||
if (hasMultipleAccounts && accountsMenu != null) {
|
||||
openWithAccounts.setVisible(true);
|
||||
accountsMenu.clear();
|
||||
populateAccountsMenu(accountsMenu);
|
||||
} else if (openWithAccounts != null) {
|
||||
openWithAccounts.setVisible(false);
|
||||
}
|
||||
|
||||
Account account=item.user;
|
||||
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
|
||||
boolean isPostScheduled=item.scheduledStatus!=null;
|
||||
menu.findItem(R.id.open_with_account).setVisible(!isPostScheduled && hasMultipleAccounts);
|
||||
menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.delete_and_redraft).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
|
||||
menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned);
|
||||
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
|
||||
menu.findItem(R.id.delete_and_redraft).setVisible(!isPostScheduled && item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.pin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && !item.status.pinned);
|
||||
menu.findItem(R.id.unpin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && item.status.pinned);
|
||||
menu.findItem(R.id.open_in_browser).setVisible(!isPostScheduled && item.status!=null);
|
||||
menu.findItem(R.id.copy_link).setVisible(!isPostScheduled && item.status!=null);
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
MenuItem mute=menu.findItem(R.id.mute);
|
||||
MenuItem block=menu.findItem(R.id.block);
|
||||
@@ -322,7 +455,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
bookmark.setVisible(false);
|
||||
}
|
||||
*/
|
||||
if(isOwnPost){
|
||||
if(isPostScheduled || isOwnPost){
|
||||
mute.setVisible(false);
|
||||
block.setVisible(false);
|
||||
report.setVisible(false);
|
||||
@@ -333,16 +466,22 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
block.setVisible(true);
|
||||
report.setVisible(true);
|
||||
follow.setVisible(relationship==null || relationship.following || (!relationship.blocking && !relationship.blockedBy && !relationship.domainBlocking && !relationship.muting));
|
||||
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
report.setTitle(item.parentFragment.getString(R.string.report_user, account.getDisplayUsername()));
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setVisible(true);
|
||||
blockDomain.setTitle(item.parentFragment.getString(relationship!=null && relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
}else{
|
||||
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
||||
mute.setIcon(relationship!=null && relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), mute);
|
||||
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
|
||||
report.setTitle(item.parentFragment.getString(R.string.report_user, account.getShortUsername()));
|
||||
// disabled in megalodon. domain blocks from a post clutters the context menu and looks out of place
|
||||
// if(!account.isLocal()){
|
||||
// blockDomain.setVisible(true);
|
||||
// blockDomain.setTitle(item.parentFragment.getString(relationship!=null && relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
// }else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.getDisplayUsername()));
|
||||
// }
|
||||
boolean following = relationship!=null && relationship.following;
|
||||
follow.setTitle(item.parentFragment.getString(following ? R.string.unfollow_user : R.string.follow_user, account.getShortUsername()));
|
||||
follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,12 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
photo.setImageDrawable(null);
|
||||
if(item.imgRequest!=null){
|
||||
crossfadeDrawable.setSize(card.width, card.height);
|
||||
if (card.width > 0) {
|
||||
// akkoma servers don't provide width and height
|
||||
crossfadeDrawable.setSize(card.width, card.height);
|
||||
} else {
|
||||
crossfadeDrawable.setSize(itemView.getWidth(), itemView.getHeight());
|
||||
}
|
||||
crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder);
|
||||
crossfadeDrawable.setCrossfadeAlpha(item.status.spoilerRevealed ? 0f : 1f);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
@@ -95,7 +100,7 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onClick(View v){
|
||||
UiUtils.launchWebBrowser(itemView.getContext(), item.status.card.url);
|
||||
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
@@ -10,6 +21,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
|
||||
public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
|
||||
public PhotoStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
|
||||
@@ -23,9 +35,109 @@ public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends ImageStatusDisplayItem.Holder<PhotoStatusDisplayItem>{
|
||||
private final FrameLayout altTextWrapper;
|
||||
private final TextView altTextButton;
|
||||
private final View altTextScroller;
|
||||
private final ImageButton altTextClose;
|
||||
private final TextView altText;
|
||||
|
||||
private boolean altTextShown;
|
||||
private AnimatorSet currentAnim;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_photo, parent);
|
||||
altTextWrapper=findViewById(R.id.alt_text_wrapper);
|
||||
altTextButton=findViewById(R.id.alt_button);
|
||||
altTextScroller=findViewById(R.id.alt_text_scroller);
|
||||
altTextClose=findViewById(R.id.alt_text_close);
|
||||
altText=findViewById(R.id.alt_text);
|
||||
|
||||
altTextButton.setOnClickListener(this::onShowHideClick);
|
||||
altTextClose.setOnClickListener(this::onShowHideClick);
|
||||
// altTextScroller.setNestedScrollingEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ImageStatusDisplayItem item){
|
||||
super.onBind(item);
|
||||
altTextShown=false;
|
||||
if(currentAnim!=null)
|
||||
currentAnim.cancel();
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
altTextButton.setVisibility(View.VISIBLE);
|
||||
if(TextUtils.isEmpty(item.attachment.description)){
|
||||
altTextWrapper.setVisibility(View.GONE);
|
||||
}else{
|
||||
altTextWrapper.setVisibility(View.VISIBLE);
|
||||
altText.setText(item.attachment.description);
|
||||
}
|
||||
}
|
||||
|
||||
private void onShowHideClick(View v){
|
||||
boolean show=v.getId()==R.id.alt_button;
|
||||
|
||||
if(altTextShown==show)
|
||||
return;
|
||||
if(currentAnim!=null)
|
||||
currentAnim.cancel();
|
||||
|
||||
altTextShown=show;
|
||||
if(show){
|
||||
altTextScroller.setVisibility(View.VISIBLE);
|
||||
altTextClose.setVisibility(View.VISIBLE);
|
||||
}else{
|
||||
altTextButton.setVisibility(View.VISIBLE);
|
||||
// Hide these views temporarily so FrameLayout measures correctly
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// This is the current size...
|
||||
int prevLeft=altTextWrapper.getLeft();
|
||||
int prevRight=altTextWrapper.getRight();
|
||||
int prevTop=altTextWrapper.getTop();
|
||||
altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
// ...and this is after the layout pass, right now the FrameLayout has its final size, but we animate that change
|
||||
if(!show){
|
||||
// Show these views again so they're visible for the duration of the animation.
|
||||
// No one would notice they were missing during measure/layout.
|
||||
altTextScroller.setVisibility(View.VISIBLE);
|
||||
altTextClose.setVisibility(View.VISIBLE);
|
||||
}
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofInt(altTextWrapper, "left", prevLeft, altTextWrapper.getLeft()),
|
||||
ObjectAnimator.ofInt(altTextWrapper, "right", prevRight, altTextWrapper.getRight()),
|
||||
ObjectAnimator.ofInt(altTextWrapper, "top", prevTop, altTextWrapper.getTop()),
|
||||
ObjectAnimator.ofFloat(altTextButton, View.ALPHA, show ? 1f : 0f, show ? 0f : 1f),
|
||||
ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f),
|
||||
ObjectAnimator.ofFloat(altTextClose, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
if(show){
|
||||
altTextButton.setVisibility(View.GONE);
|
||||
}else{
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
}
|
||||
currentAnim=null;
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
currentAnim=set;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
}else{
|
||||
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
|
||||
button.setBackgroundResource(R.drawable.bg_poll_option_clickable);
|
||||
icon.setSelected(itemView.isSelected());
|
||||
icon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
import static org.joinmastodon.android.MastodonApp.context;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -14,6 +15,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -30,10 +32,13 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
private CharSequence text;
|
||||
@DrawableRes
|
||||
private int icon;
|
||||
private StatusPrivacy visibility;
|
||||
@DrawableRes
|
||||
private int iconEnd;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private View.OnClickListener handleClick;
|
||||
|
||||
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, @Nullable View.OnClickListener handleClick){
|
||||
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick){
|
||||
super(parentID, parentFragment);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(text);
|
||||
HtmlParser.parseCustomEmoji(ssb, emojis);
|
||||
@@ -43,6 +48,17 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
this.handleClick=handleClick;
|
||||
TypedValue outValue = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
|
||||
updateVisibility(visibility);
|
||||
}
|
||||
|
||||
public void updateVisibility(StatusPrivacy visibility) {
|
||||
this.visibility = visibility;
|
||||
this.iconEnd = visibility != null ? switch (visibility) {
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
|
||||
default -> 0;
|
||||
} : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,10 +86,18 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
@Override
|
||||
public void onBind(ReblogOrReplyLineStatusDisplayItem item){
|
||||
text.setText(item.text);
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, 0, 0);
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0);
|
||||
if(item.handleClick!=null) text.setOnClickListener(item.handleClick);
|
||||
text.setEnabled(!item.inset);
|
||||
text.setClickable(!item.inset);
|
||||
Context ctx = itemView.getContext();
|
||||
int visibilityText = item.visibility != null ? switch (item.visibility) {
|
||||
case PUBLIC -> R.string.visibility_public;
|
||||
case UNLISTED -> R.string.sk_visibility_unlisted;
|
||||
case PRIVATE -> R.string.visibility_followers_only;
|
||||
default -> 0;
|
||||
} : 0;
|
||||
if (visibilityText != 0) text.setContentDescription(item.text + " (" + ctx.getString(visibilityText) + ")");
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(text);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,16 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
@@ -73,28 +76,35 @@ public abstract class StatusDisplayItem{
|
||||
};
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter){
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false);
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){
|
||||
String parentID=parentObject.getID();
|
||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
Status statusForContent=status.getContentStatus();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
|
||||
|
||||
if(status.reblog!=null){
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, i->{
|
||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(status.account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}));
|
||||
}else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){
|
||||
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, i->{
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, null, i->{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}));
|
||||
}
|
||||
HeaderStatusDisplayItem header;
|
||||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
|
||||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, notification, scheduledStatus));
|
||||
if(!TextUtils.isEmpty(statusForContent.content))
|
||||
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent));
|
||||
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate));
|
||||
else
|
||||
header.needBottomPadding=true;
|
||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.widget.LinearLayout;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
@@ -24,6 +25,7 @@ import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.model.TranslatedStatus;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -31,6 +33,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.MovieDrawable;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
@@ -39,17 +42,16 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
private CharSequence parsedSpoilerText;
|
||||
public boolean textSelectable;
|
||||
public final Status status;
|
||||
public boolean disableTranslate;
|
||||
public boolean translated = false;
|
||||
public TranslatedStatus translation = null;
|
||||
|
||||
private AccountSession session;
|
||||
private Instance instanceInfo;
|
||||
private boolean translateEnabled;
|
||||
|
||||
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status){
|
||||
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){
|
||||
super(parentID, parentFragment);
|
||||
this.text=text;
|
||||
this.status=status;
|
||||
this.disableTranslate=disableTranslate;
|
||||
emojiHelper.setText(text);
|
||||
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||
parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
|
||||
@@ -57,8 +59,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
spoilerEmojiHelper.setText(parsedSpoilerText);
|
||||
}
|
||||
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
|
||||
instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
translateEnabled = instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,7 +85,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
private final LinearLayout spoilerHeader;
|
||||
private final TextView spoilerTitle, spoilerTitleInline, translateInfo;
|
||||
private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress;
|
||||
private final Drawable backgroundColor, borderColor;
|
||||
private final int backgroundColor, borderColor;
|
||||
private final Button translateButton;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
@@ -103,14 +103,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
translateInfo=findViewById(R.id.translate_info);
|
||||
translateProgress=findViewById(R.id.translate_progress);
|
||||
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
||||
|
||||
TypedValue outValue=new TypedValue();
|
||||
activity.getTheme().resolveAttribute(R.attr.colorBackgroundLight, outValue, true);
|
||||
backgroundColor=activity.getDrawable(outValue.resourceId);
|
||||
// activity.getTheme().resolveAttribute(R.attr.colorBackgroundLightest, outValue, true);
|
||||
// backgroundColorInset=activity.getDrawable(outValue.resourceId);
|
||||
activity.getTheme().resolveAttribute(R.attr.colorPollVoted, outValue, true);
|
||||
borderColor=activity.getDrawable(outValue.resourceId);
|
||||
backgroundColor=UiUtils.getThemeColor(activity, R.attr.colorBackgroundLight);
|
||||
borderColor=UiUtils.getThemeColor(activity, R.attr.colorPollVoted);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,10 +115,10 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
text.setTextIsSelectable(item.textSelectable);
|
||||
spoilerTitleInline.setTextIsSelectable(item.textSelectable);
|
||||
text.setInvalidateOnEveryFrame(false);
|
||||
spoilerTitleInline.setBackground(item.inset ? null : backgroundColor);
|
||||
spoilerTitleInline.setBackgroundColor(item.inset ? 0 : backgroundColor);
|
||||
spoilerTitleInline.setPadding(spoilerTitleInline.getPaddingLeft(), item.inset ? 0 : V.dp(14), spoilerTitleInline.getPaddingRight(), item.inset ? 0 : V.dp(14));
|
||||
borderTop.setBackground(item.inset ? null : borderColor);
|
||||
borderBottom.setBackground(item.inset ? null : borderColor);
|
||||
borderTop.setBackgroundColor(item.inset ? 0 : borderColor);
|
||||
borderBottom.setBackgroundColor(item.inset ? 0 : borderColor);
|
||||
if(!TextUtils.isEmpty(item.status.spoilerText)){
|
||||
spoilerTitle.setText(item.parsedSpoilerText);
|
||||
spoilerTitleInline.setText(item.parsedSpoilerText);
|
||||
@@ -146,7 +140,13 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
itemView.setClickable(false);
|
||||
}
|
||||
|
||||
translateWrap.setVisibility(item.textSelectable && item.translateEnabled &&
|
||||
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(item.session.domain);
|
||||
boolean translateEnabled = !item.disableTranslate && instanceInfo.v2 != null &&
|
||||
instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled;
|
||||
|
||||
translateWrap.setVisibility(
|
||||
(!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable) &&
|
||||
translateEnabled &&
|
||||
!item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) &&
|
||||
item.status.language != null &&
|
||||
(item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage))
|
||||
@@ -157,6 +157,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
if (item.translation == null) {
|
||||
translateProgress.setVisibility(View.VISIBLE);
|
||||
translateButton.setClickable(false);
|
||||
translateButton.animate().alpha(0.5f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
|
||||
new TranslateStatus(item.status.id).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(TranslatedStatus translatedStatus) {
|
||||
@@ -164,6 +165,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
item.translated = true;
|
||||
translateProgress.setVisibility(View.GONE);
|
||||
translateButton.setClickable(true);
|
||||
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
|
||||
rebind();
|
||||
}
|
||||
|
||||
@@ -171,6 +173,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onError(ErrorResponse error) {
|
||||
translateProgress.setVisibility(View.GONE);
|
||||
translateButton.setClickable(true);
|
||||
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
|
||||
error.showToast(itemView.getContext());
|
||||
}
|
||||
}).exec(item.parentFragment.getAccountID());
|
||||
|
||||
@@ -10,6 +10,8 @@ import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SoundEffectConstants;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.TextView;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
@@ -20,7 +22,11 @@ public class ClickableLinksDelegate {
|
||||
private Path hlPath;
|
||||
private LinkSpan selectedSpan;
|
||||
private TextView view;
|
||||
|
||||
|
||||
private final Runnable longClickRunnable = () -> {
|
||||
if (selectedSpan != null) selectedSpan.onLongClick(view);
|
||||
};
|
||||
|
||||
public ClickableLinksDelegate(TextView view) {
|
||||
this.view=view;
|
||||
hlPaint=new Paint();
|
||||
@@ -30,6 +36,7 @@ public class ClickableLinksDelegate {
|
||||
}
|
||||
|
||||
public boolean onTouch(MotionEvent event) {
|
||||
long eventDuration = event.getEventTime() - event.getDownTime();
|
||||
if(event.getAction()==MotionEvent.ACTION_DOWN){
|
||||
int line=-1;
|
||||
Rect rect=new Rect();
|
||||
@@ -63,6 +70,7 @@ public class ClickableLinksDelegate {
|
||||
}
|
||||
hlPath=new Path();
|
||||
selectedSpan=span;
|
||||
view.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
|
||||
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
|
||||
//l.getSelectionPath(start, end, hlPath);
|
||||
for(int j=lstart;j<=lend;j++){
|
||||
@@ -90,8 +98,11 @@ public class ClickableLinksDelegate {
|
||||
}
|
||||
}
|
||||
if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){
|
||||
view.playSoundEffect(SoundEffectConstants.CLICK);
|
||||
selectedSpan.onClick(view.getContext());
|
||||
if (eventDuration <= ViewConfiguration.getLongPressTimeout()) {
|
||||
view.playSoundEffect(SoundEffectConstants.CLICK);
|
||||
selectedSpan.onClick(view.getContext());
|
||||
}
|
||||
view.removeCallbacks(longClickRunnable);
|
||||
hlPath=null;
|
||||
selectedSpan=null;
|
||||
view.invalidate();
|
||||
@@ -100,6 +111,7 @@ public class ClickableLinksDelegate {
|
||||
if(event.getAction()==MotionEvent.ACTION_CANCEL){
|
||||
hlPath=null;
|
||||
selectedSpan=null;
|
||||
view.removeCallbacks(longClickRunnable);
|
||||
view.invalidate();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -111,14 +111,14 @@ public class HtmlParser{
|
||||
@Override
|
||||
public void head(@NonNull Node node, int depth){
|
||||
if(node instanceof TextNode textNode){
|
||||
ssb.append(textNode.text());
|
||||
ssb.append(textNode.getWholeText());
|
||||
}else if(node instanceof Element el){
|
||||
switch(el.nodeName()){
|
||||
case "a" -> {
|
||||
String href=el.attr("href");
|
||||
LinkSpan.Type linkType;
|
||||
String text=el.text();
|
||||
if(el.hasClass("hashtag")){
|
||||
String text=el.text();
|
||||
if(text.startsWith("#")){
|
||||
linkType=LinkSpan.Type.HASHTAG;
|
||||
href=text.substring(1);
|
||||
@@ -136,7 +136,7 @@ public class HtmlParser{
|
||||
}else{
|
||||
linkType=LinkSpan.Type.URL;
|
||||
}
|
||||
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID), ssb.length(), el));
|
||||
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID, text), ssb.length(), el));
|
||||
}
|
||||
case "br" -> ssb.append('\n');
|
||||
case "span" -> {
|
||||
@@ -182,7 +182,7 @@ public class HtmlParser{
|
||||
ssb.append("…", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}else if(blockElements.contains(el.nodeName()) && node.nextSibling()!=null){
|
||||
ssb.append("\n"); // line end
|
||||
ssb.append("\n", new RelativeSizeSpan(0.75f), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // margin after block
|
||||
ssb.append("\n", new RelativeSizeSpan(0.65f), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // margin after block
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,7 @@ public class HtmlParser{
|
||||
String url=matcher.group(3);
|
||||
if(TextUtils.isEmpty(matcher.group(4)))
|
||||
url="http://"+url;
|
||||
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null), matcher.start(3), matcher.end(3), 0);
|
||||
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null, url), matcher.start(3), matcher.end(3), 0);
|
||||
}while(matcher.find()); // Find more URLs
|
||||
return ssb;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.text;
|
||||
import android.content.Context;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
@@ -13,12 +14,14 @@ public class LinkSpan extends CharacterStyle {
|
||||
private String link;
|
||||
private Type type;
|
||||
private String accountID;
|
||||
private String text;
|
||||
|
||||
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){
|
||||
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, String text){
|
||||
this.listener=listener;
|
||||
this.link=link;
|
||||
this.type=type;
|
||||
this.accountID=accountID;
|
||||
this.text=text;
|
||||
}
|
||||
|
||||
public int getColor(){
|
||||
@@ -38,6 +41,10 @@ public class LinkSpan extends CharacterStyle {
|
||||
}
|
||||
}
|
||||
|
||||
public void onLongClick(View view) {
|
||||
UiUtils.copyText(view, getType() == Type.URL ? link : text);
|
||||
}
|
||||
|
||||
public String getLink(){
|
||||
return link;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.ThemePreference;
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.theme;
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.annotation.StyleRes;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ColorPalette {
|
||||
public static final Map<GlobalUserPreferences.ColorPreference, ColorPalette> palettes = Map.of(
|
||||
ColorPreference.MATERIAL3, new ColorPalette(R.style.ColorPalette_Material3)
|
||||
.dark(R.style.ColorPalette_Material3_Dark, R.style.ColorPalette_Material3_AutoLightDark),
|
||||
ColorPreference.PINK, new ColorPalette(R.style.ColorPalette_Pink),
|
||||
ColorPreference.PURPLE, new ColorPalette(R.style.ColorPalette_Purple),
|
||||
ColorPreference.GREEN, new ColorPalette(R.style.ColorPalette_Green),
|
||||
ColorPreference.BLUE, new ColorPalette(R.style.ColorPalette_Blue),
|
||||
ColorPreference.BROWN, new ColorPalette(R.style.ColorPalette_Brown),
|
||||
ColorPreference.RED, new ColorPalette(R.style.ColorPalette_Red),
|
||||
ColorPreference.YELLOW, new ColorPalette(R.style.ColorPalette_Yellow)
|
||||
);
|
||||
|
||||
private @StyleRes int base;
|
||||
private @StyleRes int autoDark;
|
||||
private @StyleRes int light;
|
||||
private @StyleRes int dark;
|
||||
private @StyleRes int black;
|
||||
private @StyleRes int autoBlack;
|
||||
|
||||
public ColorPalette(@StyleRes int baseRes) { base = baseRes; }
|
||||
|
||||
public ColorPalette(@StyleRes int lightRes, @StyleRes int darkRes, @StyleRes int autoDarkRes, @StyleRes int blackRes, @StyleRes int autoBlackRes) {
|
||||
light = lightRes;
|
||||
dark = darkRes;
|
||||
autoDark = autoDarkRes;
|
||||
black = blackRes;
|
||||
autoBlack = autoBlackRes;
|
||||
}
|
||||
|
||||
public ColorPalette light(@StyleRes int res) { light = res; return this; }
|
||||
public ColorPalette dark(@StyleRes int res, @StyleRes int auto) { dark = res; autoDark = auto; return this; }
|
||||
public ColorPalette black(@StyleRes int res, @StyleRes int auto) { dark = res; autoBlack = auto; return this; }
|
||||
|
||||
public void apply(Context context) {
|
||||
if (!((dark != 0 && autoDark != 0) || (black != 0 && autoBlack != 0) || light != 0 || base != 0)) {
|
||||
throw new IllegalStateException("Invalid color scheme definition");
|
||||
}
|
||||
|
||||
Resources.Theme t = context.getTheme();
|
||||
if (base != 0) t.applyStyle(base, true);
|
||||
if (light != 0 && theme.equals(ThemePreference.LIGHT)) t.applyStyle(light, true);
|
||||
else if (theme.equals(ThemePreference.DARK)) {
|
||||
if (dark != 0 && !trueBlackTheme) t.applyStyle(dark, true);
|
||||
else if (black != 0 && trueBlackTheme) t.applyStyle(black, true);
|
||||
} else if (theme.equals(ThemePreference.AUTO)) {
|
||||
if (autoDark != 0 && !trueBlackTheme) t.applyStyle(autoDark, true);
|
||||
else if (autoBlack != 0 && trueBlackTheme) t.applyStyle(autoBlack, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ public class DiscoverInfoBannerHelper{
|
||||
case TRENDING_LINKS -> R.string.trending_links_info_banner;
|
||||
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
|
||||
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
|
||||
case POST_NOTIFICATIONS -> R.string.sk_notify_posts_info_banner;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -61,6 +62,7 @@ public class DiscoverInfoBannerHelper{
|
||||
TRENDING_LINKS,
|
||||
LOCAL_TIMELINE,
|
||||
FEDERATED_TIMELINE,
|
||||
POST_NOTIFICATIONS,
|
||||
// ACCOUNTS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import static android.view.Menu.NONE;
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.theme;
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
@@ -20,11 +29,14 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.provider.Settings;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Button;
|
||||
@@ -36,23 +48,29 @@ import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.StatusInteractionController;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
|
||||
import org.joinmastodon.android.api.requests.accounts.AuthorizeFollowRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.RejectFollowRequest;
|
||||
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||
import org.joinmastodon.android.api.requests.notifications.DismissNotification;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.FollowRequestHandledEvent;
|
||||
import org.joinmastodon.android.events.NotificationDeletedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ListTimelineFragment;
|
||||
@@ -60,16 +78,22 @@ import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.joinmastodon.android.ui.text.SpacerSpan;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
@@ -80,14 +104,19 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -317,31 +346,30 @@ public class UiUtils{
|
||||
Nav.go((Activity)context, HashtagTimelineFragment.class, args);
|
||||
}
|
||||
|
||||
public static void openListTimeline(Context context, String accountID, ListTimeline list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("listID", list.id);
|
||||
args.putString("listTitle", list.title);
|
||||
Nav.go((Activity)context, ListTimelineFragment.class, args);
|
||||
}
|
||||
|
||||
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){
|
||||
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), onConfirmed);
|
||||
showConfirmationAlert(context, title, message, confirmButton, 0, onConfirmed);
|
||||
}
|
||||
|
||||
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, Runnable onConfirmed){
|
||||
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, Runnable onConfirmed){
|
||||
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), icon, onConfirmed);
|
||||
}
|
||||
|
||||
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, int icon, Runnable onConfirmed){
|
||||
new M3AlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(confirmButton, (dlg, i)->onConfirmed.run())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setIcon(icon)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback){
|
||||
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title),
|
||||
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName),
|
||||
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{
|
||||
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
|
||||
R.drawable.ic_fluent_person_prohibited_28_regular,
|
||||
()->{
|
||||
new SetAccountBlocked(account.id, !currentlyBlocked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -362,10 +390,44 @@ public class UiUtils{
|
||||
});
|
||||
}
|
||||
|
||||
public static void confirmSoftBlockUser(Activity activity, String accountID, Account account, Consumer<Relationship> resultCallback){
|
||||
showConfirmationAlert(activity,
|
||||
activity.getString(R.string.sk_remove_follower),
|
||||
activity.getString(R.string.sk_remove_follower_confirm, account.displayName),
|
||||
activity.getString(R.string.sk_do_remove_follower),
|
||||
R.drawable.ic_fluent_person_delete_24_regular,
|
||||
() -> new SetAccountBlocked(account.id, true).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Relationship relationship) {
|
||||
new SetAccountBlocked(account.id, false).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Relationship relationship) {
|
||||
Toast.makeText(activity, R.string.sk_remove_follower_success, Toast.LENGTH_SHORT).show();
|
||||
resultCallback.accept(relationship);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(activity);
|
||||
resultCallback.accept(relationship);
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(activity);
|
||||
}
|
||||
}).exec(accountID)
|
||||
);
|
||||
}
|
||||
|
||||
public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback){
|
||||
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_domain_title : R.string.confirm_block_domain_title),
|
||||
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, domain),
|
||||
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{
|
||||
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
|
||||
R.drawable.ic_fluent_shield_28_regular,
|
||||
()->{
|
||||
new SetDomainBlocked(domain, !currentlyBlocked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -386,7 +448,9 @@ public class UiUtils{
|
||||
public static void confirmToggleMuteUser(Activity activity, String accountID, Account account, boolean currentlyMuted, Consumer<Relationship> resultCallback){
|
||||
showConfirmationAlert(activity, activity.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title),
|
||||
activity.getString(currentlyMuted ? R.string.confirm_unmute : R.string.confirm_mute, account.displayName),
|
||||
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), ()->{
|
||||
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute),
|
||||
currentlyMuted ? R.drawable.ic_fluent_speaker_0_28_regular : R.drawable.ic_fluent_speaker_off_28_regular,
|
||||
()->{
|
||||
new SetAccountMuted(account.id, !currentlyMuted)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -411,24 +475,53 @@ public class UiUtils{
|
||||
}
|
||||
|
||||
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft){
|
||||
showConfirmationAlert(activity, forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title, forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete, forRedraft ? R.string.sk_delete_and_redraft : R.string.delete, ()->{
|
||||
new DeleteStatus(status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
resultCallback.accept(result);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
|
||||
E.post(new StatusDeletedEvent(status.id, accountID));
|
||||
}
|
||||
showConfirmationAlert(activity,
|
||||
forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title,
|
||||
forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete,
|
||||
forRedraft ? R.string.sk_delete_and_redraft : R.string.delete,
|
||||
forRedraft ? R.drawable.ic_fluent_arrow_clockwise_28_regular : R.drawable.ic_fluent_delete_28_regular,
|
||||
() -> new DeleteStatus(status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
resultCallback.accept(result);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
|
||||
E.post(new StatusDeletedEvent(status.id, accountID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.deleting, false)
|
||||
.exec(accountID);
|
||||
});
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.deleting, false)
|
||||
.exec(accountID)
|
||||
);
|
||||
}
|
||||
|
||||
public static void confirmDeleteScheduledPost(Activity activity, String accountID, ScheduledStatus status, Runnable resultCallback){
|
||||
boolean isDraft = status.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT);
|
||||
showConfirmationAlert(activity,
|
||||
isDraft ? R.string.sk_confirm_delete_draft_title : R.string.sk_confirm_delete_scheduled_post_title,
|
||||
isDraft ? R.string.sk_confirm_delete_draft : R.string.sk_confirm_delete_scheduled_post,
|
||||
R.string.delete,
|
||||
R.drawable.ic_fluent_delete_28_regular,
|
||||
() -> new DeleteStatus.Scheduled(status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object nothing){
|
||||
resultCallback.run();
|
||||
E.post(new ScheduledStatusDeletedEvent(status.id, accountID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.deleting, false)
|
||||
.exec(accountID)
|
||||
);
|
||||
}
|
||||
|
||||
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
|
||||
@@ -436,6 +529,7 @@ public class UiUtils{
|
||||
pinned ? R.string.sk_confirm_pin_post_title : R.string.sk_confirm_unpin_post_title,
|
||||
pinned ? R.string.sk_confirm_pin_post : R.string.sk_confirm_unpin_post,
|
||||
pinned ? R.string.sk_pin_post : R.string.sk_unpin_post,
|
||||
pinned ? R.drawable.ic_fluent_pin_28_regular : R.drawable.ic_fluent_pin_off_28_regular,
|
||||
()->{
|
||||
new SetStatusPinned(status.id, pinned)
|
||||
.setCallback(new Callback<>() {
|
||||
@@ -458,6 +552,47 @@ public class UiUtils{
|
||||
);
|
||||
}
|
||||
|
||||
public static void confirmDeleteNotification(Activity activity, String accountID, Notification notification, Runnable callback) {
|
||||
showConfirmationAlert(activity,
|
||||
notification == null ? R.string.sk_clear_all_notifications : R.string.sk_delete_notification,
|
||||
notification == null ? R.string.sk_clear_all_notifications_confirm : R.string.sk_delete_notification_confirm,
|
||||
notification == null ? R.string.sk_clear_all_notifications_confirm_action : R.string.sk_delete_notification_confirm_action,
|
||||
notification == null ? R.drawable.ic_fluent_mail_inbox_dismiss_28_regular : R.drawable.ic_fluent_delete_28_regular,
|
||||
() -> new DismissNotification(notification != null ? notification.id : null).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {
|
||||
callback.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(activity);
|
||||
}
|
||||
}).exec(accountID)
|
||||
);
|
||||
}
|
||||
|
||||
public static void confirmDeleteList(Activity activity, String accountID, String listID, String listTitle, Runnable callback) {
|
||||
showConfirmationAlert(activity,
|
||||
activity.getString(R.string.sk_delete_list),
|
||||
activity.getString(R.string.sk_delete_list_confirm, listTitle),
|
||||
activity.getString(R.string.delete),
|
||||
R.drawable.ic_fluent_delete_28_regular,
|
||||
() -> new DeleteList(listID).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {
|
||||
callback.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.deleting, false)
|
||||
.exec(accountID));
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButton(Relationship relationship, Button button){
|
||||
setRelationshipToActionButton(relationship, button, false);
|
||||
}
|
||||
@@ -625,6 +760,53 @@ public class UiUtils{
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static void insetPopupMenuIcon(Context context, MenuItem item) {
|
||||
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
|
||||
insetPopupMenuIcon(item, iconTint);
|
||||
}
|
||||
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint) {
|
||||
Drawable icon=item.getIcon().mutate();
|
||||
if(Build.VERSION.SDK_INT>=26) item.setIconTintList(iconTint);
|
||||
else icon.setTintList(iconTint);
|
||||
icon=new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0);
|
||||
item.setIcon(icon);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
|
||||
item.setTitle(ssb);
|
||||
}
|
||||
|
||||
public static void resetPopupItemTint(MenuItem item) {
|
||||
if(Build.VERSION.SDK_INT>=26) {
|
||||
item.setIconTintList(null);
|
||||
} else {
|
||||
Drawable icon=item.getIcon().mutate();
|
||||
icon.setTintList(null);
|
||||
item.setIcon(icon);
|
||||
}
|
||||
}
|
||||
|
||||
public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int... asAction) {
|
||||
if(menu.getClass().getSimpleName().equals("MenuBuilder")){
|
||||
try {
|
||||
Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
|
||||
m.setAccessible(true);
|
||||
m.invoke(menu, true);
|
||||
enableMenuIcons(context, menu, asAction);
|
||||
}
|
||||
catch(Exception ignored){}
|
||||
}
|
||||
}
|
||||
|
||||
public static void enableMenuIcons(Context context, Menu m, @IdRes int... exclude) {
|
||||
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
|
||||
for(int i=0;i<m.size();i++){
|
||||
MenuItem item=m.getItem(i);
|
||||
SubMenu subMenu = item.getSubMenu();
|
||||
if (subMenu != null) enableMenuIcons(context, subMenu, exclude);
|
||||
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId())) continue;
|
||||
insetPopupMenuIcon(item, iconTint);
|
||||
}
|
||||
}
|
||||
|
||||
public static void enablePopupMenuIcons(Context context, PopupMenu menu){
|
||||
Menu m=menu.getMenu();
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
@@ -636,123 +818,170 @@ public class UiUtils{
|
||||
setOptionalIconsVisible.invoke(m, true);
|
||||
}catch(Exception ignore){}
|
||||
}
|
||||
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
|
||||
for(int i=0;i<m.size();i++){
|
||||
MenuItem item=m.getItem(i);
|
||||
Drawable icon=item.getIcon().mutate();
|
||||
if(Build.VERSION.SDK_INT>=26){
|
||||
item.setIconTintList(iconTint);
|
||||
}else{
|
||||
icon.setTintList(iconTint);
|
||||
}
|
||||
icon=new InsetDrawable(icon, V.dp(8), 0, 0, 0);
|
||||
item.setIcon(icon);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
|
||||
ssb.insert(0, " ");
|
||||
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
|
||||
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
|
||||
item.setTitle(ssb);
|
||||
}
|
||||
enableMenuIcons(context, m);
|
||||
}
|
||||
|
||||
public static void setUserPreferredTheme(Context context){
|
||||
// boolean isDarkTheme = isDarkTheme();
|
||||
switch(GlobalUserPreferences.color){
|
||||
case PINK:
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
|
||||
case LIGHT ->
|
||||
R.style.Theme_Mastodon_Light;
|
||||
case DARK ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
|
||||
});
|
||||
break;
|
||||
case PURPLE:
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Original : R.style.Theme_Mastodon_AutoLightDark_Original;
|
||||
case LIGHT ->
|
||||
R.style.Theme_Mastodon_Light_Original;
|
||||
case DARK ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Original : R.style.Theme_Mastodon_Dark_Original;
|
||||
});
|
||||
break;
|
||||
case GREEN:
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Green : R.style.Theme_Mastodon_AutoLightDark_Green;
|
||||
case LIGHT ->
|
||||
R.style.Theme_Mastodon_Light_Green;
|
||||
case DARK ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Green : R.style.Theme_Mastodon_Dark_Green;
|
||||
});
|
||||
break;
|
||||
case BLUE:
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Blue : R.style.Theme_Mastodon_AutoLightDark_Blue;
|
||||
case LIGHT ->
|
||||
R.style.Theme_Mastodon_Light_Blue;
|
||||
case DARK ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Blue : R.style.Theme_Mastodon_Dark_Blue;
|
||||
});
|
||||
break;
|
||||
case BROWN:
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Brown : R.style.Theme_Mastodon_AutoLightDark_Brown;
|
||||
case LIGHT ->
|
||||
R.style.Theme_Mastodon_Light_Brown;
|
||||
case DARK ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Brown : R.style.Theme_Mastodon_Dark_Brown;
|
||||
});
|
||||
break;
|
||||
case RED:
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Red : R.style.Theme_Mastodon_AutoLightDark_Red;
|
||||
case LIGHT ->
|
||||
R.style.Theme_Mastodon_Light_Red;
|
||||
case DARK ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Red : R.style.Theme_Mastodon_Dark_Red;
|
||||
});
|
||||
break;
|
||||
case YELLOW:
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Yellow : R.style.Theme_Mastodon_AutoLightDark_Yellow;
|
||||
case LIGHT ->
|
||||
R.style.Theme_Mastodon_Light_Yellow;
|
||||
case DARK ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Yellow : R.style.Theme_Mastodon_Dark_Yellow;
|
||||
});
|
||||
break;
|
||||
case MATERIAL3:
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Material3 : R.style.Theme_Mastodon_AutoLightDark_Material3;
|
||||
case LIGHT ->
|
||||
R.style.Theme_Mastodon_Light_Material3;
|
||||
case DARK ->
|
||||
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Material3 : R.style.Theme_Mastodon_Dark_Material3;
|
||||
});
|
||||
break;
|
||||
}
|
||||
context.setTheme(switch (theme) {
|
||||
case LIGHT -> R.style.Theme_Mastodon_Light;
|
||||
case DARK -> trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
|
||||
default -> trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
|
||||
});
|
||||
|
||||
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
|
||||
if (palette != null) palette.apply(context);
|
||||
}
|
||||
public static boolean isDarkTheme(){
|
||||
if(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO)
|
||||
if(theme==GlobalUserPreferences.ThemePreference.AUTO)
|
||||
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;
|
||||
return GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK;
|
||||
return theme==GlobalUserPreferences.ThemePreference.DARK;
|
||||
}
|
||||
|
||||
public static void openURL(Context context, @Nullable String accountID, String url){
|
||||
// https://mastodon.foo.bar/@User
|
||||
// https://mastodon.foo.bar/@User/43456787654678
|
||||
// https://pleroma.foo.bar/users/User
|
||||
// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
|
||||
// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
|
||||
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
||||
// https://friendica.foo.bar/profile/user
|
||||
// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
||||
// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
|
||||
// https://pixelfed.social/p/connyduck/391263492998670833
|
||||
// https://pixelfed.social/connyduck
|
||||
// https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2
|
||||
// https://gts.foo.bar/@goblin
|
||||
// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
|
||||
//
|
||||
// COPIED FROM https://github.com/tuskyapp/Tusky/blob/develop/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
|
||||
public static boolean looksLikeMastodonUrl(String urlString) {
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(urlString);
|
||||
} catch (URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null) return false;
|
||||
|
||||
String it = uri.getPath();
|
||||
return it.matches("^/@[^/]+$") ||
|
||||
it.matches("^/@[^/]+/\\d+$") ||
|
||||
it.matches("^/users/\\w+$") ||
|
||||
it.matches("^/notice/[a-zA-Z0-9]+$") ||
|
||||
it.matches("^/objects/[-a-f0-9]+$") ||
|
||||
it.matches("^/notes/[a-z0-9]+$") ||
|
||||
it.matches("^/display/[-a-f0-9]+$") ||
|
||||
it.matches("^/profile/\\w+$") ||
|
||||
it.matches("^/p/\\w+/\\d+$") ||
|
||||
it.matches("^/\\w+$") ||
|
||||
it.matches("^/@[^/]+/statuses/[a-zA-Z0-9]+$") ||
|
||||
it.matches("^/users/[^/]+/statuses/[a-zA-Z0-9]+$") ||
|
||||
it.matches("^/o/[a-f0-9]+$");
|
||||
}
|
||||
|
||||
public static String getInstanceName(String accountID) {
|
||||
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
return instance != null && !instance.title.isBlank() ? instance.title : session.domain;
|
||||
}
|
||||
|
||||
public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
|
||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts()
|
||||
.stream().filter(s->!s.getID().equals(exceptFor)).collect(Collectors.toList());
|
||||
|
||||
AlertDialog.Builder builder = new M3AlertDialogBuilder(context)
|
||||
.setItems(
|
||||
sessions.stream().map(AccountSession::getFullUsername).toArray(String[]::new),
|
||||
(dialog, which) -> sessionConsumer.accept(sessions.get(which))
|
||||
)
|
||||
.setTitle(titleRes == 0 ? R.string.choose_account : titleRes)
|
||||
.setIcon(iconRes);
|
||||
if (transformDialog != null) transformDialog.accept(builder);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public static void restartApp() {
|
||||
Intent intent = Intent.makeRestartActivityTask(MastodonApp.context.getPackageManager().getLaunchIntentForPackage(MastodonApp.context.getPackageName()).getComponent());
|
||||
MastodonApp.context.startActivity(intent);
|
||||
Runtime.getRuntime().exit(0);
|
||||
}
|
||||
|
||||
public static MenuItem makeBackItem(Menu m) {
|
||||
MenuItem back = m.add(0, R.id.menu_back, NONE, R.string.back);
|
||||
back.setIcon(R.drawable.ic_arrow_back);
|
||||
return back;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface InteractionPerformer {
|
||||
void interact(StatusInteractionController ic, Status status, Consumer<Status> resultConsumer);
|
||||
}
|
||||
|
||||
public static void pickInteractAs(Context context, String accountID, Status sourceStatus, Predicate<Status> checkInteracted, InteractionPerformer interactionPerformer, @StringRes int interactAsRes, @StringRes int interactedAsAccountRes, @StringRes int alreadyInteractedRes, @DrawableRes int iconRes) {
|
||||
pickAccount(context, accountID, interactAsRes, iconRes, session -> {
|
||||
lookupStatus(context, sourceStatus, session.getID(), accountID, status -> {
|
||||
if (checkInteracted.test(status)) {
|
||||
Toast.makeText(context, alreadyInteractedRes, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
StatusInteractionController ic = AccountSessionManager.getInstance().getAccount(session.getID()).getRemoteStatusInteractionController();
|
||||
interactionPerformer.interact(ic, status, s -> {
|
||||
if (checkInteracted.test(s)) {
|
||||
Toast.makeText(context, context.getString(interactedAsAccountRes, session.getFullUsername()), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, null);
|
||||
}
|
||||
|
||||
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> statusConsumer) {
|
||||
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
|
||||
statusConsumer.accept(queryStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
new GetSearchResults(queryStatus.url, GetSearchResults.Type.STATUSES, true).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(SearchResults results) {
|
||||
if (!results.statuses.isEmpty()) statusConsumer.accept(results.statuses.get(0));
|
||||
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(context);
|
||||
}
|
||||
})
|
||||
.wrapProgress((Activity)context, R.string.loading, true,
|
||||
d -> transformDialogForLookup(context, targetAccountID, null, d))
|
||||
.exec(targetAccountID);
|
||||
}
|
||||
|
||||
public static void openURL(Context context, String accountID, String url) {
|
||||
openURL(context, accountID, url, true);
|
||||
}
|
||||
|
||||
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
|
||||
if (accountID != null) {
|
||||
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
|
||||
} else {
|
||||
dialog.setTitle(R.string.sk_loading_fediverse_resource_title);
|
||||
}
|
||||
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.cancel), (d, which) -> d.cancel());
|
||||
if (url != null) {
|
||||
dialog.setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.open_in_browser), (d, which) -> {
|
||||
d.cancel();
|
||||
launchWebBrowser(context, url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void openURL(Context context, String accountID, String url, boolean launchBrowser){
|
||||
Uri uri=Uri.parse(url);
|
||||
if(accountID!=null && "https".equals(uri.getScheme()) && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){
|
||||
List<String> path=uri.getPathSegments();
|
||||
// Match URLs like https://mastodon.social/@Gargron/108132679274083591
|
||||
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$")){
|
||||
List<String> path=uri.getPathSegments();
|
||||
if(accountID!=null && "https".equals(uri.getScheme())){
|
||||
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){
|
||||
new GetStatusByID(path.get(1))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -766,14 +995,103 @@ public class UiUtils{
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(context);
|
||||
launchWebBrowser(context, url);
|
||||
if (launchBrowser) launchWebBrowser(context, url);
|
||||
}
|
||||
})
|
||||
.wrapProgress((Activity)context, R.string.loading, true)
|
||||
.wrapProgress((Activity)context, R.string.loading, true,
|
||||
d -> transformDialogForLookup(context, accountID, url, d))
|
||||
.exec(accountID);
|
||||
return;
|
||||
} else if (looksLikeMastodonUrl(url)) {
|
||||
new GetSearchResults(url, null, true)
|
||||
.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(SearchResults results) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
if (!results.statuses.isEmpty()) {
|
||||
args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
|
||||
Nav.go((Activity) context, ThreadFragment.class, args);
|
||||
} else if (!results.accounts.isEmpty()) {
|
||||
args.putParcelable("profileAccount", Parcels.wrap(results.accounts.get(0)));
|
||||
Nav.go((Activity) context, ProfileFragment.class, args);
|
||||
} else {
|
||||
if (launchBrowser) launchWebBrowser(context, url);
|
||||
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(context);
|
||||
if (launchBrowser) launchWebBrowser(context, url);
|
||||
}
|
||||
})
|
||||
.wrapProgress((Activity)context, R.string.loading, true,
|
||||
d -> transformDialogForLookup(context, accountID, url, d))
|
||||
.exec(accountID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
launchWebBrowser(context, url);
|
||||
}
|
||||
|
||||
public static void copyText(View v, String text) {
|
||||
Context context = v.getContext();
|
||||
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, text));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
|
||||
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
v.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
|
||||
}
|
||||
|
||||
private static String getSystemProperty(String key){
|
||||
try{
|
||||
Class<?> props=Class.forName("android.os.SystemProperties");
|
||||
Method get=props.getMethod("get", String.class);
|
||||
return (String)get.invoke(null, key);
|
||||
}catch(Exception ignore){}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isMIUI(){
|
||||
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
|
||||
}
|
||||
|
||||
public static boolean pickAccountForCompose(Activity activity, String accountID, String prefilledText){
|
||||
Bundle args = new Bundle();
|
||||
if (prefilledText != null) args.putString("prefilledText", prefilledText);
|
||||
return pickAccountForCompose(activity, accountID, args);
|
||||
}
|
||||
|
||||
public static boolean pickAccountForCompose(Activity activity, String accountID){
|
||||
return pickAccountForCompose(activity, accountID, (String) null);
|
||||
}
|
||||
|
||||
public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args){
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1) {
|
||||
UiUtils.pickAccount(activity, accountID, 0, 0, session -> {
|
||||
args.putString("account", session.getID());
|
||||
Nav.go(activity, ComposeFragment.class, args);
|
||||
}, null);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/tuskyapp/Tusky/pull/3148
|
||||
public static void reduceSwipeSensitivity(ViewPager2 pager) {
|
||||
try {
|
||||
Field recyclerViewField = ViewPager2.class.getDeclaredField("mRecyclerView");
|
||||
recyclerViewField.setAccessible(true);
|
||||
RecyclerView recyclerView = (RecyclerView) recyclerViewField.get(pager);
|
||||
Field touchSlopField = RecyclerView.class.getDeclaredField("mTouchSlop");
|
||||
touchSlopField.setAccessible(true);
|
||||
int touchSlop = touchSlopField.getInt(recyclerView);
|
||||
touchSlopField.set(recyclerView, touchSlop * 3);
|
||||
} catch (Exception ex) {
|
||||
Log.e("reduceSwipeSensitivity", Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class ListTimelineEditor extends LinearLayout {
|
||||
private ListTimeline.RepliesPolicy policy = null;
|
||||
private final TextInputFrameLayout input;
|
||||
private final Button button;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public ListTimelineEditor(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
LayoutInflater.from(context).inflate(R.layout.list_timeline_editor, this);
|
||||
|
||||
button = findViewById(R.id.button);
|
||||
input = findViewById(R.id.input);
|
||||
|
||||
PopupMenu popupMenu = new PopupMenu(context, button, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.list_reply_policies);
|
||||
popupMenu.setOnMenuItemClickListener(this::onMenuItemClick);
|
||||
|
||||
button.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
button.setOnClickListener(v->popupMenu.show());
|
||||
input.getEditText().setHint(context.getString(R.string.sk_list_name_hint));
|
||||
|
||||
setRepliesPolicy(ListTimeline.RepliesPolicy.LIST);
|
||||
}
|
||||
|
||||
public void applyList(String title, ListTimeline.RepliesPolicy policy) {
|
||||
input.getEditText().setText(title);
|
||||
setRepliesPolicy(policy);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return input.getEditText().getText().toString();
|
||||
}
|
||||
|
||||
public ListTimeline.RepliesPolicy getRepliesPolicy() {
|
||||
return policy;
|
||||
}
|
||||
|
||||
public void setRepliesPolicy(ListTimeline.RepliesPolicy policy) {
|
||||
this.policy = policy;
|
||||
switch (policy) {
|
||||
case FOLLOWED -> button.setText(R.string.sk_list_replies_policy_followed);
|
||||
case LIST -> button.setText(R.string.sk_list_replies_policy_list);
|
||||
case NONE -> button.setText(R.string.sk_list_replies_policy_none);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onMenuItemClick(MenuItem i) {
|
||||
if (i.getItemId() == R.id.reply_policy_none) {
|
||||
setRepliesPolicy(ListTimeline.RepliesPolicy.NONE);
|
||||
} else if (i.getItemId() == R.id.reply_policy_followed) {
|
||||
setRepliesPolicy(ListTimeline.RepliesPolicy.FOLLOWED);
|
||||
} else if (i.getItemId() == R.id.reply_policy_list) {
|
||||
setRepliesPolicy(ListTimeline.RepliesPolicy.LIST);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public ListTimelineEditor(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public ListTimelineEditor(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ListTimelineEditor(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
public class NestableScrollView extends ScrollView{
|
||||
private float downY, touchslop;
|
||||
private boolean didDisallow;
|
||||
|
||||
public NestableScrollView(Context context){
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public NestableScrollView(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public NestableScrollView(Context context, AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public NestableScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init(){
|
||||
touchslop=ViewConfiguration.get(getContext()).getScaledTouchSlop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev){
|
||||
if(ev.getAction()==MotionEvent.ACTION_DOWN){
|
||||
if(canScrollVertically(-1) || canScrollVertically(1)){
|
||||
getParent().requestDisallowInterceptTouchEvent(true);
|
||||
didDisallow=true;
|
||||
}else{
|
||||
didDisallow=false;
|
||||
}
|
||||
downY=ev.getY();
|
||||
}else if(didDisallow && ev.getAction()==MotionEvent.ACTION_MOVE){
|
||||
if(Math.abs(downY-ev.getY())>=touchslop){
|
||||
if(!canScrollVertically((int)(downY-ev.getY()))){
|
||||
didDisallow=false;
|
||||
getParent().requestDisallowInterceptTouchEvent(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onTouchEvent(ev);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class TextInputFrameLayout extends FrameLayout {
|
||||
private final EditText editText;
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, CharSequence hint, CharSequence text) {
|
||||
this(context, null, 0, 0, hint, text);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
this(context, attrs, defStyleAttr, defStyleRes, null, null);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes, CharSequence hint, CharSequence text) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
editText = new EditText(context);
|
||||
editText.setHint(hint);
|
||||
editText.setText(text);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.setMargins(V.dp(24), V.dp(4), V.dp(24), V.dp(16));
|
||||
editText.setLayoutParams(params);
|
||||
addView(editText);
|
||||
}
|
||||
|
||||
public EditText getEditText() {
|
||||
return editText;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ public abstract class GithubSelfUpdater{
|
||||
|
||||
public static class UpdateInfo{
|
||||
public String version;
|
||||
public String changelog;
|
||||
public long size;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user